From 3e32e5869e38c06f09b161dcaef046d91e7a11ab Mon Sep 17 00:00:00 2001 From: Abdul Rahman Reza Date: Sat, 30 Nov 2024 13:24:46 +0700 Subject: [PATCH] initial --- .gitignore | 10 + config/default.json | 29 ++ config/prod.json | 0 config/stag.json | 29 ++ package.json | 46 +++ src/controllers/user.ts | 526 +++++++++++++++++++++++++++++ src/helpers/common.ts | 66 ++++ src/helpers/express/compression.ts | 14 + src/helpers/express/cors.ts | 14 + src/helpers/express/json.ts | 12 + src/helpers/express/morgan.ts | 9 + src/helpers/express/return.ts | 58 ++++ src/helpers/express/swagger.ts | 14 + src/helpers/express/urlencoded.ts | 13 + src/helpers/jwt.ts | 38 +++ src/helpers/key/private.key | 28 ++ src/helpers/key/public.key | 9 + src/helpers/orm.ts | 37 ++ src/index.ts | 36 ++ src/langs/json/en.json | 32 ++ src/langs/lang.ts | 65 ++++ src/routes/private.ts | 12 + src/routes/public.ts | 17 + src/server.ts | 26 ++ src/swagger/builder.js | 17 + src/types/error.ts | 3 + src/types/paging.ts | 8 + src/types/role.ts | 1 + src/types/status.ts | 4 + tsconfig.json | 23 ++ 30 files changed, 1196 insertions(+) create mode 100644 .gitignore create mode 100644 config/default.json create mode 100644 config/prod.json create mode 100644 config/stag.json create mode 100644 package.json create mode 100644 src/controllers/user.ts create mode 100644 src/helpers/common.ts create mode 100644 src/helpers/express/compression.ts create mode 100644 src/helpers/express/cors.ts create mode 100644 src/helpers/express/json.ts create mode 100644 src/helpers/express/morgan.ts create mode 100644 src/helpers/express/return.ts create mode 100644 src/helpers/express/swagger.ts create mode 100644 src/helpers/express/urlencoded.ts create mode 100644 src/helpers/jwt.ts create mode 100644 src/helpers/key/private.key create mode 100644 src/helpers/key/public.key create mode 100644 src/helpers/orm.ts create mode 100644 src/index.ts create mode 100644 src/langs/json/en.json create mode 100644 src/langs/lang.ts create mode 100644 src/routes/private.ts create mode 100644 src/routes/public.ts create mode 100644 src/server.ts create mode 100644 src/swagger/builder.js create mode 100644 src/types/error.ts create mode 100644 src/types/paging.ts create mode 100644 src/types/role.ts create mode 100644 src/types/status.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7492949 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.vscode/ +.DS_Store + +node_modules/ +build/ +dist/ +temp/ +package-lock.json +src/swagger/swagger.json +swagger.json \ No newline at end of file diff --git a/config/default.json b/config/default.json new file mode 100644 index 0000000..be8cde6 --- /dev/null +++ b/config/default.json @@ -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" + } +} diff --git a/config/prod.json b/config/prod.json new file mode 100644 index 0000000..e69de29 diff --git a/config/stag.json b/config/stag.json new file mode 100644 index 0000000..9d95f92 --- /dev/null +++ b/config/stag.json @@ -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" + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..446b57f --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/controllers/user.ts b/src/controllers/user.ts new file mode 100644 index 0000000..2c5d520 --- /dev/null +++ b/src/controllers/user.ts @@ -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 = new Logger({ name: '[UserController]', type: 'pretty' }); + +export class UserController { + static async list(req: Request, res: Response, next: NextFunction): Promise { + /* + #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 { + /* + #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 { + /* + #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 { + /* + #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 { + /* + #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 { + /* + #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 { + /* + #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 { + /* + #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); + } + } +} \ No newline at end of file diff --git a/src/helpers/common.ts b/src/helpers/common.ts new file mode 100644 index 0000000..33d1f37 --- /dev/null +++ b/src/helpers/common.ts @@ -0,0 +1,66 @@ +import { Logger, ILogObj } from 'tslog'; + +const log: Logger = 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 { + 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 ''; + } +} \ No newline at end of file diff --git a/src/helpers/express/compression.ts b/src/helpers/express/compression.ts new file mode 100644 index 0000000..022f71e --- /dev/null +++ b/src/helpers/express/compression.ts @@ -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)); + } +} \ No newline at end of file diff --git a/src/helpers/express/cors.ts b/src/helpers/express/cors.ts new file mode 100644 index 0000000..4b7a501 --- /dev/null +++ b/src/helpers/express/cors.ts @@ -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)); + } +} \ No newline at end of file diff --git a/src/helpers/express/json.ts b/src/helpers/express/json.ts new file mode 100644 index 0000000..3b9b002 --- /dev/null +++ b/src/helpers/express/json.ts @@ -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'), + }) + ); + } +} \ No newline at end of file diff --git a/src/helpers/express/morgan.ts b/src/helpers/express/morgan.ts new file mode 100644 index 0000000..d0ea3ca --- /dev/null +++ b/src/helpers/express/morgan.ts @@ -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'))); + } +} \ No newline at end of file diff --git a/src/helpers/express/return.ts b/src/helpers/express/return.ts new file mode 100644 index 0000000..92e9473 --- /dev/null +++ b/src/helpers/express/return.ts @@ -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, + }); + }; +} \ No newline at end of file diff --git a/src/helpers/express/swagger.ts b/src/helpers/express/swagger.ts new file mode 100644 index 0000000..0890af8 --- /dev/null +++ b/src/helpers/express/swagger.ts @@ -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)); + } +} \ No newline at end of file diff --git a/src/helpers/express/urlencoded.ts b/src/helpers/express/urlencoded.ts new file mode 100644 index 0000000..13fe107 --- /dev/null +++ b/src/helpers/express/urlencoded.ts @@ -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'), + }) + ); + } +} \ No newline at end of file diff --git a/src/helpers/jwt.ts b/src/helpers/jwt.ts new file mode 100644 index 0000000..1db12f7 --- /dev/null +++ b/src/helpers/jwt.ts @@ -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 = 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(); + }); + + } +} \ No newline at end of file diff --git a/src/helpers/key/private.key b/src/helpers/key/private.key new file mode 100644 index 0000000..0df6851 --- /dev/null +++ b/src/helpers/key/private.key @@ -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----- \ No newline at end of file diff --git a/src/helpers/key/public.key b/src/helpers/key/public.key new file mode 100644 index 0000000..f49da69 --- /dev/null +++ b/src/helpers/key/public.key @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtLHXJ3mO1t9sRPIW4xT0 +ePwGRXbUKsEN5RLLlBXLGIdikdMD9LpqrSDBi5Xt+tN5YUPwDFw5SgQSbSAD9UiT +bxmGOn1HZvgGdDRZrLdwgohbMOzeRfilyhFtYP71DvTiidh4PZV7jpGAIQ2L/ndE +eZ7B7uZe1gHrB7a3LjcHs5hDXgMI4FjXs5cCZn+eLKDa1fhBO8z8U8sntbHt2/C4 +zIAf8w0xdlJHa761bpabIRtCTLw8tZ+0Bv/it7twXBmpEcLORmbuy2boOGTXVEWo +y9KKjfW8T0EVkefu41aptYGDURkHzybgV1IqnfwF2vmqdDNETwRVn6Jo38Z9tUKX +VQIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/src/helpers/orm.ts b/src/helpers/orm.ts new file mode 100644 index 0000000..0b6c5df --- /dev/null +++ b/src/helpers/orm.ts @@ -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 = 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)) + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..3c7cde0 --- /dev/null +++ b/src/index.ts @@ -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 = 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); \ No newline at end of file diff --git a/src/langs/json/en.json b/src/langs/json/en.json new file mode 100644 index 0000000..3f93585 --- /dev/null +++ b/src/langs/json/en.json @@ -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" +} diff --git a/src/langs/lang.ts b/src/langs/lang.ts new file mode 100644 index 0000000..ef25beb --- /dev/null +++ b/src/langs/lang.ts @@ -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(); + } +} \ No newline at end of file diff --git a/src/routes/private.ts b/src/routes/private.ts new file mode 100644 index 0000000..bf0f459 --- /dev/null +++ b/src/routes/private.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/routes/public.ts b/src/routes/public.ts new file mode 100644 index 0000000..7958e14 --- /dev/null +++ b/src/routes/public.ts @@ -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) + } +} \ No newline at end of file diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..6e62151 --- /dev/null +++ b/src/server.ts @@ -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 }; \ No newline at end of file diff --git a/src/swagger/builder.js b/src/swagger/builder.js new file mode 100644 index 0000000..75840b5 --- /dev/null +++ b/src/swagger/builder.js @@ -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); + diff --git a/src/types/error.ts b/src/types/error.ts new file mode 100644 index 0000000..1415969 --- /dev/null +++ b/src/types/error.ts @@ -0,0 +1,3 @@ +export type ErrorType = 'General' | 'Raw' | 'Validation' | 'Unauthorized'; + +export type ErrorValidation = { [key: string]: string }; \ No newline at end of file diff --git a/src/types/paging.ts b/src/types/paging.ts new file mode 100644 index 0000000..ee83de9 --- /dev/null +++ b/src/types/paging.ts @@ -0,0 +1,8 @@ +export interface Paging { + page: number, + limit: number, + filter: any, + with_deleted: boolean, + order_field: string, + order_direction: 'ASC' | 'DESC' +} \ No newline at end of file diff --git a/src/types/role.ts b/src/types/role.ts new file mode 100644 index 0000000..dc822f9 --- /dev/null +++ b/src/types/role.ts @@ -0,0 +1 @@ +export type Role = 'ADMINISTRATOR' | 'HR' | 'STAFF'; \ No newline at end of file diff --git a/src/types/status.ts b/src/types/status.ts new file mode 100644 index 0000000..7408c75 --- /dev/null +++ b/src/types/status.ts @@ -0,0 +1,4 @@ +export enum Status { + "Active" = "Y", + "Not Active" = "N" +}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..29a46ef --- /dev/null +++ b/tsconfig.json @@ -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"] +}