commit df875117b05ed3ad7a9c8e552ad81f42f881bde2 Author: Abdul Rahman Reza Date: Tue Nov 26 13:19:48 2024 +0700 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66368b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.vscode/ +.DS_Store + +node_modules/ +build/ +dist/ +temp/ +package-lock.json diff --git a/config/default.json b/config/default.json new file mode 100644 index 0000000..527f937 --- /dev/null +++ b/config/default.json @@ -0,0 +1,24 @@ +{ + "app": { + "name": "Service Name", + "description": "Description of service", + "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": "*" + }, + "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..646bf20 --- /dev/null +++ b/config/stag.json @@ -0,0 +1,24 @@ +{ + "app": { + "name": "Service Name", + "description": "Description of service", + "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": "*" + }, + "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..d17acb7 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "boilerplate-bri", + "version": "1.0.0", + "description": "This kind of project was built to present base application regarding BRI ERP Project", + "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", + "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/user.ts b/src/controllers/user/user.ts new file mode 100644 index 0000000..2091e50 --- /dev/null +++ b/src/controllers/user/user.ts @@ -0,0 +1,279 @@ +import { Request, Response, NextFunction, Application } from "express"; +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"; + +const log: Logger = new Logger({ name: '[UserController]', type: 'pretty' }); + +export class UserController { + static async list(req: Request, res: Response, next: NextFunction): Promise { + /* + #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, + enum: [ "ASC", "DESC"] + } + */ + + 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'), + }); + + 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) + .skip(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 create(req: Request, res: Response, next: NextFunction): Promise { + /* + #swagger.parameters['body'] = { + in: 'body', + description: 'User data', + required: true, + schema: { + email: "admin@gmail.com", + username: "admin", + password: "123456", + retype_password: "123456", + name: "Administrator", + role: "ADMINISTRATOR", + status:"Y" + } + }*/ + + 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'), + role: Joi.string().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.role = param.role + data.status = param.status + + 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 update(req: Request, res: Response, next: NextFunction): Promise { + /* + #swagger.parameters['id'] = { + in: 'path', + description: 'User ID.', + required: true, + type: 'uuid' + } + + #swagger.parameters['body'] = { + in: 'body', + description: 'User data', + required: true, + schema: { + email: "admin@gmail.com", + username: "admin", + password: "123456", + retype_password: "123456", + name: "Administrator", + role: "ADMINISTRATOR", + status:"Y" + } + }*/ + + 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'), + role: Joi.string().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.password = param.password + data.email = param.email + data.role = param.role + data.status = param.status + + 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.parameters['id'] = { + in: 'path', + description: 'User ID.', + required: true, + type: 'uuid' + } + */ + 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.parameters['id'] = { + in: 'path', + description: 'User ID.', + required: true, + type: 'uuid' + } + */ + 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/entity/users.ts b/src/entity/users.ts new file mode 100644 index 0000000..0ff6623 --- /dev/null +++ b/src/entity/users.ts @@ -0,0 +1,71 @@ +import bcrypt from 'bcryptjs'; +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm'; + +import { Role } from '../types/role'; +import { Status } from '../types/status'; + +@Entity('users') +export class User { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + nullable: false, + unique: true, + length: 64 + }) + email: string; + + @Column({ + nullable: false, + length: 64, + select: false + }) + password: string; + + @Column({ + nullable: false, + unique: true, + length: 64 + }) + username: string; + + @Column({ + nullable: true, + length: 64 + }) + name: string; + + @Column({ + nullable: false, + default: 'STAFF' as Role, + length: 30, + }) + role: Role; + + @Column({ + nullable: false, + length: 64 + }) + status: Status; + + @Column() + @CreateDateColumn() + created_at: Date; + + @Column({ nullable: true }) + @UpdateDateColumn() + updated_at: Date; + + @Column({ nullable: true }) + @DeleteDateColumn() + deleted_at: Date; + + hashPassword() { + this.password = bcrypt.hashSync(this.password, 8); + } + + checkIfPasswordMatch(unencryptedPassword: string) { + return bcrypt.compareSync(unencryptedPassword, this.password); + } +} \ No newline at end of file diff --git a/src/entity/users_activity.ts b/src/entity/users_activity.ts new file mode 100644 index 0000000..0ff6623 --- /dev/null +++ b/src/entity/users_activity.ts @@ -0,0 +1,71 @@ +import bcrypt from 'bcryptjs'; +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm'; + +import { Role } from '../types/role'; +import { Status } from '../types/status'; + +@Entity('users') +export class User { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + nullable: false, + unique: true, + length: 64 + }) + email: string; + + @Column({ + nullable: false, + length: 64, + select: false + }) + password: string; + + @Column({ + nullable: false, + unique: true, + length: 64 + }) + username: string; + + @Column({ + nullable: true, + length: 64 + }) + name: string; + + @Column({ + nullable: false, + default: 'STAFF' as Role, + length: 30, + }) + role: Role; + + @Column({ + nullable: false, + length: 64 + }) + status: Status; + + @Column() + @CreateDateColumn() + created_at: Date; + + @Column({ nullable: true }) + @UpdateDateColumn() + updated_at: Date; + + @Column({ nullable: true }) + @DeleteDateColumn() + deleted_at: Date; + + hashPassword() { + this.password = bcrypt.hashSync(this.password, 8); + } + + checkIfPasswordMatch(unencryptedPassword: string) { + return bcrypt.compareSync(unencryptedPassword, this.password); + } +} \ No newline at end of file diff --git a/src/entity/users_refresh_token.ts b/src/entity/users_refresh_token.ts new file mode 100644 index 0000000..0ff6623 --- /dev/null +++ b/src/entity/users_refresh_token.ts @@ -0,0 +1,71 @@ +import bcrypt from 'bcryptjs'; +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm'; + +import { Role } from '../types/role'; +import { Status } from '../types/status'; + +@Entity('users') +export class User { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + nullable: false, + unique: true, + length: 64 + }) + email: string; + + @Column({ + nullable: false, + length: 64, + select: false + }) + password: string; + + @Column({ + nullable: false, + unique: true, + length: 64 + }) + username: string; + + @Column({ + nullable: true, + length: 64 + }) + name: string; + + @Column({ + nullable: false, + default: 'STAFF' as Role, + length: 30, + }) + role: Role; + + @Column({ + nullable: false, + length: 64 + }) + status: Status; + + @Column() + @CreateDateColumn() + created_at: Date; + + @Column({ nullable: true }) + @UpdateDateColumn() + updated_at: Date; + + @Column({ nullable: true }) + @DeleteDateColumn() + deleted_at: Date; + + hashPassword() { + this.password = bcrypt.hashSync(this.password, 8); + } + + checkIfPasswordMatch(unencryptedPassword: string) { + return bcrypt.compareSync(unencryptedPassword, this.password); + } +} \ 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..6101dfe --- /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/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..aedc373 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,25 @@ +import { Logger, ILogObj } from "tslog"; +import { app } from "./server"; +import config from 'config' + + +const log: Logger = new Logger({ name: '[Index]', type: 'pretty' }); + +const HOST = config.get('server.host') +const PORT = config.get('server.port') + +const server = app.listen(Number(PORT), String(HOST), () => { + log.info(`Server ${config.get('app.name')} running on port http://${HOST}:${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..1218f0f --- /dev/null +++ b/src/routes/private.ts @@ -0,0 +1,11 @@ +import express from 'express'; +import { Language } from '../langs/lang'; + +export class RoutePrivate { + static setup(app: express.Application) { + + // app.use(Language.apply) + + + } +} \ No newline at end of file diff --git a/src/routes/public.ts b/src/routes/public.ts new file mode 100644 index 0000000..f162bd9 --- /dev/null +++ b/src/routes/public.ts @@ -0,0 +1,16 @@ +import express from 'express'; +import { UserController } from '../controllers/user/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.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..5a64a37 --- /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(); +RoutePrivate.setup(app); +RoutePublic.setup(app); +SwaggerHelper.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/swagger/swagger.json b/src/swagger/swagger.json new file mode 100644 index 0000000..1bf7f5f --- /dev/null +++ b/src/swagger/swagger.json @@ -0,0 +1,213 @@ +{ + "swagger": "2.0", + "info": { + "title": "Service Name", + "description": "Description of service", + "version": "1.0.0" + }, + "host": "127.0.0.1:4001", + "basePath": "/", + "schemes": [ + "http" + ], + "paths": { + "/api/user/list": { + "get": { + "description": "", + "parameters": [ + { + "name": "filter", + "in": "query", + "type": "string" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "number" + }, + { + "name": "page", + "in": "query", + "required": true, + "type": "number" + }, + { + "name": "with_deleted", + "in": "query", + "required": true, + "type": "boolean" + }, + { + "name": "order_field", + "in": "query", + "required": true, + "type": "string" + }, + { + "name": "order_direction", + "in": "query", + "required": true, + "enum": [ + "ASC", + "DESC" + ], + "type": "string" + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/api/user/create": { + "post": { + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "User data", + "required": true, + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "admin@gmail.com" + }, + "username": { + "type": "string", + "example": "admin" + }, + "password": { + "type": "string", + "example": "123456" + }, + "retype_password": { + "type": "string", + "example": "123456" + }, + "name": { + "type": "string", + "example": "Administrator" + }, + "role": { + "type": "string", + "example": "ADMINISTRATOR" + }, + "status": { + "type": "string", + "example": "Y" + } + } + } + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/api/user/update/{id}": { + "put": { + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "uuid", + "description": "User ID." + }, + { + "name": "body", + "in": "body", + "description": "User data", + "required": true, + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "admin@gmail.com" + }, + "username": { + "type": "string", + "example": "admin" + }, + "password": { + "type": "string", + "example": "123456" + }, + "retype_password": { + "type": "string", + "example": "123456" + }, + "name": { + "type": "string", + "example": "Administrator" + }, + "role": { + "type": "string", + "example": "ADMINISTRATOR" + }, + "status": { + "type": "string", + "example": "Y" + } + } + } + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/api/user/delete/{id}": { + "delete": { + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "uuid", + "description": "User ID." + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/api/user/restore/{id}": { + "put": { + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "uuid", + "description": "User ID." + } + ], + "responses": { + "default": { + "description": "" + } + } + } + } + } +} \ No newline at end of file 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..81c798b --- /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/**/*.tsx", "./src/**/*.ts"], + "exclude": ["node_modules", "test/**/*.ts"] +}