This commit is contained in:
2024-11-30 13:24:46 +07:00
commit 3e32e5869e
30 changed files with 1196 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.vscode/
.DS_Store
node_modules/
build/
dist/
temp/
package-lock.json
src/swagger/swagger.json
swagger.json

29
config/default.json Normal file
View File

@ -0,0 +1,29 @@
{
"app": {
"name": "Service Master Data",
"description": "Purpose of this service is to provide master data product, master data suco and etc",
"version": "1.0.0"
},
"server": {
"port": "4001",
"host": "0.0.0.0",
"host_swagger": "127.0.0.1",
"max_body": "50mb",
"max_upload": 104857600,
"morgan": "combined",
"cors": "*",
"ssl": {
"enable": false,
"cert": "",
"key": ""
}
},
"database": {
"engine": "postgres",
"host": "127.0.0.1",
"port": "5432",
"username": "postgres",
"password": "123",
"database": "bri_erp"
}
}

0
config/prod.json Normal file
View File

29
config/stag.json Normal file
View File

@ -0,0 +1,29 @@
{
"app": {
"name": "Service Master Data",
"description": "Purpose of this service is to provide master data product, master data suco and etc",
"version": "1.0.0"
},
"server": {
"port": "4001",
"host": "0.0.0.0",
"host_swagger": "127.0.0.1",
"max_body": "50mb",
"max_upload": 104857600,
"morgan": "combined",
"cors": "*",
"ssl": {
"enable": false,
"cert": "/etc/letsencrypt/live/bri-dev.shiblysolution.id/fullchain.pem",
"key": "/etc/letsencrypt/live/bri-dev.shiblysolution.id/privkey.pem"
}
},
"database": {
"engine": "postgres",
"host": "127.0.0.1",
"port": "5432",
"username": "postgres",
"password": "r00t@dm1n05",
"database": "bri_erp"
}
}

46
package.json Normal file
View File

@ -0,0 +1,46 @@
{
"name": "Service master data",
"version": "1.0.0",
"description": "Purpose of this service is to provide master data product, master data suco and etc",
"main": "src/index.ts",
"scripts": {
"dev": "nodemon",
"build": "rimraf dist && tsc",
"swagger": "node src/swagger/builder.js"
},
"keywords": [
"BRI"
],
"author": "STS",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"compression": "^1.7.5",
"config": "^3.3.12",
"cors": "^2.8.5",
"express": "^4.21.1",
"express-jwt": "^8.4.1",
"fast-csv": "^5.0.2",
"helmet": "^8.0.0",
"i": "^0.3.7",
"joi": "^17.13.3",
"morgan": "^1.10.0",
"npm": "^10.9.1",
"pg": "^8.13.1",
"reflect-metadata": "^0.2.2",
"swagger-autogen": "^2.23.7",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"tslog": "^4.9.3",
"typeorm": "^0.3.20"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/compression": "^1.7.5",
"@types/config": "^3.3.5",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/morgan": "^1.9.9",
"@types/swagger-ui-express": "^4.1.7"
}
}

526
src/controllers/user.ts Normal file
View File

