diff --git a/package.json b/package.json index f6af3f5..5a02340 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dayjs": "^1.11.13", "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", @@ -29,6 +30,7 @@ "nodemailer": "^6.9.16", "npm": "^10.9.1", "pg": "^8.13.1", + "pg-query-stream": "^4.7.1", "reflect-metadata": "^0.2.2", "swagger-autogen": "^2.23.7", "swagger-jsdoc": "^6.2.8", diff --git a/src/controllers/user.ts b/src/controllers/user.ts index 049d8ec..2c5d520 100644 --- a/src/controllers/user.ts +++ b/src/controllers/user.ts @@ -8,6 +8,8 @@ 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' }); @@ -15,9 +17,6 @@ export class UserController { static async list(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['User'] - #swagger.security = [{ - "bearerAuth": [] - }] #swagger.parameters['filter'] = { in: 'query', type: 'string' @@ -49,6 +48,11 @@ export class UserController { '@enum': ['ASC', 'DESC'] } } + #swagger.parameters['token'] = { + in: 'query', + required: true, + type: 'string' + } */ try { @@ -59,6 +63,7 @@ export class UserController { 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); @@ -103,6 +108,113 @@ export class UserController { } } + 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'] @@ -162,7 +274,7 @@ export class UserController { #swagger.security = [{ "bearerAuth": [] }] - + #swagger.requestBody = { required: true, description: "This action will effect to user related token JWT logged", @@ -217,7 +329,7 @@ export class UserController { #swagger.security = [{ "bearerAuth": [] }] - + #swagger.requestBody = { required: true, description: "This action will effect to user related token JWT logged", @@ -278,7 +390,7 @@ export class UserController { required: true, type: 'string' } - + #swagger.requestBody = { required: true, content: { diff --git a/src/controllers/user_activity.ts b/src/controllers/user_activity.ts index e73eabf..1da94a7 100644 --- a/src/controllers/user_activity.ts +++ b/src/controllers/user_activity.ts @@ -9,6 +9,8 @@ import { Paging } from "../types/paging"; import CommonHelper from "../helpers/common"; import { ActivityType } from "../types/action"; import { UserActivity } from "../entity/user_activity"; +import * as fastcsv from 'fast-csv'; +import dayjs from "dayjs"; const log: Logger = new Logger({ name: '[UserActivityController]', type: 'pretty' }); @@ -17,9 +19,6 @@ export class UserActivityController { /* #swagger.tags = ['User Activity'] - #swagger.security = [{ - "bearerAuth": [] - }] #swagger.parameters['filter'] = { in: 'query', type: 'string' @@ -51,6 +50,11 @@ export class UserActivityController { '@enum': ['ASC', 'DESC'] } } + #swagger.parameters['token'] = { + in: 'query', + required: true, + type: 'string' + } */ try { @@ -61,6 +65,7 @@ export class UserActivityController { 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); @@ -73,8 +78,8 @@ export class UserActivityController { let whereVal: any = {}; if (param.filter) { - whereAttr = '(module like :filter_like or username like :filter_like or email = :filter) and '; - whereVal['filter'] = param.filter + whereAttr = '(module like :filter_like or description like :filter_like) and '; + // whereVal['filter'] = param.filter whereVal['filter_like'] = '%' + param.filter + '%' } @@ -108,6 +113,115 @@ export class UserActivityController { } } + static async export(req: Request, res: Response, next: NextFunction): Promise { + /* + #swagger.tags = ['User Activity'] + #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 repo = OrmHelper.DB.getRepository(UserActivity); + + let whereAttr = ''; + let whereVal: any = {}; + + if (param.filter) { + whereAttr = '(module like :filter_like or description like :filter_like) and '; + // whereVal['filter'] = param.filter + whereVal['filter_like'] = '%' + param.filter + '%' + } + + whereAttr += 'id_user = :id_user' + whereVal['id_user'] = req.auth.data.id; + + const filename = "user_activity.csv"; + + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', 'attachment; filename=' + filename); + + const csvStream = fastcsv.format({ + headers: true, + writeHeaders: true, + transform: (row: UserActivity): any => ({ + ...row, + action: CommonHelper.getKeyEnum(ActivityType, row.action), + created_at: dayjs(row.created_at).format('DD-MM-YYYY HH:MM:ss'), + }) + }); + csvStream.pipe(res); + + const limit = 50; + + const fetchAndWrite = async (page: number) => { + const offset = (page - 1) * limit + + const data = await repo.createQueryBuilder() + .where(whereAttr, whereVal) + .select([ + 'module', + 'description', + 'action', + 'url', + 'created_at']) + .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 Activity'] diff --git a/src/controllers/user_refresh_token.ts b/src/controllers/user_refresh_token.ts index f5b5b25..27fbdfe 100644 --- a/src/controllers/user_refresh_token.ts +++ b/src/controllers/user_refresh_token.ts @@ -15,9 +15,6 @@ export class UserRefreshTokenController { static async list(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['User Refresh Token'] - #swagger.security = [{ - "bearerAuth": [] - }] #swagger.parameters['filter'] = { in: 'query', type: 'string' @@ -44,6 +41,11 @@ export class UserRefreshTokenController { '@enum': ['ASC', 'DESC'] } } + #swagger.parameters['token'] = { + in: 'query', + required: true, + type: 'string' + } */ try { @@ -53,6 +55,7 @@ export class UserRefreshTokenController { limit: Joi.number().required().min(1).label('Limit'), 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); diff --git a/src/controllers/user_roles.ts b/src/controllers/user_roles.ts index 2a8891c..787ca44 100644 --- a/src/controllers/user_roles.ts +++ b/src/controllers/user_roles.ts @@ -9,6 +9,8 @@ import CommonHelper from "../helpers/common"; import { Status } from "../types/status"; import { UserRole } from "../entity/users_roles"; import { RoleList } from "../types/role"; +import * as fastcsv from 'fast-csv'; +import dayjs from "dayjs"; const log: Logger = new Logger({ name: '[UserRoleController]', type: 'pretty' }); @@ -16,9 +18,6 @@ export class UserRoleController { static async list(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['User Roles'] - #swagger.security = [{ - "bearerAuth": [] - }] #swagger.parameters['filter'] = { in: 'query', type: 'string' @@ -50,6 +49,11 @@ export class UserRoleController { '@enum': ['ASC', 'DESC'] } } + #swagger.parameters['token'] = { + in: 'query', + required: true, + type: 'string' + } */ try { @@ -60,6 +64,7 @@ export class UserRoleController { 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); @@ -73,7 +78,6 @@ export class UserRoleController { if (param.filter) { whereAttr = 'name like :filter_like'; - // whereVal['filter'] = param.filter whereVal['filter_like'] = '%' + param.filter + '%' } @@ -104,12 +108,117 @@ export class UserRoleController { } } + static async export(req: Request, res: Response, next: NextFunction): Promise { + /* + #swagger.tags = ['User Roles'] + #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 repo = OrmHelper.DB.getRepository(UserRole); + + let whereAttr = ''; + let whereVal: any = {}; + + if (param.filter) { + whereAttr = 'name like :filter_like'; + whereVal['filter_like'] = '%' + param.filter + '%' + } + + const filename = "user_role.csv"; + + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', 'attachment; filename=' + filename); + + const csvStream = fastcsv.format({ + headers: true, + writeHeaders: true, + transform: (row: UserRole): 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: number) => { + const offset = (page - 1) * limit + + const data = await repo.createQueryBuilder() + .where(whereAttr, whereVal) + .select([ + 'name', + 'roles', + 'status', + 'created_at']) + .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 role(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['User Roles'] - #swagger.security = [{ - "bearerAuth": [] - }] + #swagger.parameters['token'] = { + in: 'query', + required: true, + type: 'string' + } */ try { diff --git a/src/helpers/common.ts b/src/helpers/common.ts index 7e1bcc1..9af05a2 100644 --- a/src/helpers/common.ts +++ b/src/helpers/common.ts @@ -73,4 +73,12 @@ export default class CommonHelper { return ''; } + + static getKeyEnum(obj: any, key: any): any { + try { + return Object.values(obj)[Object.keys(obj)[key]] + } catch (e: any) { + return ''; + } + } } \ No newline at end of file diff --git a/src/helpers/jwt.ts b/src/helpers/jwt.ts index 1db12f7..1a3ecba 100644 --- a/src/helpers/jwt.ts +++ b/src/helpers/jwt.ts @@ -15,7 +15,15 @@ export default class JwtHelper { app.use( expressjwt({ - secret: publicKey, algorithms: ["RS256"] + secret: publicKey, algorithms: ["RS256"], + getToken: function fromHeaderOrQuerystring(req): any { + if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') { + return req.headers.authorization.split(' ')[1]; + } else if (req.query && req.query.token) { + return req.query.token; + } + return null; + }, }).unless({ path: ["/token"] }), function (req: Request, res: Response, next: NextFunction) { if (!req.auth?.data.id) { diff --git a/src/routes/private.ts b/src/routes/private.ts index 8d9bd32..89fbfbe 100644 --- a/src/routes/private.ts +++ b/src/routes/private.ts @@ -14,6 +14,7 @@ export class RoutePrivate { JwtHelper.secure(app); 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.put('/api/user/update_profile', UserController.updateProfile) @@ -22,13 +23,15 @@ export class RoutePrivate { app.put('/api/user/restore/:id', UserController.restore) app.get('/api/user_role/list', UserRoleController.list) + app.get('/api/user_role/export', UserRoleController.export) app.get('/api/user_role/role', UserRoleController.role) app.post('/api/user_role/create', UserRoleController.create) app.put('/api/user_role/update/:id', UserRoleController.update) app.delete('/api/user_role/delete/:id', UserRoleController.delete) app.put('/api/user_role/restore/:id', UserRoleController.restore) - app.get('/api/user_activity', UserActivityController.list) + app.get('/api/user_activity/list', UserActivityController.list) + app.get('/api/user_activity/export', UserActivityController.export) app.post('/api/user_activity/create', UserActivityController.create) app.get('/api/refresh_token/list', UserRefreshTokenController.list)