@ -0,0 +1,526 @@
import { Response, NextFunction, Application } from "express";
import { Request } from "express-jwt";
import { ReturnHelper } from "../helpers/express/return";
import { OrmHelper } from "../helpers/orm";
import { User } from "../entity/users";
import Joi from "joi";
import { ILogObj, Logger } from "tslog";
import { Language } from "../langs/lang";
import { Paging } from "../types/paging";
import CommonHelper from "../helpers/common";
import * as fastcsv from 'fast-csv';
import dayjs from "dayjs";
const log: Logger<ILogObj> = new Logger({ name: '[UserController]', type: 'pretty' });
export class UserController {
static async list(req: Request, res: Response, next: NextFunction): Promise<Response> {
/*
#swagger.tags = ['User']
#swagger.parameters['filter'] = {
in: 'query',
type: 'string'
}
#swagger.parameters['limit'] = {
in: 'query',
required: true,
type: 'number'
}
#swagger.parameters['page'] = {
in: 'query',
required: true,
type: 'number'
}
#swagger.parameters['with_deleted'] = {
in: 'query',
required: true,
type: 'boolean'
}
#swagger.parameters['order_field'] = {
in: 'query',
required: true,
type: 'string'
}
#swagger.parameters['order_direction'] = {
in: 'query',
required: true,
schema: {
'@enum': ['ASC', 'DESC']
}
}
#swagger.parameters['token'] = {
in: 'query',
required: true,
type: 'string'
}
*/
try {
const schema = Joi.object().keys({
filter: Joi.string().allow('').optional().label('Filter'),
page: Joi.number().required().min(1).label('Page'),
limit: Joi.number().required().min(1).label('Limit'),
with_deleted: Joi.bool().required().label('With Deleted'),
order_field: Joi.string().required().label('Order Field'),
order_direction: Joi.string().allow('asc', 'desc').required().label('Order Direction'),
token: Joi.string().required().label('Token'),
});
const param: Paging = await schema.validateAsync(req.query);
const userRepository = OrmHelper.DB.getRepository(User);
const offset = (param.page - 1) * param.limit
let whereAttr = '';
let whereVal: any = {};
if (param.filter) {
whereAttr = 'name like :filter_like or username like :filter_like or email = :filter ';
whereVal['filter'] = param.filter
whereVal['filter_like'] = '%' + param.filter + '%'
}
const res_count = userRepository.createQueryBuilder()
.where(whereAttr, whereVal);
const res_list = userRepository.createQueryBuilder()
.where(whereAttr, whereVal)
.orderBy(param.order_field, param.order_direction)
.offset(offset)
.limit(param.limit);
if (param.with_deleted) {
res_count.withDeleted();
res_list.withDeleted();
}
const current_page = param.page;
const total_count_data = await res_count.getCount();
const list_data = await res_list.getMany();
const count_data = CommonHelper.countObject(list_data);
return ReturnHelper.successResponselist(res, 200, Language.lang.success_view, count_data, current_page, total_count_data, list_data);
} catch (e: unknown) {
log.error(e);
const err = e as Error;
return ReturnHelper.errorResponse(res, 400, 401, Language.lang.failed_view, err.message);
}
}
static async export(req: Request, res: Response, next: NextFunction): Promise<void | Response> {
/*
#swagger.tags = ['User']
#swagger.parameters['filter'] = {
in: 'query',
type: 'string'
}
#swagger.parameters['filter'] = {
in: 'query',
type: 'string'
}
#swagger.parameters['order_field'] = {
in: 'query',
required: true,
type: 'string'
}
#swagger.parameters['order_direction'] = {
in: 'query',
required: true,
schema: {
'@enum': ['ASC', 'DESC']
}
}
#swagger.parameters['token'] = {
in: 'query',
required: true,
type: 'string'
}
*/
try {
const schema = Joi.object().keys({
filter: Joi.string().allow('').optional().label('Filter'),
order_field: Joi.string().required().label('Order Field'),
order_direction: Joi.string().allow('asc', 'desc').required().label('Order Direction'),
token: Joi.string().required().label('Token'),
});
const param: Paging = await schema.validateAsync(req.query);
const userRepository = OrmHelper.DB.getRepository(User);
let whereAttr = '';
let whereVal: any = {};
if (param.filter) {
whereAttr = 'name like :filter_like or username like :filter_like or email = :filter ';
whereVal['filter'] = param.filter
whereVal['filter_like'] = '%' + param.filter + '%'
}
const filename = "user.csv";
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=' + filename);
const csvStream = fastcsv.format({
headers: true,
writeHeaders: true,
transform: (row: User): any => ({
...row,
created_at: dayjs(row.created_at).format('DD-MM-YYYY HH:MM:ss'),
})
});
csvStream.pipe(res);
const limit = 50;
const fetchAndWrite = async (page: any) => {
const offset = (page - 1) * limit
const data = await userRepository.createQueryBuilder()
.where(whereAttr, whereVal)
.select([
'id',
'name',
'email',
'username',
'status',
'created_at'])
.addSelect('(select role.name from user_roles role where role.id=User.id_role )', 'role_name')
.orderBy(param.order_field, param.order_direction)
.offset(offset)
.limit(limit).getRawMany();
if (CommonHelper.countObject(data) === 0) {
csvStream.end();
} else {
data.forEach((item: any) => csvStream.write(item));
if (CommonHelper.countObject(data) == limit) {
fetchAndWrite(page + 1);
} else {
csvStream.end();
}
}
};
fetchAndWrite(1);
} catch (e: unknown) {
log.error(e);
const err = e as Error;
return ReturnHelper.errorResponse(res, 400, 401, Language.lang.failed_view, err.message);
}
}
static async create(req: Request, res: Response, next: NextFunction): Promise<Response> {
/*
#swagger.tags = ['User']
#swagger.security = [{
"bearerAuth": []
}]
#swagger.requestBody = {
required: true,
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/user"
}
}
}
}
*/
try {
const schema = Joi.object().keys({
email: Joi.string().email().max(64).required().label('Email'),
username: Joi.string().max(64).required().label('Username'),
password: Joi.string().alphanum().min(6).required().label('Password'),
retype_password: Joi.ref('password'),
name: Joi.string().max(64).required().label('Name'),
id_role: Joi.string().uuid().required().label('Role'),
status: Joi.string().required().label('Status'),
});
const param: User = await schema.validateAsync(req.body);
const data = new User()
data.name = param.name
data.username = param.username
data.password = param.password
data.email = param.email
data.id_role = param.id_role
data.status = param.status
data.hashPassword();
await OrmHelper.DB.manager.save(data);
return ReturnHelper.successResponseAny(res, 200, Language.lang.success_insert, data);
} catch (e: unknown) {
log.error(e);
const err = e as Error;
return ReturnHelper.errorResponse(res, 500, 401, Language.lang.failed_insert, err.message);
}
}
static async updateProfile(req: Request, res: Response, next: NextFunction): Promise<Response> {
/*
#swagger.tags = ['User']
#swagger.security = [{
"bearerAuth": []
}]
#swagger.requestBody = {
required: true,
description: "This action will effect to user related token JWT logged",
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/user_profile"
}
}
}
}
*/
try {
const schema = Joi.object().keys({
id: Joi.string().uuid().required().label('ID'),
email: Joi.string().email().max(64).required().label('Email'),
username: Joi.string().max(64).required().label('Username'),
name: Joi.string().max(64).required().label('Name'),
});
req.body.id = req.auth.data.id;
const param: User = await schema.validateAsync(req.body);
const userRepository = OrmHelper.DB.getRepository(User);
const data = await userRepository.findOneBy({ id: param.id });
if (data != null) {
data.name = param.name
data.username = param.username
data.email = param.email
await userRepository.save(data);
return ReturnHelper.successResponseAny(res, 200, Language.lang.success_update, data);
} else {
return ReturnHelper.errorResponse(res, 404, 401, Language.lang.failed_not_found, "");
}
} catch (e: unknown) {
log.error(e);
const err = e as Error;
return ReturnHelper.errorResponse(res, 500, 402, Language.lang.failed_update, err.message);
}
}
static async updatePassword(req: Request, res: Response, next: NextFunction): Promise<Response> {
/*
#swagger.tags = ['User']
#swagger.security = [{
"bearerAuth": []
}]
#swagger.requestBody = {
required: true,
description: "This action will effect to user related token JWT logged",
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/user_password"
}
}
}
}
*/
try {
const schema = Joi.object().keys({
id: Joi.string().uuid().required().label('ID'),
password: Joi.string().required().label('Password'),
retype_password: Joi.ref('password'),
});
req.body.id = req.auth.data.id;
const param: User = await schema.validateAsync(req.body);
const userRepository = OrmHelper.DB.getRepository(User);
const data = await userRepository.findOneBy({ id: param.id });
if (data != null) {
if (param.password) {
data.password = param.password
data.hashPassword();
}
await userRepository.save(data);
return ReturnHelper.successResponseAny(res, 200, Language.lang.success_update, data);
} else {
return ReturnHelper.errorResponse(res, 404, 401, Language.lang.failed_not_found, "");
}
} catch (e: unknown) {
log.error(e);
const err = e as Error;
return ReturnHelper.errorResponse(res, 500, 402, Language.lang.failed_update, err.message);
}
}
static async update(req: Request, res: Response, next: NextFunction): Promise<Response> {
/*
#swagger.tags = ['User']
#swagger.security = [{
"bearerAuth": []
}]
#swagger.parameters['id'] = {
in: 'path',
description: 'User ID.',
required: true,
type: 'string'
}
#swagger.requestBody = {
required: true,
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/user"
}
}
}
}
*/
try {
const schema = Joi.object().keys({
id: Joi.string().uuid().required().label('ID'),
email: Joi.string().email().max(64).required().label('Email'),
username: Joi.string().max(64).required().label('Username'),
password: Joi.string().allow('').optional().label('Password'),
retype_password: Joi.ref('password'),
name: Joi.string().max(64).required().label('Name'),
id_role: Joi.string().uuid().required().label('Role'),
status: Joi.string().required().label('Status'),
});
req.body.id = req.params['id'];
const param: User = await schema.validateAsync(req.body);
const userRepository = OrmHelper.DB.getRepository(User);
const data = await userRepository.findOneBy({ id: param.id });
if (data != null) {
data.name = param.name
data.username = param.username
data.email = param.email
data.id_role = param.id_role
data.status = param.status
if (param.password) {
data.password = param.password
data.hashPassword();
}
await userRepository.save(data);
return ReturnHelper.successResponseAny(res, 200, Language.lang.success_update, data);
} else {
return ReturnHelper.errorResponse(res, 404, 401, Language.lang.failed_not_found, "");
}
} catch (e: unknown) {
log.error(e);
const err = e as Error;
return ReturnHelper.errorResponse(res, 500, 402, Language.lang.failed_update, err.message);
}
}
static async delete(req: Request, res: Response, next: NextFunction): Promise<Response> {
/*
#swagger.tags = ['User']
#swagger.security = [{
"bearerAuth": []
}]
#swagger.parameters['id'] = {
in: 'path',
description: 'User ID.',
required: true,
type: 'string'
}
*/
try {
const schema = Joi.object().keys({
id: Joi.string().uuid().required().label('ID')
});
const param: User = await schema.validateAsync(req.params);
const userRepository = OrmHelper.DB.getRepository(User);
const affected = (await userRepository.softDelete({ id: param.id })).affected;
if (affected > 0) {
return ReturnHelper.successResponseAny(res, 200, Language.lang.success_delete, {});
} else {
return ReturnHelper.errorResponse(res, 404, 401, Language.lang.failed_not_found, "");
}
} catch (e: unknown) {
log.error(e);
const err = e as Error;
return ReturnHelper.errorResponse(res, 500, 402, Language.lang.failed_delete, err.message);
}
}
static async restore(req: Request, res: Response, next: NextFunction): Promise<Response> {
/*
#swagger.tags = ['User']
#swagger.security = [{
"bearerAuth": []
}]
#swagger.parameters['id'] = {
in: 'path',
description: 'User ID.',
required: true,
type: 'string'
}
*/
try {
const schema = Joi.object().keys({
id: Joi.string().uuid().required().label('ID')
});
const param: User = await schema.validateAsync(req.params);
const userRepository = OrmHelper.DB.getRepository(User);
const affected = (await userRepository.restore({ id: param.id })).affected;
if (affected > 0) {
return ReturnHelper.successResponseAny(res, 200, Language.lang.success_restore, {});
} else {
return ReturnHelper.errorResponse(res, 404, 401, Language.lang.failed_not_found, "");
}
} catch (e: unknown) {
log.error(e);
const err = e as Error;
return ReturnHelper.errorResponse(res, 500, 402, Language.lang.failed_restore, err.message);
}
}
}

66
src/helpers/common.ts Normal file
View File

@ -0,0 +1,66 @@
import { Logger, ILogObj } from 'tslog';
const log: Logger<ILogObj> = new Logger({ name: '[CommonHelper]', type: 'pretty' });
export default class CommonHelper {
static randomInteger(min: number, max: number): number | null {
if (min > 0 && max > 0) {
return Math.floor(Math.random() * (max - min + 1)) + min;
} else {
log.error(new Error('Min & Max must be > 0'));
return null
}
}
static inArray(array: string[], keyword: string): boolean {
for (let val of array) {
if (val === keyword) {
return true;
}
}
return false;
}
static async sleep(seconds: number): Promise<void> {
if (seconds > 0) {
return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
} else {
log.error(new Error('Second must be > 0'));
}
}
static convertArrayToObject(object: any, key: string, val = ''): any {
const ret: any = {};
for (let i in object) {
const d = object[i];
if (val) {
ret[d[key]] = d[val];
} else {
ret[d[key]] = d;
}
}
return ret;
}
static countObject(obj: any): number {
let c = 0;
for (let s in obj) {
c++;
}
return c;
}
static capitalizeFirstLetter(str: string): string {
if (str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
return '';
}
}

View File

@ -0,0 +1,14 @@
import cors from 'cors';
import express from 'express';
import compression from 'compression';
export class CompressionHelper {
static setup(app: express.Application) {
const compressionOptions: compression.CompressionOptions = {
level: 6,
memLevel: 8
};
app.use(compression(compressionOptions));
}
}

View File

@ -0,0 +1,14 @@
import config from 'config';
import cors from 'cors';
import express from 'express';
export class CorsHelper {
static setup(app: express.Application) {
const corsOptions: cors.CorsOptions = {
origin: config.get('server.cors'),
optionsSuccessStatus: 200,
};
app.use(cors(corsOptions));
}
}

View File

@ -0,0 +1,12 @@
import config from 'config';
import express from 'express';
export class JsonHelper {
static setup(app: express.Application) {
app.use(
express.json({
limit: config.get('server.max_body'),
})
);
}
}

View File

@ -0,0 +1,9 @@
import config from 'config';
import morgan from 'morgan';
import express from 'express';
export class MorganHelper {
static setup(app: express.Application) {
app.use(morgan(config.get('server.morgan')));
}
}

View File

@ -0,0 +1,58 @@
import { Response } from 'express';
import { ErrorType, ErrorValidation } from '../../types/error';
export class ReturnHelper {
static successResponseAny(
res: Response,
status_code: number,
message: string,
data: any = null
): Response {
return res.status(status_code || 200).json({
status: true,
code: status_code || 200,
message: message || "success",
data: data || {},
});
};
static successResponselist(
res: Response,
status_code: number,
message: string,
count_data: number = 0,
current_page: number = 0,
total_count_data: number = 0,
list_data: any = null
): Response {
return res.status(status_code || 200).json({
status: true,
code: status_code || 200,
message: message || "success",
data: {
count: count_data,
page: current_page,
total_count: total_count_data,
list: list_data
},
});
};
static errorResponse(
res: Response,
status_code: number,
error_code: number,
message: string,
error: any = null
): Response {
return res.status(status_code || 200).json({
status: false,
error_code: error_code || 400,
message: message || "success",
error: error,
});
};
}

View File

@ -0,0 +1,14 @@
import config from 'config';
import express from 'express';
import swaggerUi from 'swagger-ui-express';
import swaggerDocument from '../../swagger/swagger.json';
export class SwaggerHelper {
static setup(app: express.Application) {
var options = {
explorer: false
};
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, options));
}
}

View File

@ -0,0 +1,13 @@
import config from 'config';
import express from 'express';
export class UrlencodedHelper {
static setup(app: express.Application) {
app.use(
express.urlencoded({
extended: true,
limit: config.get('server.max_body'),
})
);
}
}

38
src/helpers/jwt.ts Normal file
View File

@ -0,0 +1,38 @@
import { NextFunction, Response } from 'express';
import { Logger, ILogObj } from 'tslog';
import { Request, expressjwt } from "express-jwt";
import fs from 'fs';
import { Language } from '../langs/lang';
import { ReturnHelper } from './express/return';
import express from 'express';
const log: Logger<ILogObj> = new Logger({ name: '[JwtHelper]', type: 'pretty' });
export default class JwtHelper {
static secure = (app: express.Application) => {
var publicKey = fs.readFileSync("src/helpers/key/public.key");
app.use(
expressjwt({
secret: publicKey, algorithms: ["RS256"]
}).unless({ path: ["/token"] }),
function (req: Request, res: Response, next: NextFunction) {
if (!req.auth?.data.id) {
return ReturnHelper.errorResponse(res, 403, 666, Language.lang.failed_access);
}
return next();
}
)
app.use(function (err: any, req: Request, res: Response, next: NextFunction) {
if (err.name === 'UnauthorizedError') {
return ReturnHelper.errorResponse(res, 403, 666, Language.lang.failed_access);
}
return next();
});
}
}

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0sdcneY7W32xE
8hbjFPR4/AZFdtQqwQ3lEsuUFcsYh2KR0wP0umqtIMGLle3603lhQ/AMXDlKBBJt
IAP1SJNvGYY6fUdm+AZ0NFmst3CCiFsw7N5F+KXKEW1g/vUO9OKJ2Hg9lXuOkYAh
DYv+d0R5nsHu5l7WAesHtrcuNwezmENeAwjgWNezlwJmf54soNrV+EE7zPxTyye1
se3b8LjMgB/zDTF2UkdrvrVulpshG0JMvDy1n7QG/+K3u3BcGakRws5GZu7LZug4
ZNdURajL0oqN9bxPQRWR5+7jVqm1gYNRGQfPJuBXUiqd/AXa+ap0M0RPBFWfomjf
xn21QpdVAgMBAAECggEAKUMRHCEMhrG3YhkcM9fFqdj3P6aOdYLnPt+nYFYHrj7A
OgeDOD/Xe1hnCg5/LQ9cgOMILnJi3K2IXaX5cWoUzMJ53eJcyz0pECEiNygeh5hG
pqn4aecJSNbo8MTXxgYSsyKc9ocrk1dTeHjE9qNaniEsLPcrQdhnSLgnJWUIm7Bv
HqFFhgINdbZmE3Wx0a1Y71jGePcm3Zqx0a5sZDmZN2Vh7fNs1X8y2rnNvsKOBG5T
RmB8zlnj5SkjtD9RKkis0gGDVGjtkfskClY6wARUgSajfJnBAwpbdqrR0bmOox5k
T39aL6dxcPBQc8L0HX1sYW4yCIba1snxOUXtLEjdkQKBgQDwepPKv09waBlp7Q5W
c7Fn5dLBJ8ungbeDf83jZbIUs2I0wJ0v/UJiUegVP/h005kpEyY9NjFhcN5GXnTo
Ru1jmZ2w0UC+iOa7+JplXsS7kZfieaPIWHcmg0IkQpWHXRjoeWrd2dA+unc+6BGf
ZvUlb9XPZBATShARnjHu1z5CiQKBgQDAW3PBjLwgrT7NlRkpjuLMEvzimso1yTKa
yYdTNhJYkz5YnlPeu197seqqeGQPuRuR9KlMjfe7IQOvo0AGNB7VMxCxfF0Obyb9
hBT5fFci5dweKqgN3bR/+ppj/bpLS0veMwYNKzhaHsMrwRQzaxrRypRkri2rT7dx
jZAI0ilrbQKBgGmk//5m80gng1qkmNLj+oDxVxgiGnbZJryvTczjZUtwzujr4WIu
uZYl83Y6ZzUzrCp+TiNABouPISb64hMU7b7+wmbmVrIdxHe5rGJyMq1QNdB5rbkb
HCUgLtNtKPGRtZqTlJ4nzTNxiWdqXiuP+IxcyCpXBDTlKZAD1l3d0205AoGBAJTk
NO2UKeqBLyOiTR/F4fdkmyor9mk7m1gEtiLKr9iv4IpnwzOchYQRazsYhRtGhPit
EH6ZRTArldbV3jDvFw6fwEQhp0YM83k4S6PxguEYWxFeo0ZYXebR67+KHjE5zzfm
9sAqvCvFs/yiLyi9try8ubBUwjTgN3ZFxT+OrVDhAoGAUhFkd/Fy0FCsMROPKK5l
uadRukj5zKLMw+JJegXO+0AntV2vWPam73x2ulvVig9TqrJLqtOfqYNNX789Fzri
PBcqDaMJKfMc6ac7YgZTNP6vk7avjqWUpoJqlj1g8y3suAYkm/xfLd1HvAqQh0p8
i3/cmSzkc1MCE2YxYop0gpc=
-----END PRIVATE KEY-----

View File

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtLHXJ3mO1t9sRPIW4xT0
ePwGRXbUKsEN5RLLlBXLGIdikdMD9LpqrSDBi5Xt+tN5YUPwDFw5SgQSbSAD9UiT
bxmGOn1HZvgGdDRZrLdwgohbMOzeRfilyhFtYP71DvTiidh4PZV7jpGAIQ2L/ndE
eZ7B7uZe1gHrB7a3LjcHs5hDXgMI4FjXs5cCZn+eLKDa1fhBO8z8U8sntbHt2/C4
zIAf8w0xdlJHa761bpabIRtCTLw8tZ+0Bv/it7twXBmpEcLORmbuy2boOGTXVEWo
y9KKjfW8T0EVkefu41aptYGDURkHzybgV1IqnfwF2vmqdDNETwRVn6Jo38Z9tUKX
VQIDAQAB
-----END PUBLIC KEY-----

37
src/helpers/orm.ts Normal file
View File

@ -0,0 +1,37 @@
import config from 'config';
import { DataSource } from "typeorm";
import { User } from "../entity/users";
import { TrialBalance } from '../entity/trial_balance';
import { ILogObj, Logger } from 'tslog';
import { LNKOLEK } from '../entity/lnkolek';
import { CifAccount } from '../entity/cif_account';
export class OrmHelper {
static DB: DataSource = null
static setup() {
const log: Logger<ILogObj> = new Logger({ name: '[OrmHelper]', type: 'pretty' });
const engine: 'mysql' | 'postgres' = config.get("database.engine")
OrmHelper.DB = new DataSource({
type: engine,
host: config.get("database.host"),
port: Number(config.get("database.port")),
username: String(config.get("database.username")),
password: String(config.get("database.password")),
database: String(config.get("database.database")),
synchronize: true,
logging: true,
entities: [User, TrialBalance, LNKOLEK, CifAccount],
subscribers: [],
migrations: [],
})
OrmHelper.DB.initialize()
.then(() => {
// here you can start to work with your database
})
.catch((error: any) => log.error(error))
}
}

36
src/index.ts Normal file
View File

@ -0,0 +1,36 @@
import { Logger, ILogObj } from "tslog";
import { app } from "./server";
import config from 'config'
import https from 'https';
import fs from 'fs';
const log: Logger<ILogObj> = new Logger({ name: '[Index]', type: 'pretty' });
const HOST = config.get('server.host')
const PORT = Number(config.get('server.port'))
const server = app.listen(config.get('server.ssl.enable') ? PORT + 1000 : PORT, String(HOST), () => {
log.info(`Server ${config.get('app.name')} running on port http://${HOST}:${PORT}`);
});
if (config.get('server.ssl.enable')) {
const options = {
cert: fs.readFileSync(config.get('server.ssl.cert')),
key: fs.readFileSync(config.get('server.ssl.key'))
};
https.createServer(options, app).listen(PORT);
}
const onCloseSignal = () => {
log.info("sigint received, shutting down");
server.close(() => {
log.info("server closed");
process.exit();
});
setTimeout(() => process.exit(1), 10000).unref(); // Force shutdown after 10s
};
process.on("SIGINT", onCloseSignal);
process.on("SIGTERM", onCloseSignal);

32
src/langs/json/en.json Normal file
View File

@ -0,0 +1,32 @@
{
"success_insert": "Data has been successfully inserted",
"success_update": "Data has been successfully updated",
"success_delete": "Data has been successfully deleted",
"success_restore": "Data has been successfully restored",
"success_view": "Data has been successfully fetched",
"success": "Operation has been success",
"success_valid_otp": "Operation has been success, valid for 1 minutes",
"failed": "Operation has been failed",
"failed_access": "You're not allowed to acces",
"failed_access_otp": "Please provide valid OTP code",
"failed_expired_otp": "Your OTP has been expired, Please provide valid OTP code",
"failed_access_pin": "Please provide valid PIN code",
"failed_try_pin": "Maximum try has been reached, please re-login MyTelkomcel",
"failed_insert": "Data has been NOT successfully inserted",
"failed_update": "Data has been NOT successfully updated",
"failed_delete": "Data has been NOT successfully deleted",
"failed_restore": "Data has been NOT successfully restored",
"failed_save": "Data has been NOT successfully saved",
"failed_view": "Data has been NOT successfully fetched",
"failed_empty": "Data empty",
"failed_data": "Please provide valid data",
"failed_duplicate": "Duplicate data, please provide valid data",
"failed_not_found": "Data not found, please provide valid data",
"success_login": "Login Successfully",
"failed_password": "Please provide valid password",
"failed_limit_otp": "Please send OTP 1 minutes later",
"failed_limit_link_code": "Please send verification link 1 minutes later",
"failed_expired_token": "Token has been expired",
"failed_token": "Your session has been expired, please re-login",
"failed_add_point": "Add point failed, please try again later"
}

65
src/langs/lang.ts Normal file
View File

@ -0,0 +1,65 @@
import path from 'path';
import fs from 'fs';
import { NextFunction, Request, Response } from 'express';
interface LanguageValue {
success_insert?: string;
success_update?: string;
success_delete?: string;
success_restore?: string;
success_view?: string;
success?: string;
success_valid_otp?: string;
failed?: string;
failed_access?: string;
failed_access_otp?: string;
failed_expired_otp?: string;
failed_access_pin?: string;
failed_try_pin?: string;
failed_insert?: string;
failed_update?: string;
failed_delete?: string;
failed_restore?: string;
failed_save?: string;
failed_view?: string;
failed_empty?: string;
failed_data?: string;
failed_duplicate?: string;
failed_not_found?: string;
failed_limit_otp?: string;
failed_limit_link_code?: string;
failed_expired_token?: string;
failed_token?: string;
failed_add_point?: string;
//login
success_login?: string;
failed_password?: string;
}
export class Language {
static lang_init: LanguageValue = {};
static lang: LanguageValue = {};
static setup() {
const fileLang: any = fs.readFileSync(
path.join(__dirname, '../langs/json/en.json')
);
Language.lang_init['en'] = JSON.parse(fileLang.toString('utf8'));
}
static apply = (req: Request, res: Response, next: NextFunction) => {
const acceptLanguageHeader = req.get('Accept-Language') as string | null;
if (!acceptLanguageHeader) {
//default
req.params.language = 'en';
} else {
req.params.language = acceptLanguageHeader.substring(0, 2).toLowerCase();
}
Language.lang = Language.lang_init[req.params.language];
return next();
}
}

12
src/routes/private.ts Normal file
View File

@ -0,0 +1,12 @@
import express from 'express';
import { Language } from '../langs/lang';
import JwtHelper from '../helpers/jwt';
export class RoutePrivate {
static setup(app: express.Application) {
// app.use(Language.apply)
JwtHelper.secure(app);
}
}

17
src/routes/public.ts Normal file
View File

@ -0,0 +1,17 @@
import express from 'express';
import { UserController } from '../controllers/user';
import { Language } from '../langs/lang';
export class RoutePublic {
static setup(app: express.Application) {
app.use(Language.apply)
app.get('/api/user/list', UserController.list)
app.get('/api/user/export', UserController.export)
app.post('/api/user/create', UserController.create)
app.put('/api/user/update/:id', UserController.update)
app.delete('/api/user/delete/:id', UserController.delete)
app.put('/api/user/restore/:id', UserController.restore)
}
}

26
src/server.ts Normal file
View File

@ -0,0 +1,26 @@
import express from 'express';
import { CorsHelper } from './helpers/express/cors';
import { CompressionHelper } from './helpers/express/compression';
import { MorganHelper } from './helpers/express/morgan';
import { JsonHelper } from './helpers/express/json';
import { UrlencodedHelper } from './helpers/express/urlencoded';
import { OrmHelper } from './helpers/orm';
import { RoutePrivate } from './routes/private';
import { RoutePublic } from './routes/public';
import { Language } from './langs/lang';
import { SwaggerHelper } from './helpers/express/swagger';
const app = express();
CorsHelper.setup(app);
CompressionHelper.setup(app);
MorganHelper.setup(app);
JsonHelper.setup(app);
UrlencodedHelper.setup(app);
OrmHelper.setup();
Language.setup();
SwaggerHelper.setup(app);
RoutePublic.setup(app);
RoutePrivate.setup(app);
export { app };

17
src/swagger/builder.js Normal file
View File

@ -0,0 +1,17 @@
const swaggerAutogen = require('swagger-autogen')();
const config = require('config');
const doc = {
info: {
title: config.get('app.name'),
description: config.get('app.description'),
version: config.get('app.version')
},
host: config.get('server.host_swagger') + ':' + config.get('server.port'),
};
const outputFile = './swagger.json';
const routes = ['../routes/private.ts', '../routes/public.ts'];
swaggerAutogen(outputFile, routes, doc);

3
src/types/error.ts Normal file
View File

@ -0,0 +1,3 @@
export type ErrorType = 'General' | 'Raw' | 'Validation' | 'Unauthorized';
export type ErrorValidation = { [key: string]: string };

8
src/types/paging.ts Normal file
View File

@ -0,0 +1,8 @@
export interface Paging {
page: number,
limit: number,
filter: any,
with_deleted: boolean,
order_field: string,
order_direction: 'ASC' | 'DESC'
}

1
src/types/role.ts Normal file
View File

@ -0,0 +1 @@
export type Role = 'ADMINISTRATOR' | 'HR' | 'STAFF';

4
src/types/status.ts Normal file
View File

@ -0,0 +1,4 @@
export enum Status {
"Active" = "Y",
"Not Active" = "N"
};

23
tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"lib": ["dom", "es6", "es2017", "esnext.asynciterable"],
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./",
"moduleResolution": "node",
"removeComments": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"typeRoots": ["./src/types", "./node_modules/@types"],
// "baseUrl": "src/",
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/swagger-output.json", "src/langs/json/*.json", "./src/**/*.tsx", "./src/**/*.ts"],
"exclude": ["node_modules", "test/**/*.ts"]
}