├── .gitignore ├── .DS_Store ├── .dockerignore ├── dump.rdb ├── .vscode ├── settings.json └── extensions.json ├── src ├── .DS_Store ├── docs │ ├── erd.png │ ├── .DS_Store │ ├── cover.png │ └── tech_docs.md ├── configs │ ├── log-formats.ts │ ├── rbac.ts │ └── messages.ts ├── types │ └── types.ts ├── features │ ├── auth │ │ ├── auth.validator.ts │ │ ├── auth.route.ts │ │ ├── auth.controller.ts │ │ ├── auth.service.ts │ │ └── README.md │ ├── rbac │ │ ├── role │ │ │ ├── role.validator.ts │ │ │ ├── role.route.ts │ │ │ ├── role.service.ts │ │ │ └── role.controller.ts │ │ ├── action │ │ │ ├── action.validator.ts │ │ │ ├── action.route.ts │ │ │ ├── action.service.ts │ │ │ └── action.controller.ts │ │ ├── channel │ │ │ ├── channel.validator.ts │ │ │ ├── channel.route.ts │ │ │ ├── channel.service.ts │ │ │ └── channel.controller.ts │ │ ├── permission │ │ │ ├── permission.validator.ts │ │ │ ├── permission.route.ts │ │ │ ├── permission.controller.ts │ │ │ └── permission.service.ts │ │ ├── module │ │ │ ├── module.validator.ts │ │ │ └── module.route.ts │ │ └── sub-module │ │ │ ├── sub-module.validator.ts │ │ │ ├── sub-module.route.ts │ │ │ └── sub-module.service.ts │ ├── product-category │ │ ├── product-category.validator.ts │ │ ├── product-category.route.ts │ │ └── product-category.service.ts │ ├── cached-product-category │ │ ├── cached-product-category.validator.ts │ │ ├── cached-product-category.route.ts │ │ └── cached-product-category.service.ts │ ├── product │ │ ├── product.validator.ts │ │ ├── product.route.ts │ │ ├── product.service.ts │ │ └── product.controller.ts │ └── user │ │ ├── user.validator.ts │ │ ├── user.route.ts │ │ ├── user.service.ts │ │ └── user.controller.ts ├── utils │ ├── log.ts │ ├── node-mailer.ts │ ├── http.ts │ └── common.ts ├── middlewares │ ├── audit-log.ts │ ├── error-handler.ts │ ├── multer-upload.ts │ ├── validation.ts │ ├── jwt.ts │ ├── rbac.ts │ └── README.md ├── db │ └── db.ts ├── services │ ├── redis.ts │ └── sms-poh.ts ├── queues │ └── email-queue.ts ├── cron-jobs │ └── sample-cron.ts ├── api-client.ts ├── routes.ts ├── app.ts └── server.ts ├── nodemon.json ├── .husky └── pre-commit ├── .prettierrc.json ├── migrations └── 20251122035957_migration_name.ts ├── tsconfig.json ├── docker-entrypoint.sh ├── .env.example ├── eslint.config.cjs ├── seeds ├── 02_roles.ts ├── 01_actions_and_channels.ts ├── 03_modules_and_submodules.ts ├── 04_users.ts └── 05_permissions.ts ├── LICENSE ├── Dockerfile ├── knexfile.ts ├── docker-compose.yml ├── package.json ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinPyaeKyaw/express-template/HEAD/.DS_Store -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile 4 | .dockerignore 5 | -------------------------------------------------------------------------------- /dump.rdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinPyaeKyaw/express-template/HEAD/dump.rdb -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true, 3 | "prettier.enable": true 4 | } 5 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinPyaeKyaw/express-template/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /src/docs/erd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinPyaeKyaw/express-template/HEAD/src/docs/erd.png -------------------------------------------------------------------------------- /src/docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinPyaeKyaw/express-template/HEAD/src/docs/.DS_Store -------------------------------------------------------------------------------- /src/docs/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinPyaeKyaw/express-template/HEAD/src/docs/cover.png -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "exec": "ts-node src/server.ts" 5 | } 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | echo "Running ESLint..." 2 | npm run lint || { echo "❌ Lint failed. Aborting commit."; exit 1; } -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "tabWidth": 2 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", // ESLint 4 | "esbenp.prettier-vscode" // Prettier 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/configs/log-formats.ts: -------------------------------------------------------------------------------- 1 | export const auditLogFormat = 2 | ':method :url :status :response-time ms - :res[content-length] :body'; 3 | 4 | export const accessLogFormat = ':date[iso] :method :url'; 5 | -------------------------------------------------------------------------------- /migrations/20251122035957_migration_name.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise {} 4 | 5 | export async function down(knex: Knex): Promise {} 6 | -------------------------------------------------------------------------------- /src/types/types.ts: -------------------------------------------------------------------------------- 1 | export interface ExpressError extends Error { 2 | status?: number; 3 | stack?: string; 4 | } 5 | 6 | export interface ListQuery { 7 | page: number; 8 | size: number; 9 | sort?: string; 10 | order?: 'desc' | 'asc'; 11 | keyword?: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/features/auth/auth.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | const validator = { 4 | login: { 5 | body: Joi.object({ 6 | username: Joi.string().trim().required(), 7 | password: Joi.string().trim().min(6).required(), 8 | }), 9 | }, 10 | } as const; 11 | 12 | export default validator; 13 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { logDir } from '../middlewares/audit-log'; 3 | 4 | export function logAudit(message: string) { 5 | const timestamp = new Date().toISOString(); 6 | const fullMessage = `[${timestamp}] ${message}\n`; 7 | 8 | fs.appendFileSync(logDir, fullMessage, { encoding: 'utf-8' }); 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "target": "ES6", 5 | "module": "CommonJS", 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true 9 | }, 10 | "include": ["src/**/*"], 11 | "exclude": ["node_modules", "dist", "migrations", "seeds", "knexfile.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/node-mailer.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | 3 | const transporter = nodemailer.createTransport({ 4 | service: 'gmail', 5 | auth: { 6 | user: process.env.FROM_EMAIL, 7 | pass: process.env.PASSWORD, 8 | }, 9 | }); 10 | 11 | async function sendEmail(mailOptions: any) { 12 | return transporter.sendMail(mailOptions); 13 | } 14 | 15 | export default sendEmail; 16 | -------------------------------------------------------------------------------- /src/middlewares/audit-log.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | // Ensure log folder exists 5 | export const logDir = path.join(__dirname, '../storage/logs'); 6 | if (!fs.existsSync(logDir)) { 7 | fs.mkdirSync(logDir, { recursive: true }); 8 | } 9 | 10 | // Create a write stream in append mode 11 | const auditLogStream = fs.createWriteStream(path.join(logDir, 'audit.log'), { 12 | flags: 'a', 13 | }); 14 | 15 | export default auditLogStream; 16 | -------------------------------------------------------------------------------- /src/configs/rbac.ts: -------------------------------------------------------------------------------- 1 | export const ROLES = { 2 | ADMIN: 'Admin', 3 | USER: 'User', 4 | DEV: 'Developer', 5 | }; 6 | export const ACTIONS = { 7 | CREATE: 'Create', 8 | VIEW: 'View', 9 | UPDATE: 'Update', 10 | DELETE: 'Delete', 11 | }; 12 | export const CHANNELS = { 13 | WEB: 'Web', 14 | MOBILE: 'Mobile', 15 | }; 16 | export const MODULES = { 17 | USER_MANAGEMENT: 'User Management', 18 | PRODUCT: 'Product', 19 | }; 20 | export const SUB_MODULES = { 21 | USER: 'User', 22 | USER_ROLE_ASSIGN: 'User Role Assign', 23 | PRODUCT_CATEGORY: 'Product Category', 24 | PRODUCT: 'Product', 25 | }; 26 | -------------------------------------------------------------------------------- /src/utils/http.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | 3 | interface ResponseData { 4 | res: Response; 5 | status: number; 6 | message: string; 7 | data: any; 8 | } 9 | 10 | export const responseData = ({ 11 | res, 12 | status = 200, 13 | message, 14 | data, 15 | }: ResponseData) => { 16 | res.status(status).json({ 17 | status, 18 | message, 19 | data, 20 | }); 21 | }; 22 | 23 | export class AppError extends Error { 24 | status: string | number; 25 | constructor(message: string, status: number | string) { 26 | super(message); 27 | this.status = status; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | echo "⏳ Waiting for database to be ready..." 5 | # Wait a bit for MySQL to be fully ready (healthcheck ensures it's up, but migrations need it to be fully ready) 6 | sleep 5 7 | 8 | echo "🔄 Running database migrations..." 9 | npm run db:migrate || { 10 | echo "❌ Migration failed!" 11 | exit 1 12 | } 13 | 14 | echo "✅ Migrations completed successfully!" 15 | 16 | echo "🌱 Running database seeds..." 17 | npm run db:seed || { 18 | echo "❌ Seeding failed!" 19 | exit 1 20 | } 21 | 22 | echo "✅ Seeds completed successfully!" 23 | 24 | echo "🚀 Starting application..." 25 | exec "$@" 26 | 27 | -------------------------------------------------------------------------------- /src/configs/messages.ts: -------------------------------------------------------------------------------- 1 | export const MESSAGES = { 2 | SUCCESS: { 3 | CREATE: 'Successfully Created!', 4 | UPDATE: 'Successfully Updated!', 5 | DELETE: 'Successfully Deleted!', 6 | RETRIVE: 'Successfully Retrived!', 7 | LOGIN: 'Successfully Logged In!', 8 | }, 9 | ERROR: { 10 | CREATE: '', 11 | UPDATE: '', 12 | DELETE: '', 13 | RETRIVE: '', 14 | SERVER: '', 15 | USER_NOT_FOUND: 'User Not Found!', 16 | NO_PERMISSION: 'You have no permission to access this resource!', 17 | BAD_REQUEST: 'Invalid Request!', 18 | INVALID_CREDENTIAL: 'Invalid Credential!', 19 | UNAUTHORIZED: 'Unauthorized Access!', 20 | }, 21 | } as const; 22 | -------------------------------------------------------------------------------- /src/middlewares/error-handler.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { AppError } from '../utils/http'; 3 | import { MESSAGES } from '../configs/messages'; 4 | 5 | // Custom global error-handling middleware for Express 6 | export const errorHandler = ( 7 | err: AppError, 8 | req: Request, 9 | res: Response, 10 | next: NextFunction 11 | ) => { 12 | // Print the full error stack trace to the console for debugging 13 | console.error(err.stack); 14 | 15 | // Send a structured JSON error response 16 | res.status(+err.status || 500).json({ 17 | status: err.status, 18 | message: err.message || MESSAGES.ERROR.SERVER, 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/middlewares/multer-upload.ts: -------------------------------------------------------------------------------- 1 | import multer from 'multer'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | 5 | // Create uploads folder if it doesn't exist 6 | const uploadDir = path.join(__dirname, '../storage/uploads'); 7 | if (!fs.existsSync(uploadDir)) { 8 | fs.mkdirSync(uploadDir, { recursive: true }); 9 | } 10 | 11 | // Configure Multer Storage 12 | const storage = multer.diskStorage({ 13 | destination: (_req, _file, cb) => { 14 | cb(null, uploadDir); 15 | }, 16 | filename: (_req, file, cb) => { 17 | const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`; 18 | const ext = path.extname(file.originalname); 19 | cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`); 20 | }, 21 | }); 22 | 23 | export const upload = multer({ storage }); 24 | -------------------------------------------------------------------------------- /src/db/db.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import knex from 'knex'; 3 | 4 | // Load environment variables from .env file 5 | dotenv.config(); 6 | 7 | const db = knex({ 8 | client: 'mysql2', 9 | connection: { 10 | host: process.env.DB_HOST, 11 | port: parseInt(process.env.DB_PORT || '3306'), 12 | user: process.env.DB_USER, 13 | password: process.env.DB_PASSWORD, 14 | database: process.env.DB_NAME, 15 | }, 16 | }); 17 | 18 | async function testConnection() { 19 | try { 20 | await db.raw('SELECT 1'); 21 | console.log('✅ Database connection is working'); 22 | } catch (error: any) { 23 | console.log('ERROR', error); 24 | console.error('❌ Database connection failed:', error.message); 25 | } 26 | } 27 | 28 | testConnection(); 29 | 30 | export default db; 31 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | NODE_ENV="development" 3 | 4 | # Database 5 | DB_USER="your_db_user" 6 | DB_PASSWORD="your_db_password" 7 | DB_NAME="your_db_name" 8 | DB_HOST="your_db_host" 9 | 10 | # JWT 11 | JWT_SECRET='whatever_secret_you_want' 12 | JWT_EXPIRES_IN='1h' 13 | REFRESH_JWT_SECRET='whatever_secret_you_want' 14 | REFRESH_JWT_EXPIRES_IN='7d' 15 | 16 | # Mail 17 | FROM_EMAIL='your host mail address' 18 | PASSWORD='your host mail password' 19 | 20 | # AWS 21 | AWS_ACCESS_KEY_ID='your aws access key ID' 22 | AWS_SECRET_ACCESS_KEY='your aws secret key' 23 | AWS_BUCKET_NAME='your s3 bucket name' 24 | 25 | # SMS Poh 26 | SMS_SENDER_ID='your sms poh sender id' 27 | SMS_API_KEY='your sms poh api key' 28 | SMS_API_SECRET='your sms poh api secret' 29 | 30 | # Redis 31 | REDIS_HOST='localhost' 32 | REDIS_PORT=6379 -------------------------------------------------------------------------------- /src/features/auth/auth.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { loginController, refreshTokenController } from './auth.controller'; 3 | import { verifyRefreshToken } from '../../middlewares/jwt'; 4 | import { validateRequest } from '../../middlewares/validation'; 5 | import validator from './auth.validator'; 6 | 7 | const authRoutes = Router(); 8 | 9 | // ========================= 10 | // POST /login 11 | // - Handles user login 12 | // - Validates input using validator.login 13 | // ========================= 14 | authRoutes.post('/login', validateRequest(validator.login), loginController); 15 | 16 | // ========================= 17 | // POST /refresh-token 18 | // - Handles refreshing access token using a valid refresh token 19 | // - Validates the refresh token using verifyRefreshToken middleware 20 | // ========================= 21 | authRoutes.post('/refresh-token', verifyRefreshToken, refreshTokenController); 22 | 23 | export default authRoutes; 24 | -------------------------------------------------------------------------------- /src/services/redis.ts: -------------------------------------------------------------------------------- 1 | import Redis from 'ioredis'; 2 | import dotenv from 'dotenv'; 3 | 4 | // Load environment variables from .env file 5 | dotenv.config(); 6 | 7 | const redisHost = process.env.REDIS_HOST || '127.0.0.1'; 8 | const redisPort = process.env.REDIS_PORT 9 | ? parseInt(process.env.REDIS_PORT, 10) 10 | : 6379; 11 | 12 | const redisClient = new Redis({ 13 | host: redisHost, 14 | port: redisPort, 15 | maxRetriesPerRequest: null, 16 | }); 17 | 18 | redisClient.on('connect', () => { 19 | console.log(`✅ Redis connected to ${redisHost}:${redisPort}`); 20 | }); 21 | 22 | redisClient.on('error', (err) => { 23 | console.error('Redis connection error:', err); 24 | }); 25 | 26 | // Cache invalidation 27 | export const invalidateCache = async (cachePrefix: string) => { 28 | const keys = await redisClient.keys(`${cachePrefix}:*`); 29 | if (keys.length > 0) { 30 | await redisClient.del(...keys); 31 | } 32 | }; 33 | 34 | export default redisClient; 35 | -------------------------------------------------------------------------------- /eslint.config.cjs: -------------------------------------------------------------------------------- 1 | // eslint.config.js 2 | const js = require('@eslint/js'); 3 | const ts = require('@typescript-eslint/eslint-plugin'); 4 | const tsParser = require('@typescript-eslint/parser'); 5 | const prettier = require('eslint-plugin-prettier'); 6 | const configPrettier = require('eslint-config-prettier'); 7 | 8 | module.exports = [ 9 | js.configs.recommended, 10 | { 11 | files: ['**/*.ts'], 12 | languageOptions: { 13 | parser: tsParser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | sourceType: 'module', 17 | }, 18 | }, 19 | plugins: { 20 | '@typescript-eslint': ts, 21 | prettier, 22 | }, 23 | rules: { 24 | ...ts.configs.recommended.rules, 25 | 'prettier/prettier': 'error', 26 | 'no-console': 'off', 27 | 'no-undef': 'off', 28 | '@typescript-eslint/no-unused-vars': 'off', 29 | '@typescript-eslint/no-explicit-any': 'off', 30 | }, 31 | }, 32 | configPrettier, 33 | ]; 34 | -------------------------------------------------------------------------------- /seeds/02_roles.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from 'knex'; 2 | 3 | export async function seed(knex: Knex): Promise { 4 | // Delete existing data 5 | await knex('role').del(); 6 | 7 | // Insert roles 8 | await knex('role').insert([ 9 | { 10 | id: 'bfbdd16a-05e0-11f0-9bc1-32adce0096f0', 11 | name: 'Super Admin', 12 | description: 'Super Administrator with full system access', 13 | is_deleted: false, 14 | }, 15 | { 16 | id: '91e945da-0a45-11f0-9bc1-32adce0096f0', 17 | name: 'Admin', 18 | description: 'Administrator with management access', 19 | is_deleted: false, 20 | }, 21 | { 22 | id: 'c8c02538-05e0-11f0-9bc1-32adce0096f0', 23 | name: 'Developer', 24 | description: 'Developer role', 25 | is_deleted: false, 26 | }, 27 | { 28 | id: 'd9d13649-05e0-11f0-9bc1-32adce0096f0', 29 | name: 'User', 30 | description: 'Regular user role', 31 | is_deleted: false, 32 | }, 33 | ]); 34 | 35 | console.log('✅ Seeded roles'); 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 RBAC Express.js Starter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/features/rbac/role/role.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | const validator = { 4 | select: { 5 | query: Joi.object({ 6 | keyword: Joi.string().allow('').optional(), 7 | size: Joi.number().optional(), 8 | page: Joi.number().optional(), 9 | sort: Joi.string().optional(), 10 | order: Joi.string().valid('asc', 'desc').optional(), 11 | }), 12 | }, 13 | detail: { 14 | params: Joi.object({ 15 | id: Joi.string().required(), 16 | }), 17 | }, 18 | create: { 19 | body: Joi.object({ 20 | name: Joi.string().required(), 21 | }), 22 | }, 23 | createMany: { 24 | body: Joi.object({ 25 | roles: Joi.array().items( 26 | Joi.object({ 27 | name: Joi.string().required(), 28 | }) 29 | ), 30 | }), 31 | }, 32 | update: { 33 | params: Joi.object({ 34 | id: Joi.string().required(), 35 | }), 36 | body: Joi.object({ 37 | name: Joi.string().required(), 38 | }), 39 | }, 40 | delete: { 41 | params: Joi.object({ 42 | id: Joi.string().required(), 43 | }), 44 | }, 45 | deleteMany: { 46 | body: Joi.object({ 47 | ids: Joi.array().items(Joi.string().required()), 48 | }), 49 | }, 50 | } as const; 51 | 52 | export default validator; 53 | -------------------------------------------------------------------------------- /src/features/rbac/action/action.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | const validator = { 4 | select: { 5 | query: Joi.object({ 6 | keyword: Joi.string().allow('').optional(), 7 | size: Joi.number().optional(), 8 | page: Joi.number().optional(), 9 | sort: Joi.string().optional(), 10 | order: Joi.string().valid('asc', 'desc').optional(), 11 | }), 12 | }, 13 | detail: { 14 | params: Joi.object({ 15 | id: Joi.string().required(), 16 | }), 17 | }, 18 | create: { 19 | body: Joi.object({ 20 | name: Joi.string().required(), 21 | }), 22 | }, 23 | createMany: { 24 | body: Joi.object({ 25 | actions: Joi.array().items( 26 | Joi.object({ 27 | name: Joi.string().required(), 28 | }) 29 | ), 30 | }), 31 | }, 32 | update: { 33 | params: Joi.object({ 34 | id: Joi.string().required(), 35 | }), 36 | body: Joi.object({ 37 | name: Joi.string().required(), 38 | }), 39 | }, 40 | delete: { 41 | params: Joi.object({ 42 | id: Joi.string().required(), 43 | }), 44 | }, 45 | deleteMany: { 46 | body: Joi.object({ 47 | ids: Joi.array().items(Joi.string().required()), 48 | }), 49 | }, 50 | } as const; 51 | 52 | export default validator; 53 | -------------------------------------------------------------------------------- /src/features/rbac/channel/channel.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | const validator = { 4 | select: { 5 | query: Joi.object({ 6 | keyword: Joi.string().allow('').optional(), 7 | size: Joi.number().optional(), 8 | page: Joi.number().optional(), 9 | sort: Joi.string().optional(), 10 | order: Joi.string().valid('asc', 'desc').optional(), 11 | }), 12 | }, 13 | detail: { 14 | params: Joi.object({ 15 | id: Joi.string().required(), 16 | }), 17 | }, 18 | create: { 19 | body: Joi.object({ 20 | name: Joi.string().required(), 21 | }), 22 | }, 23 | createMany: { 24 | body: Joi.object({ 25 | channels: Joi.array().items( 26 | Joi.object({ 27 | name: Joi.string().required(), 28 | }) 29 | ), 30 | }), 31 | }, 32 | update: { 33 | params: Joi.object({ 34 | id: Joi.string().required(), 35 | }), 36 | body: Joi.object({ 37 | name: Joi.string().required(), 38 | }), 39 | }, 40 | delete: { 41 | params: Joi.object({ 42 | id: Joi.string().required(), 43 | }), 44 | }, 45 | deleteMany: { 46 | body: Joi.object({ 47 | ids: Joi.array().items(Joi.string().required()), 48 | }), 49 | }, 50 | } as const; 51 | 52 | export default validator; 53 | -------------------------------------------------------------------------------- /src/features/product-category/product-category.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | const validator = { 4 | select: { 5 | query: Joi.object({ 6 | keyword: Joi.string().allow('').optional(), 7 | size: Joi.number().optional(), 8 | page: Joi.number().optional(), 9 | sort: Joi.string().optional(), 10 | order: Joi.string().valid('asc', 'desc').optional(), 11 | }), 12 | }, 13 | detail: { 14 | params: Joi.object({ 15 | id: Joi.string().required(), 16 | }), 17 | }, 18 | create: { 19 | body: Joi.object({ 20 | name: Joi.string().required(), 21 | }), 22 | }, 23 | createMany: { 24 | body: Joi.object({ 25 | productCategories: Joi.array().items( 26 | Joi.object({ 27 | name: Joi.string().required(), 28 | }) 29 | ), 30 | }), 31 | }, 32 | update: { 33 | params: Joi.object({ 34 | id: Joi.string().required(), 35 | }), 36 | body: Joi.object({ 37 | name: Joi.string().required(), 38 | }), 39 | }, 40 | delete: { 41 | params: Joi.object({ 42 | id: Joi.string().required(), 43 | }), 44 | }, 45 | deleteMany: { 46 | body: Joi.object({ 47 | ids: Joi.array().items(Joi.string().required()), 48 | }), 49 | }, 50 | } as const; 51 | 52 | export default validator; 53 | -------------------------------------------------------------------------------- /src/services/sms-poh.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { base64Encode } from '../utils/common'; 3 | import apiClient from '../api-client'; 4 | dotenv.config(); 5 | 6 | const encodedToken = base64Encode( 7 | `${process.env.SMS_API_KEY}:${process.env.SMS_API_SECRET}` 8 | ); 9 | 10 | export const sendSMS = async (phones: string, message: string) => { 11 | return apiClient.post( 12 | 'https://v3.smspoh.com/api/rest/send/bulk', 13 | { 14 | messages: [ 15 | { 16 | to: phones, 17 | message, 18 | from: process.env.SMS_SENDER_ID, 19 | }, 20 | ], 21 | }, 22 | { 23 | headers: { 24 | 'Content-Type': 'application/json', 25 | Authorization: `Bearer ${encodedToken}`, 26 | }, 27 | } 28 | ); 29 | }; 30 | 31 | export const sendBulkSMS = async (phones: string[], message: string) => { 32 | return apiClient.post( 33 | 'https://v3.smspoh.com/api/rest/send/bulk', 34 | { 35 | messages: [ 36 | { 37 | to: phones, 38 | message, 39 | from: process.env.SMS_SENDER_ID, 40 | }, 41 | ], 42 | }, 43 | { 44 | headers: { 45 | 'Content-Type': 'application/json', 46 | Authorization: `Bearer ${encodedToken}`, 47 | }, 48 | } 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/features/rbac/permission/permission.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | const validator = { 4 | select: { 5 | query: Joi.object({ 6 | user_id: Joi.string().optional(), 7 | role_id: Joi.string().optional(), 8 | }), 9 | }, 10 | detail: { 11 | params: Joi.object({ 12 | id: Joi.string().required(), 13 | }), 14 | }, 15 | create: { 16 | body: Joi.object({ 17 | channel_id: Joi.string().required(), 18 | module_id: Joi.string().required(), 19 | sub_module_id: Joi.string().required(), 20 | role_id: Joi.string().required(), 21 | action_id: Joi.string().required(), 22 | }), 23 | }, 24 | update: { 25 | body: Joi.object({ 26 | role_id: Joi.string().required(), 27 | channel_id: Joi.string().required(), 28 | permissions: Joi.array() 29 | .items( 30 | Joi.object({ 31 | module_id: Joi.string().required(), 32 | sub_module_id: Joi.string().required(), 33 | channel_id: Joi.string().required(), 34 | actions: Joi.array().items(Joi.string()).required(), 35 | }) 36 | ) 37 | .required(), 38 | }), 39 | }, 40 | delete: { 41 | params: Joi.object({ 42 | id: Joi.string().required(), 43 | }), 44 | }, 45 | } as const; 46 | 47 | export default validator; 48 | -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | import { ListQuery } from '../types/types'; 2 | 3 | export const base64Encode = (data: string): string => { 4 | return Buffer.from(data).toString('base64'); 5 | }; 6 | 7 | export const getPagination = ({ 8 | page, 9 | size, 10 | }: { 11 | page: number; 12 | size: number; 13 | }) => { 14 | const limit = size; 15 | const offset = page * size; 16 | return { offset, limit }; 17 | }; 18 | 19 | // Reusable function to get paginated data 20 | export async function getPaginatedData( 21 | query: any, 22 | countQuery: any, 23 | filters: ListQuery, 24 | pagination?: { limit: number; offset: number } 25 | ): Promise<{ 26 | data: T[]; 27 | meta: { 28 | page: number; 29 | size: number; 30 | totalCount: number; 31 | totalPages: number; 32 | }; 33 | }> { 34 | // Execute the queries concurrently 35 | const [data, countResult]: any = await Promise.all([query, countQuery]); 36 | 37 | // Calculate pagination data 38 | const totalCount = countResult[0].count; 39 | let totalPages = 1; 40 | if (pagination) totalPages = Math.ceil(totalCount / pagination.limit || 1); 41 | 42 | // Return the paginated data with pagination information 43 | return { 44 | data, 45 | meta: { 46 | page: filters.page, 47 | size: filters.size, 48 | totalCount, 49 | totalPages, 50 | }, 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/queues/email-queue.ts: -------------------------------------------------------------------------------- 1 | import { Queue, Worker } from 'bullmq'; 2 | import IORedis from 'ioredis'; 3 | import dotenv from 'dotenv'; 4 | 5 | // Load environment variables from .env file 6 | dotenv.config(); 7 | 8 | const connection = new IORedis({ 9 | host: process.env.REDIS_HOST || '127.0.0.1', 10 | port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT, 10) : 6379, 11 | maxRetriesPerRequest: null, 12 | }); 13 | 14 | const emailQueue = new Queue('email-queue', { connection }); 15 | 16 | export const addEmailJobs = async (name: string, emails: any) => { 17 | const jobs = emails.map((email: any) => ({ 18 | name: name, 19 | data: email, 20 | })); 21 | return emailQueue.addBulk(jobs); 22 | }; 23 | 24 | const worker = new Worker( 25 | 'email-queue', 26 | async (job: any) => { 27 | console.log(`Sending email to ${job}`); 28 | // Simulate email sending logic 29 | await new Promise((res) => setTimeout(res, 1000)); 30 | 31 | return { status: 'sent' }; 32 | }, 33 | { connection } 34 | ); 35 | 36 | worker.on('completed', (job: any) => { 37 | console.log(`✅ Email sent: ${job.id}`); 38 | }); 39 | 40 | worker.on('failed', (job: any, err: any) => { 41 | console.error(`❌ Failed to send email: ${job.id}`, err); 42 | }); 43 | 44 | // Export worker, queue, and connection for graceful shutdown 45 | export { worker, emailQueue, connection }; 46 | -------------------------------------------------------------------------------- /src/cron-jobs/sample-cron.ts: -------------------------------------------------------------------------------- 1 | import cron from 'node-cron'; 2 | 3 | const CRON_INTERVALS = { 4 | ONE_MIN: '* * * * *', // Every minute 5 | ONE_HOUR: '0 * * * *', // At minute 0 past every hour 6 | SIX_HOUR: '0 */6 * * *', // Every 6th hour 7 | TWELVE_HOUR: '0 */12 * * *', // Every 12th hour 8 | ONE_DAY: '0 0 * * *', // At 00:00 (midnight) every day 9 | ONE_WEEK: '0 0 * * 0', // At 00:00 every Sunday (weekly) 10 | ONE_MONTH: '0 0 1 * *', // At 00:00 on the 1st day of every month 11 | } as const; 12 | 13 | let cronTask: cron.ScheduledTask | null = null; 14 | 15 | // Function to start cron jobs 16 | export function startCronJobs(): cron.ScheduledTask { 17 | if (cronTask) { 18 | return cronTask; 19 | } 20 | 21 | cronTask = cron.schedule(CRON_INTERVALS.ONE_MIN, () => { 22 | console.log('Cron job running every minute'); 23 | // You can replace this with your task, such as sending an email, cleaning up data, etc. 24 | }); 25 | 26 | console.log('✅ Cron jobs started'); 27 | return cronTask; 28 | } 29 | 30 | // Function to stop cron jobs 31 | export function stopCronJobs(): void { 32 | if (cronTask) { 33 | cronTask.stop(); 34 | cronTask = null; 35 | console.log('✅ Cron jobs stopped'); 36 | } 37 | } 38 | 39 | // Export cron task getter for graceful shutdown 40 | export function getCronTask(): cron.ScheduledTask | null { 41 | return cronTask; 42 | } 43 | -------------------------------------------------------------------------------- /src/api-client.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { logAudit } from './utils/log'; 3 | 4 | const apiClient = axios.create(); 5 | 6 | // Request interceptor 7 | apiClient.interceptors.request.use((config) => { 8 | (config as any).metadata = { startTime: Date.now() }; 9 | return config; 10 | }); 11 | 12 | // Response interceptor 13 | apiClient.interceptors.response.use( 14 | (response) => { 15 | const { method, url, data } = response.config; 16 | const status = response.status; 17 | const responseLength = JSON.stringify(response.data).length; 18 | const duration = Date.now() - (response.config as any).metadata.startTime; 19 | 20 | logAudit( 21 | `${method?.toUpperCase()} ${url} ${status} ${duration} ms - ${responseLength} ${JSON.stringify(data)}` 22 | ); 23 | return response; 24 | }, 25 | (error) => { 26 | const config = error.config || {}; 27 | const { method, url, data } = config; 28 | const duration = Date.now() - (config?.metadata?.startTime || Date.now()); 29 | const status = error.response?.status || 'ERROR'; 30 | const responseLength = error.response 31 | ? JSON.stringify(error.response.data).length 32 | : 0; 33 | 34 | logAudit( 35 | `${method?.toUpperCase()} ${url} ${status} ${duration} ms - ${responseLength} ${JSON.stringify(data)}` 36 | ); 37 | return Promise.reject(error); 38 | } 39 | ); 40 | 41 | export default apiClient; 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Step 1: Use official Node image as base 2 | FROM node:20-alpine AS builder 3 | 4 | # Step 2: Set working directory 5 | WORKDIR /app 6 | 7 | # Step 3: Install dependencies 8 | COPY package*.json ./ 9 | RUN npm install 10 | 11 | # Step 4: Copy the source code 12 | COPY . . 13 | 14 | # Step 5: Build TypeScript 15 | RUN npm run build 16 | 17 | # Step 6: Create a smaller image for production 18 | FROM node:20-alpine 19 | 20 | # Step 7: Set working directory 21 | WORKDIR /app 22 | 23 | # Step 8: Copy only necessary files from builder 24 | COPY --from=builder /app/package*.json ./ 25 | COPY --from=builder /app/dist ./dist 26 | COPY --from=builder /app/node_modules ./node_modules 27 | # Copy source files needed for migrations and seeds 28 | COPY --from=builder /app/migrations ./migrations 29 | COPY --from=builder /app/seeds ./seeds 30 | COPY --from=builder /app/knexfile.ts ./knexfile.ts 31 | COPY --from=builder /app/tsconfig.json ./tsconfig.json 32 | COPY --from=builder /app/src ./src 33 | 34 | # Step 9: Install mysql client for health checks 35 | RUN apk add --no-cache mysql-client 36 | 37 | # Step 10: Copy entrypoint script 38 | COPY docker-entrypoint.sh /usr/local/bin/ 39 | RUN chmod +x /usr/local/bin/docker-entrypoint.sh 40 | 41 | # Step 11: Expose port (adjust based on your Express server port) 42 | EXPOSE 3000 43 | 44 | # Step 12: Set entrypoint 45 | ENTRYPOINT ["docker-entrypoint.sh"] 46 | 47 | # Step 13: Run the app 48 | CMD ["node", "dist/server.js"] 49 | -------------------------------------------------------------------------------- /knexfile.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | import dotenv from 'dotenv'; 3 | 4 | // Load environment variables from .env file 5 | dotenv.config(); 6 | 7 | const config: { [key: string]: Knex.Config } = { 8 | development: { 9 | client: 'mysql2', 10 | connection: { 11 | host: process.env.DB_HOST, 12 | port: parseInt(process.env.DB_PORT || '3306'), 13 | user: process.env.DB_USER, 14 | password: process.env.DB_PASSWORD, 15 | database: process.env.DB_NAME, 16 | }, 17 | seeds: { 18 | directory: './seeds', 19 | }, 20 | migrations: { 21 | tableName: 'knex_migrations', 22 | }, 23 | }, 24 | 25 | staging: { 26 | client: 'mysql2', 27 | connection: { 28 | host: process.env.DB_HOST, 29 | port: parseInt(process.env.DB_PORT || '3306'), 30 | user: process.env.DB_USER, 31 | password: process.env.DB_PASSWORD, 32 | database: process.env.DB_NAME, 33 | }, 34 | seeds: { 35 | directory: './seeds', 36 | }, 37 | migrations: { 38 | tableName: 'knex_migrations', 39 | }, 40 | }, 41 | 42 | production: { 43 | client: 'mysql2', 44 | connection: { 45 | host: process.env.DB_HOST, 46 | port: parseInt(process.env.DB_PORT || '3306'), 47 | user: process.env.DB_USER, 48 | password: process.env.DB_PASSWORD, 49 | database: process.env.DB_NAME, 50 | }, 51 | migrations: { 52 | tableName: 'knex_migrations', 53 | }, 54 | }, 55 | }; 56 | 57 | module.exports = config; 58 | -------------------------------------------------------------------------------- /src/features/cached-product-category/cached-product-category.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | const validator = { 4 | select: { 5 | query: Joi.object({ 6 | keyword: Joi.string().allow('').optional(), 7 | size: Joi.number().optional(), 8 | page: Joi.number().optional(), 9 | sort: Joi.string().optional(), 10 | order: Joi.string().valid('asc', 'desc').optional(), 11 | }), 12 | }, 13 | detail: { 14 | params: Joi.object({ 15 | id: Joi.string().required(), 16 | }), 17 | }, 18 | create: { 19 | body: Joi.object({ 20 | name: Joi.string().required(), 21 | }), 22 | }, 23 | createMany: { 24 | body: Joi.object({ 25 | productCategories: Joi.array().items( 26 | Joi.object({ 27 | name: Joi.string().required(), 28 | }) 29 | ), 30 | }), 31 | }, 32 | update: { 33 | params: Joi.object({ 34 | id: Joi.string().required(), 35 | }), 36 | body: Joi.object({ 37 | name: Joi.string().required(), 38 | }), 39 | }, 40 | delete: { 41 | params: Joi.object({ 42 | id: Joi.string().required(), 43 | }), 44 | }, 45 | deleteMany: { 46 | body: Joi.object({ 47 | ids: Joi.array().items(Joi.string().required()), 48 | }), 49 | }, 50 | // Cache management validators 51 | clearCache: { 52 | // No validation needed for cache clearing 53 | }, 54 | getCacheStats: { 55 | // No validation needed for cache stats 56 | }, 57 | } as const; 58 | 59 | export default validator; 60 | -------------------------------------------------------------------------------- /src/middlewares/validation.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { Schema } from 'joi'; 3 | import { AppError } from '../utils/http'; 4 | 5 | interface ValidationSchemas { 6 | body?: Schema; 7 | query?: Schema; 8 | params?: Schema; 9 | } 10 | 11 | export const validateRequest = (schemas: ValidationSchemas) => { 12 | return (req: Request, res: Response, next: NextFunction): void => { 13 | // Validate body 14 | if (schemas.body) { 15 | // Clone the request body to avoid mutating the original request 16 | const bodyToValidate = { ...req.body }; 17 | 18 | // If the user object is optional and not present, remove it from the validation 19 | if (bodyToValidate.user) { 20 | delete bodyToValidate.user; 21 | } 22 | 23 | // Validate the modified body 24 | const { error } = schemas.body.validate(bodyToValidate); 25 | if (error) { 26 | throw new AppError(`${error.details[0].message} in body`, 400); 27 | } 28 | } 29 | 30 | // Validate query 31 | if (schemas.query) { 32 | const { error } = schemas.query.validate(req.query); 33 | if (error) { 34 | throw new AppError(`${error.details[0].message} in query`, 400); 35 | } 36 | } 37 | 38 | // Validate params 39 | if (schemas.params) { 40 | const { error } = schemas.params.validate(req.params); 41 | if (error) { 42 | throw new AppError(`${error.details[0].message} in params`, 400); 43 | } 44 | } 45 | 46 | next(); 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /src/features/rbac/permission/permission.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | getAllPermissionsController, 4 | getAllRoleOnChannelsController, 5 | updatePermissionsByRoleController, 6 | } from './permission.controller'; 7 | import { validateRequest } from '../../../middlewares/validation'; 8 | import validator from './permission.validator'; 9 | import verifyRBAC from '../../../middlewares/rbac'; 10 | import { ACTIONS, MODULES, ROLES, SUB_MODULES } from '../../../configs/rbac'; 11 | 12 | const permissionRoutes = Router(); 13 | 14 | permissionRoutes.get( 15 | '/role-channel-list', 16 | verifyRBAC({ 17 | action: ACTIONS.VIEW, 18 | roles: [ROLES.ADMIN], 19 | module: MODULES.USER_MANAGEMENT, 20 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 21 | }), 22 | validateRequest(validator.select), 23 | getAllRoleOnChannelsController 24 | ); 25 | 26 | permissionRoutes.get( 27 | '/permissions', 28 | verifyRBAC({ 29 | action: ACTIONS.VIEW, 30 | roles: [ROLES.ADMIN], 31 | module: MODULES.USER_MANAGEMENT, 32 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 33 | }), 34 | validateRequest(validator.select), 35 | getAllPermissionsController 36 | ); 37 | 38 | permissionRoutes.patch( 39 | '/permissions', 40 | verifyRBAC({ 41 | action: ACTIONS.UPDATE, 42 | roles: [ROLES.ADMIN], 43 | module: MODULES.USER_MANAGEMENT, 44 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 45 | }), 46 | validateRequest(validator.update), 47 | updatePermissionsByRoleController 48 | ); 49 | 50 | export default permissionRoutes; 51 | -------------------------------------------------------------------------------- /src/features/product/product.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | const validator = { 4 | select: { 5 | query: Joi.object({ 6 | keyword: Joi.string().allow('').optional(), 7 | size: Joi.number().optional(), 8 | page: Joi.number().optional(), 9 | sort: Joi.string().optional(), 10 | order: Joi.string().valid('asc', 'desc').optional(), 11 | }), 12 | }, 13 | detail: { 14 | params: Joi.object({ 15 | id: Joi.string().required(), 16 | }), 17 | }, 18 | create: { 19 | body: Joi.object({ 20 | name: Joi.string().required(), 21 | category_id: Joi.string().required(), 22 | price: Joi.number().required(), 23 | }), 24 | }, 25 | createMany: { 26 | body: Joi.object({ 27 | products: Joi.array().items( 28 | Joi.object({ 29 | name: Joi.string().required(), 30 | category_id: Joi.string().required(), 31 | price: Joi.number().required(), 32 | }) 33 | ), 34 | }), 35 | }, 36 | update: { 37 | params: Joi.object({ 38 | id: Joi.string().required(), 39 | }), 40 | body: Joi.object({ 41 | name: Joi.string().required(), 42 | price: Joi.number().required(), 43 | category_id: Joi.string().required(), 44 | }), 45 | }, 46 | delete: { 47 | params: Joi.object({ 48 | id: Joi.string().required(), 49 | }), 50 | }, 51 | deleteMany: { 52 | body: Joi.object({ 53 | ids: Joi.array().items(Joi.string().required()), 54 | }), 55 | }, 56 | } as const; 57 | 58 | export default validator; 59 | -------------------------------------------------------------------------------- /src/features/rbac/module/module.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | const validator = { 4 | select: { 5 | query: Joi.object({ 6 | keyword: Joi.string().allow('').optional(), 7 | size: Joi.number().optional(), 8 | page: Joi.number().optional(), 9 | sort: Joi.string().optional(), 10 | order: Joi.string().valid('asc', 'desc').optional(), 11 | channel_id: Joi.string().optional(), 12 | }), 13 | }, 14 | detail: { 15 | params: Joi.object({ 16 | id: Joi.string().required(), 17 | }), 18 | }, 19 | moduleWithPermissionSelect: { 20 | query: Joi.object({ 21 | channel_id: Joi.string().optional(), 22 | role_id: Joi.string().optional(), 23 | }), 24 | }, 25 | create: { 26 | body: Joi.object({ 27 | name: Joi.string().required(), 28 | channel_id: Joi.string().required(), 29 | }), 30 | }, 31 | createMany: { 32 | body: Joi.object({ 33 | modules: Joi.array().items( 34 | Joi.object({ 35 | name: Joi.string().required(), 36 | channel_id: Joi.string().required(), 37 | }) 38 | ), 39 | }), 40 | }, 41 | update: { 42 | params: Joi.object({ 43 | id: Joi.string().required(), 44 | }), 45 | body: Joi.object({ 46 | name: Joi.string().required(), 47 | channel_id: Joi.string().required(), 48 | }), 49 | }, 50 | delete: { 51 | params: Joi.object({ 52 | id: Joi.string().required(), 53 | }), 54 | }, 55 | deleteMany: { 56 | body: Joi.object({ 57 | ids: Joi.array().items(Joi.string().required()), 58 | }), 59 | }, 60 | } as const; 61 | 62 | export default validator; 63 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | app: 5 | build: . 6 | container_name: rbac-expressjs-app 7 | ports: 8 | - "3000:3000" 9 | volumes: 10 | - .:/app 11 | - /app/node_modules 12 | depends_on: 13 | redis: 14 | condition: service_started 15 | mysql: 16 | condition: service_healthy 17 | environment: 18 | - REDIS_HOST=redis 19 | - REDIS_PORT=6379 20 | - DB_HOST=mysql 21 | - DB_PORT=3306 22 | - DB_USER=root 23 | - DB_PASSWORD=41199smpK 24 | - DB_NAME=rbac_express 25 | restart: unless-stopped 26 | 27 | redis: 28 | image: "redis:alpine" 29 | container_name: rbac-redis 30 | ports: 31 | - "6379:6379" 32 | volumes: 33 | - redis-data:/data 34 | restart: unless-stopped 35 | 36 | mysql: 37 | image: "mysql:8.0" 38 | container_name: rbac-mysql 39 | ports: 40 | - "3306:3306" 41 | environment: 42 | - MYSQL_ROOT_PASSWORD=41199smpK 43 | - MYSQL_DATABASE=rbac_express 44 | - MYSQL_USER=root 45 | - MYSQL_PASSWORD=41199smpK 46 | volumes: 47 | - mysql-data:/var/lib/mysql 48 | command: --default-authentication-plugin=mysql_native_password 49 | healthcheck: 50 | test: 51 | [ 52 | "CMD", 53 | "mysqladmin", 54 | "ping", 55 | "-h", 56 | "localhost", 57 | "-u", 58 | "root", 59 | "-p41199smpK", 60 | ] 61 | timeout: 20s 62 | retries: 10 63 | interval: 10s 64 | start_period: 30s 65 | restart: unless-stopped 66 | 67 | volumes: 68 | redis-data: 69 | mysql-data: 70 | -------------------------------------------------------------------------------- /src/features/rbac/sub-module/sub-module.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | const validator = { 4 | select: { 5 | query: Joi.object({ 6 | keyword: Joi.string().allow('').optional(), 7 | size: Joi.number().optional(), 8 | page: Joi.number().optional(), 9 | sort: Joi.string().optional(), 10 | order: Joi.string().valid('asc', 'desc').optional(), 11 | channel_id: Joi.string().optional(), 12 | module_id: Joi.string().optional(), 13 | }), 14 | }, 15 | detail: { 16 | params: Joi.object({ 17 | id: Joi.string().required(), 18 | }), 19 | }, 20 | create: { 21 | body: Joi.object({ 22 | name: Joi.string().required(), 23 | channel_id: Joi.string().required(), 24 | module_id: Joi.string().required(), 25 | }), 26 | }, 27 | createMany: { 28 | body: Joi.object({ 29 | subModules: Joi.array().items( 30 | Joi.object({ 31 | name: Joi.string().required(), 32 | channel_id: Joi.string().required(), 33 | module_id: Joi.string().required(), 34 | }) 35 | ), 36 | }), 37 | }, 38 | update: { 39 | params: Joi.object({ 40 | id: Joi.string().required(), 41 | }), 42 | body: Joi.object({ 43 | name: Joi.string().required(), 44 | channel_id: Joi.string().required(), 45 | module_id: Joi.string().required(), 46 | }), 47 | }, 48 | delete: { 49 | params: Joi.object({ 50 | id: Joi.string().required(), 51 | }), 52 | }, 53 | deleteMany: { 54 | body: Joi.object({ 55 | ids: Joi.array().items(Joi.string().required()), 56 | }), 57 | }, 58 | } as const; 59 | 60 | export default validator; 61 | -------------------------------------------------------------------------------- /seeds/01_actions_and_channels.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from 'knex'; 2 | 3 | export async function seed(knex: Knex): Promise { 4 | // Delete existing data 5 | await knex('permission').del(); 6 | await knex('action').del(); 7 | await knex('channel').del(); 8 | 9 | // Insert actions 10 | await knex('action').insert([ 11 | { 12 | id: '242b2ed2-0757-11f0-9bc1-32adce0096f0', 13 | name: 'Create', 14 | description: 'Create new records', 15 | is_deleted: false, 16 | }, 17 | { 18 | id: '242b6262-0757-11f0-9bc1-32adce0096f0', 19 | name: 'Delete', 20 | description: 'Delete records', 21 | is_deleted: false, 22 | }, 23 | { 24 | id: '9e50ed1a-075b-11f0-9bc1-32adce0096f0', 25 | name: 'View', 26 | description: 'View records', 27 | is_deleted: false, 28 | }, 29 | { 30 | id: 'a18f576e-075b-11f0-9bc1-32adce0096f0', 31 | name: 'Update', 32 | description: 'Update existing records', 33 | is_deleted: false, 34 | }, 35 | ]); 36 | 37 | // Insert channels 38 | await knex('channel').insert([ 39 | { 40 | id: '6682d258-05e0-11f0-9bc1-32adce0096f0', 41 | name: 'Web', 42 | description: 'Web application channel', 43 | is_deleted: false, 44 | }, 45 | { 46 | id: 'b366f7a2-0761-11f0-9bc1-32adce0096f0', 47 | name: 'Mobile', 48 | description: 'Mobile application channel', 49 | is_deleted: false, 50 | }, 51 | { 52 | id: 'c477e8b3-0761-11f0-9bc1-32adce0096f0', 53 | name: 'API', 54 | description: 'API channel', 55 | is_deleted: false, 56 | }, 57 | ]); 58 | 59 | console.log('✅ Seeded actions and channels'); 60 | } 61 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import authRoutes from './features/auth/auth.route'; 3 | import productRoutes from './features/product/product.route'; 4 | import userRoutes from './features/user/user.route'; 5 | import { verifyToken } from './middlewares/jwt'; 6 | import actionRoutes from './features/rbac/action/action.route'; 7 | import permissionRoutes from './features/rbac/permission/permission.route'; 8 | import roleRoutes from './features/rbac/role/role.route'; 9 | import channelRoutes from './features/rbac/channel/channel.route'; 10 | import moduleRoutes from './features/rbac/module/module.route'; 11 | import subModuleRoutes from './features/rbac/sub-module/sub-module.route'; 12 | import productCategoryRoutes from './features/product-category/product-category.route'; 13 | import cachedProductCategoryRoutes from './features/cached-product-category/cached-product-category.route'; 14 | 15 | const routes = Router(); 16 | 17 | // Health check route 18 | routes.get('/health', (req, res) => { 19 | res.status(200).json({ status: 'OK', message: 'API is running smoothly!' }); 20 | }); 21 | 22 | // Public routes (No authentication required) 23 | routes.use('/api/auth', authRoutes); 24 | 25 | // Protected routes (Require JWT authentication) 26 | routes.use('/api', verifyToken, moduleRoutes); 27 | routes.use('/api', verifyToken, subModuleRoutes); 28 | routes.use('/api', verifyToken, channelRoutes); 29 | routes.use('/api', verifyToken, roleRoutes); 30 | routes.use('/api', verifyToken, actionRoutes); 31 | routes.use('/api', verifyToken, permissionRoutes); 32 | routes.use('/api', verifyToken, productCategoryRoutes); 33 | routes.use('/api', verifyToken, cachedProductCategoryRoutes); 34 | routes.use('/api', verifyToken, productRoutes); 35 | routes.use('/api', verifyToken, userRoutes); 36 | 37 | export default routes; 38 | -------------------------------------------------------------------------------- /src/features/user/user.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | const validator = { 4 | select: { 5 | query: Joi.object({ 6 | keyword: Joi.string().allow('').optional(), 7 | size: Joi.number().required(), 8 | page: Joi.number().required(), 9 | sort: Joi.string().optional(), 10 | order: Joi.string().valid('asc', 'desc').optional(), 11 | }), 12 | }, 13 | detail: { 14 | params: Joi.object({ 15 | id: Joi.string().required(), 16 | }), 17 | }, 18 | create: { 19 | body: Joi.object({ 20 | username: Joi.string().trim().required(), 21 | first_name: Joi.string().trim().required(), 22 | last_name: Joi.string().trim().required(), 23 | email: Joi.string().trim().email().required(), 24 | phone1: Joi.string().trim().required(), 25 | phone2: Joi.string().trim().allow('').optional(), 26 | phone3: Joi.string().trim().allow('').optional(), 27 | password: Joi.string().trim().min(6).required(), 28 | address1: Joi.string().trim().required(), 29 | address2: Joi.string().trim().allow('').optional(), 30 | }), 31 | }, 32 | update: { 33 | params: Joi.object({ 34 | id: Joi.string().required(), 35 | }), 36 | body: Joi.object({ 37 | username: Joi.string().trim().required(), 38 | first_name: Joi.string().trim().required(), 39 | last_name: Joi.string().trim().required(), 40 | email: Joi.string().trim().email().required(), 41 | phone1: Joi.string().trim().required(), 42 | phone2: Joi.string().trim().allow('').optional(), 43 | phone3: Joi.string().trim().allow('').optional(), 44 | password: Joi.string().trim().min(6).required(), 45 | address1: Joi.string().trim().required(), 46 | address2: Joi.string().trim().allow('').optional(), 47 | }), 48 | }, 49 | delete: { 50 | params: Joi.object({ 51 | id: Joi.string().required(), 52 | }), 53 | }, 54 | } as const; 55 | 56 | export default validator; 57 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import morgan from 'morgan'; 3 | import cors from 'cors'; 4 | import cookieParser from 'cookie-parser'; 5 | import hpp from 'hpp'; 6 | import routes from './routes'; 7 | import { errorHandler } from './middlewares/error-handler'; 8 | import helmet from 'helmet'; 9 | import { accessLogFormat, auditLogFormat } from './configs/log-formats'; 10 | import auditLogStream from './middlewares/audit-log'; 11 | import { upload } from './middlewares/multer-upload'; 12 | 13 | // Initialize the Express application 14 | const app = express(); 15 | 16 | // Middleware to set security-related HTTP headers 17 | app.use(helmet()); 18 | 19 | // HPP middleware to prevent HTTP Parameter Pollution attacks 20 | app.use(hpp()); 21 | 22 | // Enable CORS for all routes and origins 23 | app.use(cors()); 24 | 25 | // Middleware to handle file uploads using multer. 26 | app.use(upload.single('file')); 27 | 28 | // Middleware to parse incoming JSON payloads 29 | app.use(express.json()); 30 | 31 | // Middleware to parse URL-encoded payloads (e.g., form submissions) 32 | app.use(express.urlencoded({ extended: true })); 33 | 34 | // Middleware to parse cookies attached to the client request 35 | app.use(cookieParser()); 36 | 37 | // Access Logger middleware that logs each request in ISO date format, HTTP method, and URL 38 | app.use(morgan(accessLogFormat)); 39 | 40 | // Audit Logger middleware that logs detailed request information to /src/storage/logs/audit.log file 41 | // This includes method, URL, status, response time, content length, and request body 42 | morgan.token('body', (req) => JSON.stringify((req as express.Request).body)); 43 | app.use(morgan(auditLogFormat, { stream: auditLogStream })); 44 | 45 | // Register all application routes 46 | app.use(routes); 47 | 48 | // Register custom error handling middleware at the end 49 | // This ensures it catches errors from previous middlewares or routes 50 | app.use(errorHandler); 51 | 52 | // Export the configured Express app for use (e.g., in server.ts) 53 | export default app; 54 | -------------------------------------------------------------------------------- /src/features/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { 3 | getAccessTokenService, 4 | getPermissionsByRoleService, 5 | getRefreshTokenService, 6 | getUserService, 7 | verifyPasswordService, 8 | } from './auth.service'; 9 | import { AppError, responseData } from '../../utils/http'; 10 | import { MESSAGES } from '../../configs/messages'; 11 | 12 | export async function loginController( 13 | req: Request, 14 | res: Response, 15 | next: NextFunction 16 | ) { 17 | try { 18 | const user = await getUserService({ username: req.body.username }); 19 | if (!user) throw new AppError(MESSAGES.ERROR.USER_NOT_FOUND, 400); 20 | 21 | const isCorrectPassword = await verifyPasswordService( 22 | user.password, 23 | req.body.password 24 | ); 25 | if (!isCorrectPassword) 26 | throw new AppError(MESSAGES.ERROR.INVALID_CREDENTIAL, 400); 27 | delete user.password; 28 | 29 | const accessToken = await getAccessTokenService(user); 30 | const refreshToken = await getRefreshTokenService({ id: user.id }); 31 | const userPermissions = await getPermissionsByRoleService(user.role_id); 32 | 33 | responseData({ 34 | res, 35 | status: 200, 36 | message: MESSAGES.SUCCESS.LOGIN, 37 | data: { 38 | accessToken, 39 | refreshToken, 40 | ...user, 41 | permissions: userPermissions, 42 | }, 43 | }); 44 | } catch (error) { 45 | next(error); 46 | } 47 | } 48 | 49 | export async function refreshTokenController( 50 | req: Request, 51 | res: Response, 52 | next: NextFunction 53 | ) { 54 | try { 55 | const user = await getUserService({ id: req.body.user.id }); 56 | if (!user) throw new AppError(MESSAGES.ERROR.USER_NOT_FOUND, 400); 57 | 58 | const accessToken = await getAccessTokenService(user); 59 | const refreshToken = await getRefreshTokenService({ id: user.id }); 60 | 61 | responseData({ 62 | res, 63 | status: 200, 64 | message: MESSAGES.SUCCESS.CREATE, 65 | data: { accessToken, refreshToken }, 66 | }); 67 | } catch (error) { 68 | next(error); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/features/user/user.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import validator from './user.validator'; 3 | import { validateRequest } from '../../middlewares/validation'; 4 | import { 5 | createOneUserController, 6 | deleteOneUserController, 7 | getAllUsersController, 8 | getOneUserController, 9 | sendEmailToAllUsersController, 10 | updateOneUserController, 11 | } from './user.controller'; 12 | import verifyRBAC from '../../middlewares/rbac'; 13 | import { ACTIONS, MODULES, ROLES, SUB_MODULES } from '../../configs/rbac'; 14 | import { send } from 'process'; 15 | 16 | const userRoutes = Router(); 17 | 18 | userRoutes.post('/send-email-users', sendEmailToAllUsersController); 19 | 20 | userRoutes.get( 21 | '/users', 22 | verifyRBAC({ 23 | action: ACTIONS.VIEW, 24 | roles: [ROLES.ADMIN], 25 | module: MODULES.USER_MANAGEMENT, 26 | subModule: SUB_MODULES.USER, 27 | }), 28 | validateRequest(validator.select), 29 | getAllUsersController 30 | ); 31 | 32 | userRoutes.get( 33 | '/users/:id', 34 | verifyRBAC({ 35 | action: ACTIONS.VIEW, 36 | roles: [ROLES.ADMIN], 37 | module: MODULES.USER_MANAGEMENT, 38 | subModule: SUB_MODULES.USER, 39 | }), 40 | validateRequest(validator.detail), 41 | getOneUserController 42 | ); 43 | 44 | userRoutes.post( 45 | '/users', 46 | verifyRBAC({ 47 | action: ACTIONS.CREATE, 48 | roles: [ROLES.ADMIN], 49 | module: MODULES.USER_MANAGEMENT, 50 | subModule: SUB_MODULES.USER, 51 | }), 52 | validateRequest(validator.create), 53 | createOneUserController 54 | ); 55 | 56 | userRoutes.patch( 57 | '/users/:id', 58 | verifyRBAC({ 59 | action: ACTIONS.UPDATE, 60 | roles: [ROLES.ADMIN], 61 | module: MODULES.USER_MANAGEMENT, 62 | subModule: SUB_MODULES.USER, 63 | }), 64 | validateRequest(validator.update), 65 | updateOneUserController 66 | ); 67 | 68 | userRoutes.delete( 69 | '/users/:id', 70 | verifyRBAC({ 71 | action: ACTIONS.DELETE, 72 | roles: [ROLES.ADMIN], 73 | module: MODULES.USER_MANAGEMENT, 74 | subModule: SUB_MODULES.USER, 75 | }), 76 | validateRequest(validator.delete), 77 | deleteOneUserController 78 | ); 79 | 80 | export default userRoutes; 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-ts-feature-based", 3 | "version": "2.3.0", 4 | "main": "server.js", 5 | "scripts": { 6 | "dev": "nodemon --ext ts --exec ts-node src/server.ts", 7 | "start": "node dist/server.js", 8 | "db:seed": "ts-node knex seed:run", 9 | "db:migrate": "ts-node knex migrate:latest", 10 | "build": "tsc", 11 | "lint": "eslint . --ext .ts", 12 | "lint:fix": "eslint . --ext .ts --fix", 13 | "format": "prettier --write .", 14 | "prepare": "husky", 15 | "release": "standard-version" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "MIT", 20 | "description": "", 21 | "dependencies": { 22 | "axios": "^1.8.4", 23 | "bcrypt": "^5.1.1", 24 | "bullmq": "^5.55.0", 25 | "compression": "^1.8.0", 26 | "cookie-parser": "^1.4.7", 27 | "cors": "^2.8.5", 28 | "dayjs": "^1.11.13", 29 | "dotenv": "^16.4.7", 30 | "express": "^4.21.2", 31 | "helmet": "^8.1.0", 32 | "hpp": "^0.2.3", 33 | "ioredis": "^5.6.1", 34 | "joi": "^17.13.3", 35 | "jsonwebtoken": "^9.0.2", 36 | "knex": "^3.1.0", 37 | "morgan": "^1.10.0", 38 | "multer": "^1.4.5-lts.2", 39 | "mysql2": "^3.12.0", 40 | "node-cron": "^3.0.3", 41 | "nodemailer": "^6.10.1", 42 | "pg": "^8.16.3", 43 | "stripe": "^18.3.0", 44 | "uuid": "^11.1.0" 45 | }, 46 | "devDependencies": { 47 | "@types/bcrypt": "^5.0.2", 48 | "@types/cookie-parser": "^1.4.8", 49 | "@types/cors": "^2.8.17", 50 | "@types/express": "^5.0.0", 51 | "@types/hpp": "^0.2.6", 52 | "@types/jsonwebtoken": "^9.0.9", 53 | "@types/morgan": "^1.9.9", 54 | "@types/multer": "^1.4.12", 55 | "@types/node": "^22.13.1", 56 | "@types/node-cron": "^3.0.11", 57 | "@types/nodemailer": "^6.4.17", 58 | "@types/stripe": "^8.0.416", 59 | "@typescript-eslint/eslint-plugin": "^8.26.1", 60 | "@typescript-eslint/parser": "^8.26.1", 61 | "eslint": "^9.22.0", 62 | "eslint-config-prettier": "^10.1.1", 63 | "eslint-plugin-prettier": "^5.2.3", 64 | "husky": "^9.1.7", 65 | "lint-staged": "^15.5.0", 66 | "nodemon": "^3.1.9", 67 | "prettier": "^3.5.3", 68 | "standard-version": "^9.5.0", 69 | "ts-node": "^10.9.2", 70 | "typescript": "^5.8.2" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /seeds/03_modules_and_submodules.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from 'knex'; 2 | 3 | export async function seed(knex: Knex): Promise { 4 | // Delete existing data 5 | await knex('sub_module').del(); 6 | await knex('module').del(); 7 | 8 | const webChannelId = '6682d258-05e0-11f0-9bc1-32adce0096f0'; 9 | 10 | // Insert modules 11 | await knex('module').insert([ 12 | { 13 | id: '2059b11a-05dc-11f0-9bc1-32adce0096f0', 14 | name: 'User Management', 15 | description: 'User management module', 16 | is_deleted: false, 17 | channel_id: webChannelId, 18 | }, 19 | { 20 | id: '83732965-a8aa-44a4-b1d5-30b2ef267d2a', 21 | name: 'Product', 22 | description: 'Product management module', 23 | is_deleted: false, 24 | channel_id: webChannelId, 25 | }, 26 | { 27 | id: '394472fe-0a42-11f0-9bc1-32adce0096f0', 28 | name: 'Unit', 29 | description: 'Unit management module', 30 | is_deleted: false, 31 | channel_id: webChannelId, 32 | }, 33 | ]); 34 | 35 | // Insert sub-modules 36 | await knex('sub_module').insert([ 37 | { 38 | id: '78a5a376-1e8f-11f0-b5b5-df40a1682685', 39 | name: 'User', 40 | description: 'User sub-module', 41 | is_deleted: false, 42 | module_id: '2059b11a-05dc-11f0-9bc1-32adce0096f0', 43 | channel_id: webChannelId, 44 | }, 45 | { 46 | id: '4ddd5b28-05e6-11f0-9bc1-32adce0096f0', 47 | name: 'User Role Assign', 48 | description: 'User role assignment sub-module', 49 | is_deleted: false, 50 | module_id: '2059b11a-05dc-11f0-9bc1-32adce0096f0', 51 | channel_id: webChannelId, 52 | }, 53 | { 54 | id: '86b59139-2c35-440e-9c1a-5004b2ff3996', 55 | name: 'Product', 56 | description: 'Product sub-module', 57 | is_deleted: false, 58 | module_id: '83732965-a8aa-44a4-b1d5-30b2ef267d2a', 59 | channel_id: webChannelId, 60 | }, 61 | { 62 | id: '1976b4cc-f6c6-4529-b628-6cd03c8c2616', 63 | name: 'Product Category', 64 | description: 'Product category sub-module', 65 | is_deleted: false, 66 | module_id: '83732965-a8aa-44a4-b1d5-30b2ef267d2a', 67 | channel_id: webChannelId, 68 | }, 69 | ]); 70 | 71 | console.log('✅ Seeded modules and sub-modules'); 72 | } 73 | -------------------------------------------------------------------------------- /src/features/rbac/permission/permission.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { responseData } from '../../../utils/http'; 3 | import { MESSAGES } from '../../../configs/messages'; 4 | import { 5 | getRolesOnChannelDataService, 6 | getAllPermissionsService, 7 | deleteManyPermissionsService, 8 | createManyPermissionsService, 9 | } from './permission.service'; 10 | import db from '../../../db/db'; 11 | import { Knex } from 'knex'; 12 | import { ListQuery } from '../../../types/types'; 13 | 14 | export async function getAllRoleOnChannelsController( 15 | req: Request, 16 | res: Response, 17 | next: NextFunction 18 | ) { 19 | try { 20 | const result = await getRolesOnChannelDataService( 21 | req.query as unknown as ListQuery 22 | ); 23 | 24 | responseData({ 25 | res, 26 | status: 200, 27 | message: MESSAGES.SUCCESS.RETRIVE, 28 | data: result, 29 | }); 30 | } catch (error) { 31 | next(error); 32 | } 33 | } 34 | 35 | export async function getAllPermissionsController( 36 | req: Request, 37 | res: Response, 38 | next: NextFunction 39 | ) { 40 | try { 41 | const result = await getAllPermissionsService(req.query); 42 | 43 | responseData({ 44 | res, 45 | status: 200, 46 | message: MESSAGES.SUCCESS.RETRIVE, 47 | data: result, 48 | }); 49 | } catch (error) { 50 | next(error); 51 | } 52 | } 53 | 54 | export async function updatePermissionsByRoleController( 55 | req: Request, 56 | res: Response, 57 | next: NextFunction 58 | ) { 59 | const trx: Knex.Transaction = await db.transaction(); 60 | try { 61 | await deleteManyPermissionsService( 62 | { role_id: req.body.role_id, channel_id: req.body.channel_id }, 63 | trx 64 | ); 65 | 66 | const preparedMultiCreatePayload = req.body.permissions.flatMap( 67 | (permission: Record) => 68 | permission.actions.map((action_id: string | number) => ({ 69 | action_id, 70 | role_id: req.body.role_id, 71 | module_id: permission.module_id, 72 | sub_module_id: permission.sub_module_id, 73 | channel_id: permission.channel_id, 74 | })) 75 | ); 76 | await createManyPermissionsService(preparedMultiCreatePayload, trx); 77 | 78 | await trx.commit(); 79 | 80 | responseData({ 81 | res, 82 | status: 200, 83 | message: MESSAGES.SUCCESS.UPDATE, 84 | data: null, 85 | }); 86 | } catch (error) { 87 | await trx.rollback(); 88 | next(error); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/features/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import db from '../../db/db'; 2 | import bcrypt from 'bcrypt'; 3 | import jwt from 'jsonwebtoken'; 4 | import dotenv from 'dotenv'; 5 | dotenv.config(); 6 | 7 | export async function getUserService(conds: Record) { 8 | const user = await db 9 | .table('user') 10 | .select( 11 | 'user.id', 12 | 'user.username', 13 | 'user.password', 14 | 'user.phone1', 15 | 'user.email', 16 | 'user.is_deleted', 17 | 'user.role_id', 18 | 'role.name as role' 19 | ) 20 | .leftJoin('role', 'role.id', 'user.role_id') 21 | .where(conds); 22 | return user[0] || null; 23 | } 24 | 25 | export async function getPermissionsByRoleService(roleId: string) { 26 | const permissions = await db 27 | .table('permission') 28 | .select( 29 | 'permission.module_id', 30 | 'module.name as module', 31 | 'permission.sub_module_id', 32 | 'sub_module.name as sub_module', 33 | 'permission.role_id', 34 | 'role.name as role', 35 | 'permission.channel_id', 36 | 'channel.name as channel', 37 | db.raw(` 38 | JSON_ARRAYAGG( 39 | JSON_OBJECT('id', action.id, 'name', action.name) 40 | ) as actions 41 | `) 42 | ) 43 | .leftJoin('channel', 'channel.id', 'permission.channel_id') 44 | .leftJoin('module', 'module.id', 'permission.module_id') 45 | .leftJoin('sub_module', 'sub_module.id', 'permission.sub_module_id') 46 | .leftJoin('role', 'role.id', 'permission.role_id') 47 | .leftJoin('action', 'action.id', 'permission.action_id') 48 | .where('permission.role_id', '=', roleId) 49 | .groupBy( 50 | 'permission.module_id', 51 | 'module.name', 52 | 'permission.sub_module_id', 53 | 'sub_module.name', 54 | 'permission.role_id', 55 | 'role.name', 56 | 'permission.channel_id', 57 | 'channel.name' 58 | ); 59 | 60 | return permissions; 61 | } 62 | 63 | export async function getAccessTokenService(payload: Record) { 64 | return jwt.sign(payload, process.env.JWT_SECRET || 'smsk-jwt-secret', { 65 | expiresIn: process.env.JWT_EXPIRES_IN || '1h', 66 | } as jwt.SignOptions); 67 | } 68 | 69 | export async function getRefreshTokenService(payload: Record) { 70 | return jwt.sign( 71 | payload, 72 | process.env.REFRESH_JWT_SECRET || 'smsk-refresh-jwt-secret', 73 | { 74 | expiresIn: process.env.REFRESH_JWT_EXPIRES_IN || '7d', 75 | } as jwt.SignOptions 76 | ); 77 | } 78 | 79 | export async function verifyPasswordService( 80 | hashedPassword: string, 81 | password: string 82 | ) { 83 | return bcrypt.compare(password, hashedPassword); 84 | } 85 | -------------------------------------------------------------------------------- /src/middlewares/jwt.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import jwt from 'jsonwebtoken'; 3 | import dotenv from 'dotenv'; 4 | import { MESSAGES } from '../configs/messages'; 5 | import { AppError } from '../utils/http'; 6 | 7 | // Load environment variables from a .env file 8 | dotenv.config(); 9 | 10 | /** 11 | * Middleware to verify access token 12 | * This function checks for the presence of a Bearer token in the Authorization header. 13 | * If valid, it decodes the token and attaches the payload to the request body. 14 | */ 15 | export function verifyToken(req: Request, res: Response, next: NextFunction) { 16 | const authHeader = req.headers.authorization; 17 | 18 | // Check if the Authorization header is present and correctly formatted 19 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 20 | throw new AppError(MESSAGES.ERROR.UNAUTHORIZED, 401); 21 | } 22 | 23 | // Extract the token from the Authorization header 24 | const token = authHeader.split(' ')[1]; 25 | 26 | // Verify the token using the secret key 27 | jwt.verify( 28 | token, 29 | process.env.JWT_SECRET || 'smsk-jwt-secret', // Fallback secret if env variable is missing 30 | ( 31 | err: jwt.VerifyErrors | null, 32 | decoded: jwt.JwtPayload | string | undefined 33 | ) => { 34 | if (err) { 35 | throw new AppError(MESSAGES.ERROR.UNAUTHORIZED, 401); 36 | } 37 | 38 | // Attach decoded token payload to request body 39 | req.body.user = decoded; 40 | next(); 41 | } 42 | ); 43 | } 44 | 45 | /** 46 | * Middleware to verify refresh token 47 | * This function checks for the presence of a refresh token in the 'x-refresh-token' header. 48 | * If valid, it decodes the token and attaches the payload to the request body. 49 | */ 50 | export function verifyRefreshToken( 51 | req: Request, 52 | res: Response, 53 | next: NextFunction 54 | ) { 55 | const refreshToken = req.headers['x-refresh-token'] as string | undefined; 56 | 57 | // Check if the refresh token is present 58 | if (!refreshToken) { 59 | throw new AppError(MESSAGES.ERROR.UNAUTHORIZED, 401); 60 | } 61 | 62 | // Verify the refresh token using the secret key 63 | jwt.verify( 64 | refreshToken, 65 | process.env.REFRESH_JWT_SECRET || 'smsk-refresh-jwt-secret', // Fallback secret if env variable is missing 66 | ( 67 | err: jwt.VerifyErrors | null, 68 | decoded: jwt.JwtPayload | string | undefined 69 | ) => { 70 | if (err) { 71 | throw new AppError(MESSAGES.ERROR.UNAUTHORIZED, 401); 72 | } 73 | 74 | // Attach decoded token payload to request body 75 | req.body.user = decoded; 76 | next(); 77 | } 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /seeds/04_users.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from 'knex'; 2 | import bcrypt from 'bcrypt'; 3 | 4 | export async function seed(knex: Knex): Promise { 5 | // Delete existing data 6 | await knex('user').del(); 7 | 8 | // Hash passwords (default password: 'password123') 9 | const hashedPassword = await bcrypt.hash('password123', 10); 10 | const adminHashedPassword = await bcrypt.hash('admin123', 10); 11 | 12 | // Insert users 13 | await knex('user').insert([ 14 | { 15 | id: 'ff570050-01a1-11f0-9bc1-32adce0096f0', 16 | username: 'admin', 17 | first_name: 'Admin', 18 | last_name: 'User', 19 | email: 'admin@example.com', 20 | phone1: '09876543210', 21 | phone2: null, 22 | phone3: null, 23 | password: adminHashedPassword, 24 | address1: 'Admin Address', 25 | address2: null, 26 | img: null, 27 | is_deleted: false, 28 | role_id: '91e945da-0a45-11f0-9bc1-32adce0096f0', // Admin role 29 | }, 30 | { 31 | id: 'ab546ce6-f5f2-11ef-9bc1-32adce0096f0', 32 | username: 'superadmin', 33 | first_name: 'Super', 34 | last_name: 'Admin', 35 | email: 'superadmin@example.com', 36 | phone1: '09876543211', 37 | phone2: null, 38 | phone3: null, 39 | password: adminHashedPassword, 40 | address1: 'Super Admin Address', 41 | address2: null, 42 | img: null, 43 | is_deleted: false, 44 | role_id: 'bfbdd16a-05e0-11f0-9bc1-32adce0096f0', // Super Admin role 45 | }, 46 | { 47 | id: 'c1d2e3f4-5678-90ab-cdef-123456789abc', 48 | username: 'developer', 49 | first_name: 'Dev', 50 | last_name: 'User', 51 | email: 'developer@example.com', 52 | phone1: '09876543212', 53 | phone2: null, 54 | phone3: null, 55 | password: hashedPassword, 56 | address1: 'Developer Address', 57 | address2: null, 58 | img: null, 59 | is_deleted: false, 60 | role_id: 'c8c02538-05e0-11f0-9bc1-32adce0096f0', // Developer role 61 | }, 62 | { 63 | id: 'd2e3f4a5-6789-01bc-def0-234567890bcd', 64 | username: 'testuser', 65 | first_name: 'Test', 66 | last_name: 'User', 67 | email: 'testuser@example.com', 68 | phone1: '09876543213', 69 | phone2: null, 70 | phone3: null, 71 | password: hashedPassword, 72 | address1: 'Test User Address', 73 | address2: null, 74 | img: null, 75 | is_deleted: false, 76 | role_id: 'd9d13649-05e0-11f0-9bc1-32adce0096f0', // User role 77 | }, 78 | ]); 79 | 80 | console.log('✅ Seeded users'); 81 | console.log(' Default passwords:'); 82 | console.log(' - admin/superadmin: admin123'); 83 | console.log(' - developer/testuser: password123'); 84 | } 85 | -------------------------------------------------------------------------------- /src/features/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from 'knex'; 2 | import bcrypt from 'bcrypt'; 3 | import db from '../../db/db'; 4 | import { getPaginatedData, getPagination } from '../../utils/common'; 5 | import { ListQuery } from '../../types/types'; 6 | 7 | export async function getAllUsersService(filters: ListQuery) { 8 | const pagination = getPagination({ 9 | page: filters.page as number, 10 | size: filters.size as number, 11 | }); 12 | 13 | const query = db 14 | .table('user') 15 | .select('*') 16 | .limit(pagination.limit) 17 | .offset(pagination.offset); 18 | const totalCountQuery = db.table('user').count('* as count'); 19 | 20 | if (filters.sort) { 21 | query.orderBy(filters.sort, filters.order || 'asc'); 22 | } else { 23 | query.orderBy('user.created_at', 'desc'); 24 | } 25 | 26 | if (filters.keyword) { 27 | query.whereILike('user.name', `%${filters.keyword}%`); 28 | totalCountQuery.whereILike('user.name', `%${filters.keyword}%`); 29 | } 30 | 31 | return getPaginatedData(query, totalCountQuery, filters, pagination); 32 | } 33 | 34 | export async function getOneUserService(id: string | number) { 35 | const user = await db 36 | .table('user') 37 | .select('id', 'name', 'is_deleted') 38 | .where('id', id); 39 | return user[0] || null; 40 | } 41 | 42 | export async function createOneUserService( 43 | data: Record, 44 | trx?: Knex.Transaction 45 | ) { 46 | const query = db.table('user').insert(data); 47 | if (trx) query.transacting(trx); 48 | await query; 49 | console.log('oops'); 50 | return data; 51 | } 52 | 53 | export async function updateOneUserService( 54 | { 55 | id, 56 | data, 57 | }: { 58 | id: string | number; 59 | data: Record; 60 | }, 61 | trx?: Knex.Transaction 62 | ) { 63 | const query = db.table('user').update(data).where('id', id); 64 | 65 | if (trx) query.transacting(trx); 66 | 67 | return query; 68 | } 69 | 70 | export async function deleteOneUserService( 71 | id: string | number, 72 | trx?: Knex.Transaction 73 | ) { 74 | const toDelete = await db.table('user').where('id', id); 75 | 76 | const query = db.table('user').where('id', id).del(); 77 | if (trx) query.transacting(trx); 78 | await query; 79 | 80 | return toDelete[0] || null; 81 | } 82 | 83 | export async function getExistingUserService(data: Record) { 84 | const user = await db 85 | .table('user') 86 | .select('id', 'name', 'is_deleted') 87 | .where(data); 88 | return user[0] || null; 89 | } 90 | 91 | export const hashPasswordService = async (password: string) => { 92 | const saltRounds = 10; 93 | const hashedPassword = await bcrypt.hash(password, saltRounds); 94 | return hashedPassword; 95 | }; 96 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [2.3.0](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/compare/v2.2.1...v2.3.0) (2025-11-29) 6 | 7 | 8 | ### Features 9 | 10 | * added graceful shutdown ([05e40f9](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/commit/05e40f90a7ad5c8c93124007ae8340e7be973499)) 11 | * added seeds and migration schema ([d9ee25f](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/commit/d9ee25ff5e562b6ebaa73dcda1ceb8a21dc5a00a)) 12 | * init seed & migration ([9d82b2a](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/commit/9d82b2abf4b9261670cc87bdc31cd9c4acb19255)) 13 | 14 | ### [2.2.1](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/compare/v2.2.0...v2.2.1) (2025-07-13) 15 | 16 | ## [2.2.0](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/compare/v2.1.0...v2.2.0) (2025-07-13) 17 | 18 | 19 | ### Features 20 | 21 | * **stripe:** added stripe integration ([542c997](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/commit/542c997d161f4b4a0178985c8a1b0681ddb7717f)) 22 | 23 | ## [2.1.0](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/compare/v2.0.1...v2.1.0) (2025-06-28) 24 | 25 | 26 | ### Features 27 | 28 | * **redis-cache:** crud of product category ([d87d78f](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/commit/d87d78fe7221cfe4f96d768daeff7258e1851111)) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * **cached-product-category:** fixed cache key ([28c573f](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/commit/28c573f2489b45dab74e325a6d02b096c7995207)) 34 | 35 | ### [2.0.1](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/compare/v2.0.0...v2.0.1) (2025-06-23) 36 | 37 | 38 | ### Features 39 | 40 | * **change-log:** added release note ([0204f7d](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/commit/0204f7dde005c73d3dc51a76c64b280c5648c6d0)) 41 | 42 | ## [2.0.0](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/compare/v1.2.0...v2.0.0) (2025-06-23) 43 | 44 | ### Features 45 | 46 | - **change-log:** added change log ([b407a68](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/commit/b407a680fc3fdaf83a26ef9a683d2d8691ba403f)) 47 | - **queue:** added bull queue set up ([ccb6dca](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/commit/ccb6dca174ab23ae6d32c3a2dd008f05708fd1c7)) 48 | - **redis-queue:** added redis image in docker compose ([842458d](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/commit/842458de7109692310ed239970ad6ef48d287c88)) 49 | - **redis-queue:** fixed queue ([fe0095e](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/commit/fe0095ef0fb897a83bc4b6f3dcc683c285414409)) 50 | 51 | ### Bug Fixes 52 | 53 | - **rbac:** modules with permissions ([5694130](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/commit/5694130c790db94f68bb13da782ef601b0509ff4)) 54 | 55 | ## [1.1.0](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/compare/v1.0.1...v1.1.0) (2025-05-01) 56 | 57 | ### Features 58 | 59 | - **docs:** updated postman ([31c7c7f](https://github.com/MinPyaeKyaw/rbac-expressjs-starter/commit/31c7c7f02231361f4b80a2f581d9409a50442bf3)) 60 | -------------------------------------------------------------------------------- /src/middlewares/rbac.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import db from '../db/db'; 3 | import { AppError } from '../utils/http'; 4 | import { MESSAGES } from '../configs/messages'; 5 | 6 | interface RBACParams { 7 | roles: string[]; // Allowed roles for access 8 | action: string; // Required action (e.g., 'Create', 'Delete') 9 | module?: string; // Optional module name (e.g., 'User') 10 | subModule?: string; // Optional sub-module name (e.g., 'Profile') 11 | } 12 | 13 | /** 14 | * Middleware to verify if a user has permission to perform a specific action 15 | * based on their role and assigned permissions in the database. 16 | */ 17 | const verifyRBAC = ({ roles, action, module, subModule }: RBACParams) => { 18 | return async (req: Request, res: Response, next: NextFunction) => { 19 | try { 20 | // Step 1: Check if the user's role is in the allowed roles list 21 | if (!roles.includes(req.body.user.role as string)) { 22 | throw new AppError(MESSAGES.ERROR.NO_PERMISSION, 403); 23 | } 24 | 25 | // Step 2: Query the permissions from the database for the user's role 26 | const permissions = await db 27 | .table('permission') 28 | .select( 29 | 'permission.module_id', 30 | 'module.name as module', 31 | 'permission.sub_module_id', 32 | 'sub_module.name as sub_module', 33 | 'permission.role_id', 34 | 'role.name as role', 35 | 'permission.channel_id', 36 | 'channel.name as channel', 37 | db.raw(` 38 | JSON_ARRAYAGG( 39 | JSON_OBJECT('id', action.id, 'name', action.name) 40 | ) as actions 41 | `) // Group actions into a JSON array 42 | ) 43 | .leftJoin('channel', 'channel.id', 'permission.channel_id') 44 | .leftJoin('module', 'module.id', 'permission.module_id') 45 | .leftJoin('sub_module', 'sub_module.id', 'permission.sub_module_id') 46 | .leftJoin('role', 'role.id', 'permission.role_id') 47 | .leftJoin('action', 'action.id', 'permission.action_id') 48 | .where('permission.role_id', '=', req.body.user.role_id) 49 | .groupBy( 50 | 'permission.module_id', 51 | 'module.name', 52 | 'permission.sub_module_id', 53 | 'sub_module.name', 54 | 'permission.role_id', 55 | 'role.name', 56 | 'permission.channel_id', 57 | 'channel.name' 58 | ); 59 | 60 | // Step 3: Find the relevant permission entry for the given module/subModule 61 | const permission = permissions.find( 62 | (permission) => 63 | permission.module === module && permission.sub_module === subModule 64 | ); 65 | 66 | // If no matching module/subModule permission found, deny access 67 | if (!permission) { 68 | throw new AppError(MESSAGES.ERROR.NO_PERMISSION, 403); 69 | } 70 | 71 | // Step 4: Check if the specific action is allowed within the found permission 72 | const isValidAction = permission.actions.find( 73 | (ac: Record) => ac.name === action 74 | ); 75 | 76 | // If the action is not listed, deny access 77 | if (!isValidAction) { 78 | throw new AppError(MESSAGES.ERROR.NO_PERMISSION, 403); 79 | } 80 | 81 | // All checks passed, move to the next middleware or controller 82 | next(); 83 | } catch (err) { 84 | next(err); 85 | } 86 | }; 87 | }; 88 | 89 | export default verifyRBAC; 90 | -------------------------------------------------------------------------------- /src/features/product/product.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import validator from './product.validator'; 3 | import { validateRequest } from '../../middlewares/validation'; 4 | import { 5 | createManyProductsController, 6 | createOneProductController, 7 | deleteManyProductsController, 8 | deleteOneProductController, 9 | getAllProductsController, 10 | getOneProductController, 11 | softDeleteManyProductsController, 12 | softDeleteOneProductController, 13 | updateOneProductController, 14 | } from './product.controller'; 15 | import verifyRBAC from '../../middlewares/rbac'; 16 | import { ACTIONS, MODULES, ROLES, SUB_MODULES } from '../../configs/rbac'; 17 | 18 | const productRoutes = Router(); 19 | 20 | productRoutes.get( 21 | '/products', 22 | verifyRBAC({ 23 | action: ACTIONS.VIEW, 24 | roles: [ROLES.ADMIN], 25 | module: MODULES.PRODUCT, 26 | subModule: SUB_MODULES.PRODUCT, 27 | }), 28 | validateRequest(validator.select), 29 | getAllProductsController 30 | ); 31 | 32 | productRoutes.get( 33 | '/products/:id', 34 | verifyRBAC({ 35 | action: ACTIONS.VIEW, 36 | roles: [ROLES.ADMIN], 37 | module: MODULES.PRODUCT, 38 | subModule: SUB_MODULES.PRODUCT, 39 | }), 40 | validateRequest(validator.detail), 41 | getOneProductController 42 | ); 43 | 44 | productRoutes.post( 45 | '/products', 46 | verifyRBAC({ 47 | action: ACTIONS.CREATE, 48 | roles: [ROLES.ADMIN], 49 | module: MODULES.PRODUCT, 50 | subModule: SUB_MODULES.PRODUCT, 51 | }), 52 | validateRequest(validator.create), 53 | createOneProductController 54 | ); 55 | 56 | productRoutes.post( 57 | '/products/create-many', 58 | verifyRBAC({ 59 | action: ACTIONS.CREATE, 60 | roles: [ROLES.ADMIN], 61 | module: MODULES.PRODUCT, 62 | subModule: SUB_MODULES.PRODUCT, 63 | }), 64 | validateRequest(validator.createMany), 65 | createManyProductsController 66 | ); 67 | 68 | productRoutes.patch( 69 | '/products/:id', 70 | verifyRBAC({ 71 | action: ACTIONS.UPDATE, 72 | roles: [ROLES.ADMIN], 73 | module: MODULES.PRODUCT, 74 | subModule: SUB_MODULES.PRODUCT, 75 | }), 76 | validateRequest(validator.update), 77 | updateOneProductController 78 | ); 79 | 80 | productRoutes.delete( 81 | '/products/:id', 82 | verifyRBAC({ 83 | action: ACTIONS.DELETE, 84 | roles: [ROLES.ADMIN], 85 | module: MODULES.PRODUCT, 86 | subModule: SUB_MODULES.PRODUCT, 87 | }), 88 | validateRequest(validator.delete), 89 | deleteOneProductController 90 | ); 91 | 92 | productRoutes.post( 93 | '/products/delete-many', 94 | verifyRBAC({ 95 | action: ACTIONS.DELETE, 96 | roles: [ROLES.ADMIN], 97 | module: MODULES.PRODUCT, 98 | subModule: SUB_MODULES.PRODUCT, 99 | }), 100 | validateRequest(validator.deleteMany), 101 | deleteManyProductsController 102 | ); 103 | 104 | productRoutes.delete( 105 | '/products/soft-delete/:id', 106 | verifyRBAC({ 107 | action: ACTIONS.DELETE, 108 | roles: [ROLES.ADMIN], 109 | module: MODULES.PRODUCT, 110 | subModule: SUB_MODULES.PRODUCT, 111 | }), 112 | validateRequest(validator.delete), 113 | softDeleteOneProductController 114 | ); 115 | 116 | productRoutes.post( 117 | '/products/soft-delete-many', 118 | verifyRBAC({ 119 | action: ACTIONS.DELETE, 120 | roles: [ROLES.ADMIN], 121 | module: MODULES.PRODUCT, 122 | subModule: SUB_MODULES.PRODUCT, 123 | }), 124 | validateRequest(validator.deleteMany), 125 | softDeleteManyProductsController 126 | ); 127 | 128 | export default productRoutes; 129 | -------------------------------------------------------------------------------- /src/features/rbac/role/role.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | getAllRolesController, 4 | getOneRoleController, 5 | createOneRoleController, 6 | createManyRolesController, 7 | updateOneRoleController, 8 | deleteOneRoleController, 9 | deleteManyRolesController, 10 | softDeleteOneRoleController, 11 | softDeleteManyRolesController, 12 | } from './role.controller'; 13 | import { validateRequest } from '../../../middlewares/validation'; 14 | import validator from './role.validator'; 15 | import verifyRBAC from '../../../middlewares/rbac'; 16 | import { ACTIONS, MODULES, ROLES, SUB_MODULES } from '../../../configs/rbac'; 17 | 18 | const roleRoutes = Router(); 19 | 20 | roleRoutes.get( 21 | '/roles', 22 | verifyRBAC({ 23 | action: ACTIONS.VIEW, 24 | roles: [ROLES.ADMIN], 25 | module: MODULES.USER_MANAGEMENT, 26 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 27 | }), 28 | validateRequest(validator.select), 29 | getAllRolesController 30 | ); 31 | 32 | roleRoutes.get( 33 | '/roles/:id', 34 | verifyRBAC({ 35 | action: ACTIONS.VIEW, 36 | roles: [ROLES.ADMIN], 37 | module: MODULES.USER_MANAGEMENT, 38 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 39 | }), 40 | validateRequest(validator.detail), 41 | getOneRoleController 42 | ); 43 | 44 | roleRoutes.post( 45 | '/roles/create', 46 | verifyRBAC({ 47 | action: ACTIONS.CREATE, 48 | roles: [ROLES.ADMIN], 49 | module: MODULES.USER_MANAGEMENT, 50 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 51 | }), 52 | validateRequest(validator.create), 53 | createOneRoleController 54 | ); 55 | 56 | roleRoutes.post( 57 | '/roles/create-multi', 58 | verifyRBAC({ 59 | action: ACTIONS.CREATE, 60 | roles: [ROLES.ADMIN], 61 | module: MODULES.USER_MANAGEMENT, 62 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 63 | }), 64 | validateRequest(validator.createMany), 65 | createManyRolesController 66 | ); 67 | 68 | roleRoutes.patch( 69 | '/roles/update/:id', 70 | verifyRBAC({ 71 | action: ACTIONS.UPDATE, 72 | roles: [ROLES.ADMIN], 73 | module: MODULES.USER_MANAGEMENT, 74 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 75 | }), 76 | validateRequest(validator.update), 77 | updateOneRoleController 78 | ); 79 | 80 | roleRoutes.delete( 81 | '/roles/delete/:id', 82 | verifyRBAC({ 83 | action: ACTIONS.DELETE, 84 | roles: [ROLES.ADMIN], 85 | module: MODULES.USER_MANAGEMENT, 86 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 87 | }), 88 | validateRequest(validator.delete), 89 | deleteOneRoleController 90 | ); 91 | 92 | roleRoutes.post( 93 | '/roles/delete-multi', 94 | verifyRBAC({ 95 | action: ACTIONS.DELETE, 96 | roles: [ROLES.ADMIN], 97 | module: MODULES.USER_MANAGEMENT, 98 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 99 | }), 100 | validateRequest(validator.deleteMany), 101 | deleteManyRolesController 102 | ); 103 | 104 | roleRoutes.delete( 105 | '/roles/soft-delete/:id', 106 | verifyRBAC({ 107 | action: ACTIONS.DELETE, 108 | roles: [ROLES.ADMIN], 109 | module: MODULES.USER_MANAGEMENT, 110 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 111 | }), 112 | validateRequest(validator.delete), 113 | softDeleteOneRoleController 114 | ); 115 | 116 | roleRoutes.post( 117 | '/roles/soft-delete-multi', 118 | verifyRBAC({ 119 | action: ACTIONS.DELETE, 120 | roles: [ROLES.ADMIN], 121 | module: MODULES.USER_MANAGEMENT, 122 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 123 | }), 124 | validateRequest(validator.deleteMany), 125 | softDeleteManyRolesController 126 | ); 127 | 128 | export default roleRoutes; 129 | -------------------------------------------------------------------------------- /src/features/rbac/action/action.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | createManyActionsController, 4 | createOneActionController, 5 | deleteManyActionsController, 6 | deleteOneActionController, 7 | getAllActionsController, 8 | getOneActionController, 9 | softDeleteManyActionsController, 10 | softDeleteOneActionController, 11 | updateOneActionController, 12 | } from './action.controller'; 13 | import { validateRequest } from '../../../middlewares/validation'; 14 | import validator from './action.validator'; 15 | import verifyRBAC from '../../../middlewares/rbac'; 16 | import { ACTIONS, MODULES, ROLES, SUB_MODULES } from '../../../configs/rbac'; 17 | 18 | const actionRoutes = Router(); 19 | 20 | actionRoutes.get( 21 | '/actions', 22 | verifyRBAC({ 23 | action: ACTIONS.VIEW, 24 | roles: [ROLES.ADMIN], 25 | module: MODULES.USER_MANAGEMENT, 26 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 27 | }), 28 | validateRequest(validator.select), 29 | getAllActionsController 30 | ); 31 | 32 | actionRoutes.get( 33 | '/actions/:id', 34 | verifyRBAC({ 35 | action: ACTIONS.VIEW, 36 | roles: [ROLES.ADMIN], 37 | module: MODULES.USER_MANAGEMENT, 38 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 39 | }), 40 | validateRequest(validator.detail), 41 | getOneActionController 42 | ); 43 | 44 | actionRoutes.post( 45 | '/actions/create', 46 | verifyRBAC({ 47 | action: ACTIONS.CREATE, 48 | roles: [ROLES.ADMIN], 49 | module: MODULES.USER_MANAGEMENT, 50 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 51 | }), 52 | validateRequest(validator.create), 53 | createOneActionController 54 | ); 55 | 56 | actionRoutes.post( 57 | '/actions/create-multi', 58 | verifyRBAC({ 59 | action: ACTIONS.CREATE, 60 | roles: [ROLES.ADMIN], 61 | module: MODULES.USER_MANAGEMENT, 62 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 63 | }), 64 | validateRequest(validator.createMany), 65 | createManyActionsController 66 | ); 67 | 68 | actionRoutes.patch( 69 | '/actions/:id', 70 | verifyRBAC({ 71 | action: ACTIONS.UPDATE, 72 | roles: [ROLES.ADMIN], 73 | module: MODULES.USER_MANAGEMENT, 74 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 75 | }), 76 | validateRequest(validator.update), 77 | updateOneActionController 78 | ); 79 | 80 | actionRoutes.delete( 81 | '/actions/:id', 82 | verifyRBAC({ 83 | action: ACTIONS.DELETE, 84 | roles: [ROLES.ADMIN], 85 | module: MODULES.USER_MANAGEMENT, 86 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 87 | }), 88 | validateRequest(validator.delete), 89 | deleteOneActionController 90 | ); 91 | 92 | actionRoutes.post( 93 | '/actions/delete-multi', 94 | verifyRBAC({ 95 | action: ACTIONS.DELETE, 96 | roles: [ROLES.ADMIN], 97 | module: MODULES.USER_MANAGEMENT, 98 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 99 | }), 100 | validateRequest(validator.deleteMany), 101 | deleteManyActionsController 102 | ); 103 | 104 | actionRoutes.delete( 105 | '/actions/soft-delete/:id', 106 | verifyRBAC({ 107 | action: ACTIONS.DELETE, 108 | roles: [ROLES.ADMIN], 109 | module: MODULES.USER_MANAGEMENT, 110 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 111 | }), 112 | validateRequest(validator.delete), 113 | softDeleteOneActionController 114 | ); 115 | 116 | actionRoutes.post( 117 | '/actions/soft-delete-multi', 118 | verifyRBAC({ 119 | action: ACTIONS.DELETE, 120 | roles: [ROLES.ADMIN], 121 | module: MODULES.USER_MANAGEMENT, 122 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 123 | }), 124 | validateRequest(validator.deleteMany), 125 | softDeleteManyActionsController 126 | ); 127 | 128 | export default actionRoutes; 129 | -------------------------------------------------------------------------------- /src/features/rbac/channel/channel.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | createManyChannelsController, 4 | createOneChannelController, 5 | deleteManyChannelsController, 6 | deleteOneChannelController, 7 | getAllChannelsController, 8 | getOneChannelController, 9 | softDeleteManyChannelsController, 10 | softDeleteOneChannelController, 11 | updateOneChannelController, 12 | } from './channel.controller'; 13 | import { validateRequest } from '../../../middlewares/validation'; 14 | import validator from './channel.validator'; 15 | import verifyRBAC from '../../../middlewares/rbac'; 16 | import { ACTIONS, MODULES, ROLES, SUB_MODULES } from '../../../configs/rbac'; 17 | 18 | const channelRoutes = Router(); 19 | 20 | channelRoutes.get( 21 | '/channels', 22 | verifyRBAC({ 23 | action: ACTIONS.VIEW, 24 | roles: [ROLES.ADMIN], 25 | module: MODULES.USER_MANAGEMENT, 26 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 27 | }), 28 | validateRequest(validator.select), 29 | getAllChannelsController 30 | ); 31 | 32 | channelRoutes.get( 33 | '/channels/:id', 34 | verifyRBAC({ 35 | action: ACTIONS.VIEW, 36 | roles: [ROLES.ADMIN], 37 | module: MODULES.USER_MANAGEMENT, 38 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 39 | }), 40 | validateRequest(validator.detail), 41 | getOneChannelController 42 | ); 43 | 44 | channelRoutes.post( 45 | '/channels/create', 46 | verifyRBAC({ 47 | action: ACTIONS.CREATE, 48 | roles: [ROLES.ADMIN], 49 | module: MODULES.USER_MANAGEMENT, 50 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 51 | }), 52 | validateRequest(validator.create), 53 | createOneChannelController 54 | ); 55 | 56 | channelRoutes.post( 57 | '/channels/create-multi', 58 | verifyRBAC({ 59 | action: ACTIONS.CREATE, 60 | roles: [ROLES.ADMIN], 61 | module: MODULES.USER_MANAGEMENT, 62 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 63 | }), 64 | validateRequest(validator.createMany), 65 | createManyChannelsController 66 | ); 67 | 68 | channelRoutes.patch( 69 | '/channels/update/:id', 70 | verifyRBAC({ 71 | action: ACTIONS.UPDATE, 72 | roles: [ROLES.ADMIN], 73 | module: MODULES.USER_MANAGEMENT, 74 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 75 | }), 76 | validateRequest(validator.update), 77 | updateOneChannelController 78 | ); 79 | 80 | channelRoutes.delete( 81 | '/channels/delete/:id', 82 | verifyRBAC({ 83 | action: ACTIONS.DELETE, 84 | roles: [ROLES.ADMIN], 85 | module: MODULES.USER_MANAGEMENT, 86 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 87 | }), 88 | validateRequest(validator.delete), 89 | deleteOneChannelController 90 | ); 91 | 92 | channelRoutes.post( 93 | '/channels/delete-multi', 94 | verifyRBAC({ 95 | action: ACTIONS.DELETE, 96 | roles: [ROLES.ADMIN], 97 | module: MODULES.USER_MANAGEMENT, 98 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 99 | }), 100 | validateRequest(validator.deleteMany), 101 | deleteManyChannelsController 102 | ); 103 | 104 | channelRoutes.delete( 105 | '/channels/soft-delete/:id', 106 | verifyRBAC({ 107 | action: ACTIONS.DELETE, 108 | roles: [ROLES.ADMIN], 109 | module: MODULES.USER_MANAGEMENT, 110 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 111 | }), 112 | validateRequest(validator.delete), 113 | softDeleteOneChannelController 114 | ); 115 | 116 | channelRoutes.post( 117 | '/channels/soft-delete-multi', 118 | verifyRBAC({ 119 | action: ACTIONS.DELETE, 120 | roles: [ROLES.ADMIN], 121 | module: MODULES.USER_MANAGEMENT, 122 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 123 | }), 124 | validateRequest(validator.deleteMany), 125 | softDeleteManyChannelsController 126 | ); 127 | 128 | export default channelRoutes; 129 | -------------------------------------------------------------------------------- /src/features/rbac/sub-module/sub-module.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | getAllSubModulesController, 4 | getOneSubModuleController, 5 | createOneSubModuleController, 6 | createManySubModulesController, 7 | updateOneSubModuleController, 8 | deleteOneSubModuleController, 9 | deleteManySubModulesController, 10 | softDeleteOneSubModuleController, 11 | softDeleteManySubModulesController, 12 | } from './sub-module.controller'; 13 | import { validateRequest } from '../../../middlewares/validation'; 14 | import validator from './sub-module.validator'; 15 | import verifyRBAC from '../../../middlewares/rbac'; 16 | import { ACTIONS, MODULES, ROLES, SUB_MODULES } from '../../../configs/rbac'; 17 | 18 | const subModuleRoutes = Router(); 19 | 20 | subModuleRoutes.get( 21 | '/sub-modules', 22 | verifyRBAC({ 23 | action: ACTIONS.VIEW, 24 | roles: [ROLES.ADMIN], 25 | module: MODULES.USER_MANAGEMENT, 26 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 27 | }), 28 | validateRequest(validator.select), 29 | getAllSubModulesController 30 | ); 31 | 32 | subModuleRoutes.get( 33 | '/sub-modules/:id', 34 | verifyRBAC({ 35 | action: ACTIONS.VIEW, 36 | roles: [ROLES.ADMIN], 37 | module: MODULES.USER_MANAGEMENT, 38 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 39 | }), 40 | validateRequest(validator.detail), 41 | getOneSubModuleController 42 | ); 43 | 44 | subModuleRoutes.post( 45 | '/sub-modules/create', 46 | verifyRBAC({ 47 | action: ACTIONS.CREATE, 48 | roles: [ROLES.ADMIN], 49 | module: MODULES.USER_MANAGEMENT, 50 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 51 | }), 52 | validateRequest(validator.create), 53 | createOneSubModuleController 54 | ); 55 | 56 | subModuleRoutes.post( 57 | '/sub-modules/create-multi', 58 | verifyRBAC({ 59 | action: ACTIONS.CREATE, 60 | roles: [ROLES.ADMIN], 61 | module: MODULES.USER_MANAGEMENT, 62 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 63 | }), 64 | validateRequest(validator.createMany), 65 | createManySubModulesController 66 | ); 67 | 68 | subModuleRoutes.patch( 69 | '/sub-modules/update/:id', 70 | verifyRBAC({ 71 | action: ACTIONS.UPDATE, 72 | roles: [ROLES.ADMIN], 73 | module: MODULES.USER_MANAGEMENT, 74 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 75 | }), 76 | validateRequest(validator.update), 77 | updateOneSubModuleController 78 | ); 79 | 80 | subModuleRoutes.delete( 81 | '/sub-modules/delete/:id', 82 | verifyRBAC({ 83 | action: ACTIONS.DELETE, 84 | roles: [ROLES.ADMIN], 85 | module: MODULES.USER_MANAGEMENT, 86 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 87 | }), 88 | validateRequest(validator.delete), 89 | deleteOneSubModuleController 90 | ); 91 | 92 | subModuleRoutes.post( 93 | '/sub-modules/delete-multi', 94 | verifyRBAC({ 95 | action: ACTIONS.DELETE, 96 | roles: [ROLES.ADMIN], 97 | module: MODULES.USER_MANAGEMENT, 98 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 99 | }), 100 | validateRequest(validator.deleteMany), 101 | deleteManySubModulesController 102 | ); 103 | 104 | subModuleRoutes.delete( 105 | '/sub-modules/soft-delete/:id', 106 | verifyRBAC({ 107 | action: ACTIONS.DELETE, 108 | roles: [ROLES.ADMIN], 109 | module: MODULES.USER_MANAGEMENT, 110 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 111 | }), 112 | validateRequest(validator.delete), 113 | softDeleteOneSubModuleController 114 | ); 115 | 116 | subModuleRoutes.post( 117 | '/sub-modules/soft-delete-multi', 118 | verifyRBAC({ 119 | action: ACTIONS.DELETE, 120 | roles: [ROLES.ADMIN], 121 | module: MODULES.USER_MANAGEMENT, 122 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 123 | }), 124 | validateRequest(validator.deleteMany), 125 | softDeleteManySubModulesController 126 | ); 127 | 128 | export default subModuleRoutes; 129 | -------------------------------------------------------------------------------- /src/features/product-category/product-category.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import validator from './product-category.validator'; 3 | import { validateRequest } from '../../middlewares/validation'; 4 | import { 5 | createManyProductCategoriesController, 6 | createOneProductCategoryController, 7 | deleteManyProductCategoriesController, 8 | deleteOneProductCategoryController, 9 | getAllProductCategoriesController, 10 | getOneProductCategoryController, 11 | softDeleteManyProductCategoriesController, 12 | softDeleteOneProductCategoryController, 13 | updateOneProductCategoryController, 14 | } from './product-category.controller'; 15 | import verifyRBAC from '../../middlewares/rbac'; 16 | import { ACTIONS, MODULES, ROLES, SUB_MODULES } from '../../configs/rbac'; 17 | 18 | const productCategoryRoutes = Router(); 19 | 20 | productCategoryRoutes.get( 21 | '/product-categories', 22 | verifyRBAC({ 23 | action: ACTIONS.VIEW, 24 | roles: [ROLES.ADMIN], 25 | module: MODULES.PRODUCT, 26 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 27 | }), 28 | validateRequest(validator.select), 29 | getAllProductCategoriesController 30 | ); 31 | 32 | productCategoryRoutes.get( 33 | '/product-categories/:id', 34 | verifyRBAC({ 35 | action: ACTIONS.VIEW, 36 | roles: [ROLES.ADMIN], 37 | module: MODULES.PRODUCT, 38 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 39 | }), 40 | validateRequest(validator.detail), 41 | getOneProductCategoryController 42 | ); 43 | 44 | productCategoryRoutes.post( 45 | '/product-categories', 46 | verifyRBAC({ 47 | action: ACTIONS.CREATE, 48 | roles: [ROLES.ADMIN], 49 | module: MODULES.PRODUCT, 50 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 51 | }), 52 | validateRequest(validator.create), 53 | createOneProductCategoryController 54 | ); 55 | 56 | productCategoryRoutes.post( 57 | '/product-categories/create-multi', 58 | verifyRBAC({ 59 | action: ACTIONS.CREATE, 60 | roles: [ROLES.ADMIN], 61 | module: MODULES.PRODUCT, 62 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 63 | }), 64 | validateRequest(validator.createMany), 65 | createManyProductCategoriesController 66 | ); 67 | 68 | productCategoryRoutes.patch( 69 | '/product-categories/:id', 70 | verifyRBAC({ 71 | action: ACTIONS.UPDATE, 72 | roles: [ROLES.ADMIN], 73 | module: MODULES.PRODUCT, 74 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 75 | }), 76 | validateRequest(validator.update), 77 | updateOneProductCategoryController 78 | ); 79 | 80 | productCategoryRoutes.delete( 81 | '/product-categories/:id', 82 | verifyRBAC({ 83 | action: ACTIONS.DELETE, 84 | roles: [ROLES.ADMIN], 85 | module: MODULES.PRODUCT, 86 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 87 | }), 88 | validateRequest(validator.delete), 89 | deleteOneProductCategoryController 90 | ); 91 | 92 | productCategoryRoutes.post( 93 | '/product-categories/delete-multi', 94 | verifyRBAC({ 95 | action: ACTIONS.DELETE, 96 | roles: [ROLES.ADMIN], 97 | module: MODULES.PRODUCT, 98 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 99 | }), 100 | validateRequest(validator.deleteMany), 101 | deleteManyProductCategoriesController 102 | ); 103 | 104 | productCategoryRoutes.delete( 105 | '/product-categories/soft-delete/:id', 106 | verifyRBAC({ 107 | action: ACTIONS.DELETE, 108 | roles: [ROLES.ADMIN], 109 | module: MODULES.PRODUCT, 110 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 111 | }), 112 | validateRequest(validator.delete), 113 | softDeleteOneProductCategoryController 114 | ); 115 | 116 | productCategoryRoutes.post( 117 | '/product-categories/soft-delete-multi', 118 | verifyRBAC({ 119 | action: ACTIONS.DELETE, 120 | roles: [ROLES.ADMIN], 121 | module: MODULES.PRODUCT, 122 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 123 | }), 124 | validateRequest(validator.deleteMany), 125 | softDeleteManyProductCategoriesController 126 | ); 127 | 128 | export default productCategoryRoutes; 129 | -------------------------------------------------------------------------------- /src/features/rbac/module/module.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | createManyModulesController, 4 | createOneModuleController, 5 | deleteManyModulesController, 6 | deleteOneModuleController, 7 | getAllModulesController, 8 | getAllModulesWithPermissionsController, 9 | getOneModuleController, 10 | softDeleteManyModulesController, 11 | softDeleteOneModuleController, 12 | updateOneModuleController, 13 | } from './module.controller'; 14 | import { validateRequest } from '../../../middlewares/validation'; 15 | import validator from './module.validator'; 16 | import verifyRBAC from '../../../middlewares/rbac'; 17 | import { ACTIONS, MODULES, ROLES, SUB_MODULES } from '../../../configs/rbac'; 18 | 19 | const moduleRoutes = Router(); 20 | 21 | moduleRoutes.get( 22 | '/modules-with-permissions', 23 | verifyRBAC({ 24 | action: ACTIONS.VIEW, 25 | roles: [ROLES.ADMIN], 26 | module: MODULES.USER_MANAGEMENT, 27 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 28 | }), 29 | validateRequest(validator.moduleWithPermissionSelect), 30 | getAllModulesWithPermissionsController 31 | ); 32 | 33 | moduleRoutes.get( 34 | '/modules', 35 | verifyRBAC({ 36 | action: ACTIONS.VIEW, 37 | roles: [ROLES.ADMIN], 38 | module: MODULES.USER_MANAGEMENT, 39 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 40 | }), 41 | validateRequest(validator.select), 42 | getAllModulesController 43 | ); 44 | 45 | moduleRoutes.get( 46 | '/modules/:id', 47 | verifyRBAC({ 48 | action: ACTIONS.VIEW, 49 | roles: [ROLES.ADMIN], 50 | module: MODULES.USER_MANAGEMENT, 51 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 52 | }), 53 | validateRequest(validator.detail), 54 | getOneModuleController 55 | ); 56 | 57 | moduleRoutes.post( 58 | '/modules/create', 59 | verifyRBAC({ 60 | action: ACTIONS.CREATE, 61 | roles: [ROLES.ADMIN], 62 | module: MODULES.USER_MANAGEMENT, 63 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 64 | }), 65 | validateRequest(validator.create), 66 | createOneModuleController 67 | ); 68 | 69 | moduleRoutes.post( 70 | '/modules/create-multi', 71 | verifyRBAC({ 72 | action: ACTIONS.CREATE, 73 | roles: [ROLES.ADMIN], 74 | module: MODULES.USER_MANAGEMENT, 75 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 76 | }), 77 | validateRequest(validator.createMany), 78 | createManyModulesController 79 | ); 80 | 81 | moduleRoutes.patch( 82 | '/modules/update/:id', 83 | verifyRBAC({ 84 | action: ACTIONS.UPDATE, 85 | roles: [ROLES.ADMIN], 86 | module: MODULES.USER_MANAGEMENT, 87 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 88 | }), 89 | validateRequest(validator.update), 90 | updateOneModuleController 91 | ); 92 | 93 | moduleRoutes.delete( 94 | '/modules/delete/:id', 95 | verifyRBAC({ 96 | action: ACTIONS.DELETE, 97 | roles: [ROLES.ADMIN], 98 | module: MODULES.USER_MANAGEMENT, 99 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 100 | }), 101 | validateRequest(validator.delete), 102 | deleteOneModuleController 103 | ); 104 | 105 | moduleRoutes.post( 106 | '/modules/delete-multi', 107 | verifyRBAC({ 108 | action: ACTIONS.DELETE, 109 | roles: [ROLES.ADMIN], 110 | module: MODULES.USER_MANAGEMENT, 111 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 112 | }), 113 | validateRequest(validator.deleteMany), 114 | deleteManyModulesController 115 | ); 116 | 117 | moduleRoutes.delete( 118 | '/modules/soft-delete/:id', 119 | verifyRBAC({ 120 | action: ACTIONS.DELETE, 121 | roles: [ROLES.ADMIN], 122 | module: MODULES.USER_MANAGEMENT, 123 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 124 | }), 125 | validateRequest(validator.delete), 126 | softDeleteOneModuleController 127 | ); 128 | 129 | moduleRoutes.post( 130 | '/modules/soft-delete-multi', 131 | verifyRBAC({ 132 | action: ACTIONS.DELETE, 133 | roles: [ROLES.ADMIN], 134 | module: MODULES.USER_MANAGEMENT, 135 | subModule: SUB_MODULES.USER_ROLE_ASSIGN, 136 | }), 137 | validateRequest(validator.deleteMany), 138 | softDeleteManyModulesController 139 | ); 140 | 141 | export default moduleRoutes; 142 | -------------------------------------------------------------------------------- /src/features/rbac/role/role.service.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from 'knex'; 2 | import db from '../../../db/db'; 3 | import { getPaginatedData, getPagination } from '../../../utils/common'; 4 | import { ListQuery } from '../../../types/types'; 5 | 6 | export async function getAllRolesService(filters: ListQuery) { 7 | const query = db 8 | .table('role') 9 | .select('id', 'name', 'is_deleted') 10 | .where('is_deleted', 0); 11 | const totalCountQuery = db.table('role').count('* as count'); 12 | 13 | let pagination; 14 | if (filters.page && filters.size) { 15 | pagination = getPagination({ 16 | page: filters.page as number, 17 | size: filters.size as number, 18 | }); 19 | query.limit(pagination.limit).offset(pagination.offset); 20 | } 21 | 22 | if (filters.sort) { 23 | query.orderBy(filters.sort, filters.order || 'asc'); 24 | } else { 25 | query.orderBy('created_at', 'desc'); 26 | } 27 | 28 | if (filters.keyword) { 29 | query.whereILike('name', `%${filters.keyword}%`); 30 | totalCountQuery.whereILike('name', `%${filters.keyword}%`); 31 | } 32 | 33 | return getPaginatedData(query, totalCountQuery, filters, pagination); 34 | } 35 | 36 | export async function getOneRoleService(id: string | number) { 37 | const role = await db 38 | .table('role') 39 | .select('id', 'name', 'is_deleted') 40 | .where('id', id); 41 | return role[0] || null; 42 | } 43 | 44 | export async function createOneRoleService( 45 | data: Record, 46 | trx?: Knex.Transaction 47 | ) { 48 | const query = db.table('role').insert(data); 49 | if (trx) query.transacting(trx); 50 | await query; 51 | 52 | return data; 53 | } 54 | 55 | export async function createManyRolesService( 56 | data: Record[], 57 | trx?: Knex.Transaction 58 | ) { 59 | const query = db.table('role').insert(data); 60 | 61 | if (trx) query.transacting(trx); 62 | 63 | return query; 64 | } 65 | 66 | export async function updateOneRoleService( 67 | { 68 | id, 69 | data, 70 | }: { 71 | id: string | number; 72 | data: Record; 73 | }, 74 | trx?: Knex.Transaction 75 | ) { 76 | const query = db.table('role').update(data).where('id', id); 77 | 78 | if (trx) query.transacting(trx); 79 | 80 | return query; 81 | } 82 | 83 | export async function deleteOneRoleService( 84 | id: string | number, 85 | trx?: Knex.Transaction 86 | ) { 87 | const toDelete = await db.table('role').select('*').where('id', id); 88 | 89 | const query = db.table('role').where('id', id).del(); 90 | if (trx) query.transacting(trx); 91 | await query; 92 | 93 | return toDelete[0] || null; 94 | } 95 | 96 | export async function deleteManyRolesService( 97 | ids: string[], 98 | trx?: Knex.Transaction 99 | ) { 100 | const toDelete = await db.table('role').select('*').whereIn('id', ids); 101 | 102 | const query = db.table('role').whereIn('id', ids).del(); 103 | if (trx) query.transacting(trx); 104 | await query; 105 | 106 | return toDelete || null; 107 | } 108 | 109 | export async function softDeleteOneRoleService( 110 | id: string | number, 111 | trx?: Knex.Transaction 112 | ) { 113 | const toDelete = await db.table('role').select('*').where('id', id); 114 | 115 | const query = db.table('role').update({ is_deleted: true }).where('id', id); 116 | if (trx) query.transacting(trx); 117 | await query; 118 | 119 | return toDelete[0] || null; 120 | } 121 | 122 | export async function softDeleteManyRolesService( 123 | ids: string[] | number[], 124 | trx?: Knex.Transaction 125 | ) { 126 | const toDelete = await db.table('role').select('*').whereIn('id', ids); 127 | 128 | const query = db 129 | .table('role') 130 | .update({ is_deleted: true }) 131 | .whereIn('id', ids); 132 | if (trx) query.transacting(trx); 133 | await query; 134 | 135 | return toDelete || null; 136 | } 137 | 138 | export async function getExistingRoleService(data: Record) { 139 | const role = await db 140 | .table('role') 141 | .select('id', 'name', 'is_deleted') 142 | .where(data); 143 | return role[0] || null; 144 | } 145 | -------------------------------------------------------------------------------- /src/features/rbac/action/action.service.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from 'knex'; 2 | import db from '../../../db/db'; 3 | import { getPaginatedData, getPagination } from '../../../utils/common'; 4 | import { ListQuery } from '../../../types/types'; 5 | 6 | export async function getAllActionsService(filters: ListQuery) { 7 | const query = db 8 | .table('action') 9 | .select('id', 'name', 'is_deleted') 10 | .where('is_deleted', 0); 11 | const totalCountQuery = db.table('action').count('* as count'); 12 | 13 | let pagination; 14 | if (filters.page && filters.size) { 15 | pagination = getPagination({ 16 | page: filters.page as number, 17 | size: filters.size as number, 18 | }); 19 | query.limit(pagination.limit).offset(pagination.offset); 20 | } 21 | 22 | if (filters.sort) { 23 | query.orderBy(filters.sort, filters.order || 'asc'); 24 | } else { 25 | query.orderBy('created_at', 'desc'); 26 | } 27 | 28 | if (filters.keyword) { 29 | query.whereILike('name', `%${filters.keyword}%`); 30 | totalCountQuery.whereILike('name', `%${filters.keyword}%`); 31 | } 32 | 33 | return getPaginatedData(query, totalCountQuery, filters, pagination); 34 | } 35 | 36 | export async function getOneActionService(id: string | number) { 37 | const action = await db 38 | .table('action') 39 | .select('id', 'name', 'is_deleted') 40 | .where('id', id); 41 | return action[0] || null; 42 | } 43 | 44 | export async function createOneActionService( 45 | data: Record, 46 | trx?: Knex.Transaction 47 | ) { 48 | const query = db.table('action').insert(data); 49 | if (trx) query.transacting(trx); 50 | await query; 51 | 52 | return data; 53 | } 54 | 55 | export async function createManyActionsService( 56 | data: Record[], 57 | trx?: Knex.Transaction 58 | ) { 59 | const query = db.table('action').insert(data); 60 | if (trx) query.transacting(trx); 61 | await query; 62 | 63 | return data; 64 | } 65 | 66 | export async function updateOneActionService( 67 | { 68 | id, 69 | data, 70 | }: { 71 | id: string | number; 72 | data: Record; 73 | }, 74 | trx?: Knex.Transaction 75 | ) { 76 | const query = db.table('action').update(data).where('id', id); 77 | 78 | if (trx) query.transacting(trx); 79 | 80 | return query; 81 | } 82 | 83 | export async function deleteOneActionService( 84 | id: string | number, 85 | trx?: Knex.Transaction 86 | ) { 87 | const toDelete = await db.table('action').select('*').where('id', id); 88 | 89 | const query = db.table('action').where('id', id).del(); 90 | if (trx) query.transacting(trx); 91 | await query; 92 | 93 | return toDelete; 94 | } 95 | 96 | export async function deleteManyActionsService( 97 | ids: string[], 98 | trx?: Knex.Transaction 99 | ) { 100 | const toDelete = await db.table('action').select('*').whereIn('id', ids); 101 | 102 | const query = db.table('action').whereIn('id', ids).del(); 103 | if (trx) query.transacting(trx); 104 | await query; 105 | 106 | return toDelete || null; 107 | } 108 | 109 | export async function softDeleteOneActionService( 110 | id: string | number, 111 | trx?: Knex.Transaction 112 | ) { 113 | const toDelete = await db.table('action').select('*').where('id', id); 114 | 115 | const query = db.table('action').update({ is_deleted: true }).where('id', id); 116 | if (trx) query.transacting(trx); 117 | await query; 118 | 119 | return toDelete[0] || null; 120 | } 121 | 122 | export async function softDeleteManyActionsService( 123 | ids: string[] | number[], 124 | trx?: Knex.Transaction 125 | ) { 126 | const toDelete = await db.table('action').select('*').whereIn('id', ids); 127 | 128 | const query = db 129 | .table('action') 130 | .update({ is_deleted: true }) 131 | .whereIn('id', ids); 132 | if (trx) query.transacting(trx); 133 | await query; 134 | 135 | return toDelete || null; 136 | } 137 | 138 | export async function getExistingActionService(data: Record) { 139 | const action = await db 140 | .table('action') 141 | .select('id', 'name', 'is_deleted') 142 | .where(data); 143 | return action[0] || null; 144 | } 145 | -------------------------------------------------------------------------------- /src/features/rbac/channel/channel.service.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from 'knex'; 2 | import db from '../../../db/db'; 3 | import { getPaginatedData, getPagination } from '../../../utils/common'; 4 | import { ListQuery } from '../../../types/types'; 5 | 6 | export async function getAllChannelsService(filters: ListQuery) { 7 | const query = db 8 | .table('channel') 9 | .select('id', 'name', 'is_deleted') 10 | .where('is_deleted', 0); 11 | const totalCountQuery = db.table('channel').count('* as count'); 12 | 13 | let pagination; 14 | if (filters.page && filters.size) { 15 | pagination = getPagination({ 16 | page: filters.page as number, 17 | size: filters.size as number, 18 | }); 19 | query.limit(pagination.limit).offset(pagination.offset); 20 | } 21 | 22 | if (filters.sort) { 23 | query.orderBy(filters.sort, filters.order || 'asc'); 24 | } else { 25 | query.orderBy('created_at', 'desc'); 26 | } 27 | 28 | if (filters.keyword) { 29 | query.whereILike('name', `%${filters.keyword}%`); 30 | totalCountQuery.whereILike('name', `%${filters.keyword}%`); 31 | } 32 | 33 | return getPaginatedData(query, totalCountQuery, filters, pagination); 34 | } 35 | 36 | export async function getOneChannelService(id: string | number) { 37 | const channel = await db 38 | .table('channel') 39 | .select('id', 'name', 'is_deleted') 40 | .where('id', id); 41 | return channel[0] || null; 42 | } 43 | 44 | export async function createOneChannelService( 45 | data: Record, 46 | trx?: Knex.Transaction 47 | ) { 48 | const query = db.table('channel').insert(data); 49 | if (trx) query.transacting(trx); 50 | await query; 51 | 52 | return data; 53 | } 54 | 55 | export async function createManyChannelsService( 56 | data: Record[], 57 | trx?: Knex.Transaction 58 | ) { 59 | const query = db.table('channel').insert(data); 60 | if (trx) query.transacting(trx); 61 | await query; 62 | 63 | return data; 64 | } 65 | 66 | export async function updateOneChannelService( 67 | { 68 | id, 69 | data, 70 | }: { 71 | id: string | number; 72 | data: Record; 73 | }, 74 | trx?: Knex.Transaction 75 | ) { 76 | const query = db.table('channel').update(data).where('id', id); 77 | 78 | if (trx) query.transacting(trx); 79 | 80 | return query; 81 | } 82 | 83 | export async function deleteOneChannelService( 84 | id: string | number, 85 | trx?: Knex.Transaction 86 | ) { 87 | const toDelete = await db.table('channel').select('*').where('id', id); 88 | 89 | const query = db.table('channel').where('id', id).del(); 90 | if (trx) query.transacting(trx); 91 | await query; 92 | 93 | return toDelete[0] || null; 94 | } 95 | 96 | export async function deleteManyChannelsService( 97 | ids: string[], 98 | trx?: Knex.Transaction 99 | ) { 100 | const toDelete = await db.table('channel').select('*').whereIn('id', ids); 101 | 102 | const query = db.table('channel').whereIn('id', ids).del(); 103 | if (trx) query.transacting(trx); 104 | await query; 105 | 106 | return toDelete || null; 107 | } 108 | 109 | export async function softDeleteOneChannelService( 110 | id: string | number, 111 | trx?: Knex.Transaction 112 | ) { 113 | const toDelete = await db.table('channel').select('*').where('id', id); 114 | 115 | const query = db 116 | .table('channel') 117 | .update({ is_deleted: true }) 118 | .where('id', id); 119 | if (trx) query.transacting(trx); 120 | await query; 121 | 122 | return toDelete[0] || null; 123 | } 124 | 125 | export async function softDeleteManyChannelsService( 126 | ids: string[] | number[], 127 | trx?: Knex.Transaction 128 | ) { 129 | const toDelete = await db.table('channel').select('*').whereIn('id', ids); 130 | 131 | const query = db 132 | .table('channel') 133 | .update({ is_deleted: true }) 134 | .whereIn('id', ids); 135 | if (trx) query.transacting(trx); 136 | await query; 137 | 138 | return toDelete || null; 139 | } 140 | 141 | export async function getExistingChannelService(data: Record) { 142 | const channel = await db 143 | .table('channel') 144 | .select('id', 'name', 'is_deleted') 145 | .where(data); 146 | return channel[0] || null; 147 | } 148 | -------------------------------------------------------------------------------- /src/features/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { AppError, responseData } from '../../utils/http'; 3 | import { MESSAGES } from '../../configs/messages'; 4 | import { 5 | createOneUserService, 6 | deleteOneUserService, 7 | getAllUsersService, 8 | getOneUserService, 9 | hashPasswordService, 10 | updateOneUserService, 11 | } from './user.service'; 12 | import db from '../../db/db'; 13 | import { Knex } from 'knex'; 14 | import { ListQuery } from '../../types/types'; 15 | import { v4 as uuidv4 } from 'uuid'; 16 | import { addEmailJobs } from '../../queues/email-queue'; 17 | 18 | export async function sendEmailToAllUsersController( 19 | req: Request, 20 | res: Response, 21 | next: NextFunction 22 | ) { 23 | try { 24 | await addEmailJobs('user-email', ['minpyaekyaw419@gmail.com']); 25 | 26 | responseData({ 27 | res, 28 | status: 200, 29 | message: 'Emails added to queue!', 30 | data: null, 31 | }); 32 | } catch (error) { 33 | next(error); 34 | } 35 | } 36 | 37 | export async function getAllUsersController( 38 | req: Request, 39 | res: Response, 40 | next: NextFunction 41 | ) { 42 | try { 43 | const result = await getAllUsersService(req.query as unknown as ListQuery); 44 | 45 | responseData({ 46 | res, 47 | status: 200, 48 | message: MESSAGES.SUCCESS.RETRIVE, 49 | data: result, 50 | }); 51 | } catch (error) { 52 | next(error); 53 | } 54 | } 55 | 56 | export async function getOneUserController( 57 | req: Request, 58 | res: Response, 59 | next: NextFunction 60 | ) { 61 | try { 62 | const user = await getOneUserService(req.params.id); 63 | 64 | responseData({ 65 | res, 66 | status: 200, 67 | message: MESSAGES.SUCCESS.RETRIVE, 68 | data: user, 69 | }); 70 | } catch (error) { 71 | next(error); 72 | } 73 | } 74 | 75 | export async function createOneUserController( 76 | req: Request, 77 | res: Response, 78 | next: NextFunction 79 | ) { 80 | const trx: Knex.Transaction = await db.transaction(); 81 | try { 82 | if (!req.file) { 83 | throw new AppError(`File is required!`, 400); 84 | } 85 | 86 | const password = await hashPasswordService(req.body.password); 87 | const payload = { 88 | id: uuidv4(), 89 | username: req.body.username, 90 | first_name: req.body.first_name, 91 | last_name: req.body.last_name, 92 | email: req.body.email, 93 | phone1: req.body.phone1, 94 | phone2: req.body.phone2, 95 | phone3: req.body.phone3, 96 | password: password, 97 | address1: req.body.address1, 98 | address2: req.body.address2, 99 | img: req.file.path, 100 | created_by: req.body.user.id, 101 | }; 102 | const createdUser = await createOneUserService(payload, trx); 103 | 104 | await trx.commit(); 105 | 106 | responseData({ 107 | res, 108 | status: 200, 109 | message: MESSAGES.SUCCESS.CREATE, 110 | data: createdUser, 111 | }); 112 | } catch (error) { 113 | await trx.rollback(); 114 | next(error); 115 | } 116 | } 117 | 118 | export async function updateOneUserController( 119 | req: Request, 120 | res: Response, 121 | next: NextFunction 122 | ) { 123 | const trx: Knex.Transaction = await db.transaction(); 124 | try { 125 | const payload = { 126 | name: req.body.name, 127 | price: req.body.price, 128 | updated_by: req.body.user.id, 129 | }; 130 | const updatedUser = await updateOneUserService( 131 | { 132 | id: req.params.id, 133 | data: payload, 134 | }, 135 | trx 136 | ); 137 | 138 | await trx.commit(); 139 | 140 | responseData({ 141 | res, 142 | status: 200, 143 | message: MESSAGES.SUCCESS.UPDATE, 144 | data: updatedUser, 145 | }); 146 | } catch (error) { 147 | await trx.rollback(); 148 | next(error); 149 | } 150 | } 151 | 152 | export async function deleteOneUserController( 153 | req: Request, 154 | res: Response, 155 | next: NextFunction 156 | ) { 157 | const trx: Knex.Transaction = await db.transaction(); 158 | try { 159 | const deletedUser = await deleteOneUserService(req.params.id); 160 | 161 | await trx.commit(); 162 | 163 | responseData({ 164 | res, 165 | status: 200, 166 | message: MESSAGES.SUCCESS.DELETE, 167 | data: deletedUser, 168 | }); 169 | } catch (error) { 170 | await trx.rollback(); 171 | next(error); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/features/product/product.service.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from 'knex'; 2 | import db from '../../db/db'; 3 | import { getPaginatedData, getPagination } from '../../utils/common'; 4 | import { ListQuery } from '../../types/types'; 5 | 6 | export async function getAllProductsService(filters: ListQuery) { 7 | const query = db 8 | .table('product') 9 | .select( 10 | 'product.id', 11 | 'product.name', 12 | 'product.price', 13 | 'product.is_deleted', 14 | db.raw( 15 | `JSON_OBJECT('id', product_category.id, 'name', product_category.name) as category` 16 | ) 17 | ) 18 | .leftJoin('product_category', 'product_category.id', 'product.category_id'); 19 | const totalCountQuery = db.table('product').count('* as count'); 20 | 21 | let pagination; 22 | if (filters.page && filters.size) { 23 | pagination = getPagination({ 24 | page: filters.page as number, 25 | size: filters.size as number, 26 | }); 27 | query.limit(pagination.limit).offset(pagination.offset); 28 | } 29 | 30 | if (filters.sort) { 31 | query.orderBy(filters.sort, filters.order || 'asc'); 32 | } else { 33 | query.orderBy('product.created_at', 'desc'); 34 | } 35 | 36 | if (filters.keyword) { 37 | query.whereILike('product.name', `%${filters.keyword}%`); 38 | totalCountQuery.whereILike('product.name', `%${filters.keyword}%`); 39 | } 40 | 41 | return getPaginatedData(query, totalCountQuery, filters, pagination); 42 | } 43 | 44 | export async function getOneProductService(id: string | number) { 45 | const product = await db 46 | .table('product') 47 | .select('id', 'name', 'is_deleted') 48 | .where('id', id); 49 | return product[0] || null; 50 | } 51 | 52 | export async function createOneProductService( 53 | data: Record, 54 | trx?: Knex.Transaction 55 | ) { 56 | const query = db.table('product').insert(data); 57 | if (trx) query.transacting(trx); 58 | await query; 59 | 60 | return data; 61 | } 62 | 63 | export async function createManyProductsService( 64 | data: Record[], 65 | trx?: Knex.Transaction 66 | ) { 67 | const query = db.table('product').insert(data); 68 | if (trx) query.transacting(trx); 69 | await query; 70 | 71 | return data; 72 | } 73 | 74 | export async function updateOneProductService( 75 | { 76 | id, 77 | data, 78 | }: { 79 | id: string | number; 80 | data: Record; 81 | }, 82 | trx?: Knex.Transaction 83 | ) { 84 | const query = db.table('product').update(data).where('id', id); 85 | 86 | if (trx) query.transacting(trx); 87 | 88 | return query; 89 | } 90 | 91 | export async function deleteOneProductService( 92 | id: string | number, 93 | trx?: Knex.Transaction 94 | ) { 95 | const toDelete = await db.table('product').select('*').where('id', id); 96 | 97 | const query = db.table('product').where('id', id).del(); 98 | if (trx) query.transacting(trx); 99 | await query; 100 | 101 | return toDelete[0] || null; 102 | } 103 | 104 | export async function deleteManyProductsService( 105 | ids: string[], 106 | trx?: Knex.Transaction 107 | ) { 108 | const toDelete = await db.table('product').select('*').whereIn('id', ids); 109 | 110 | const query = db.table('product').whereIn('id', ids).del(); 111 | if (trx) query.transacting(trx); 112 | await query; 113 | 114 | return toDelete; 115 | } 116 | 117 | export async function softDeleteOneProductService( 118 | id: string | number, 119 | trx?: Knex.Transaction 120 | ) { 121 | const query = db 122 | .table('product') 123 | .update({ is_deleted: true }) 124 | .where('id', id); 125 | 126 | if (trx) query.transacting(trx); 127 | await query; 128 | 129 | const toDelete = await db.table('product').select('*').where('id', id); 130 | 131 | return toDelete[0] || null; 132 | } 133 | 134 | export async function softDeleteManyProductsService( 135 | ids: string[] | number[], 136 | trx?: Knex.Transaction 137 | ) { 138 | const query = db 139 | .table('product') 140 | .update({ is_deleted: true }) 141 | .whereIn('id', ids); 142 | if (trx) query.transacting(trx); 143 | await query; 144 | 145 | const toDelete = await db 146 | .table('product') 147 | .select('id', 'name', 'is_deleted') 148 | .whereIn('id', ids); 149 | 150 | return toDelete || null; 151 | } 152 | 153 | export async function getExistingProductService(data: Record) { 154 | const product = await db 155 | .table('product') 156 | .select('id', 'name', 'is_deleted') 157 | .where(data); 158 | return product[0] || null; 159 | } 160 | -------------------------------------------------------------------------------- /src/features/product-category/product-category.service.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from 'knex'; 2 | import db from '../../db/db'; 3 | import { getPaginatedData, getPagination } from '../../utils/common'; 4 | import { ListQuery } from '../../types/types'; 5 | 6 | export async function getAllProductCategoriesService(filters: ListQuery) { 7 | const query = db 8 | .table('product_category') 9 | .select( 10 | 'product_category.id', 11 | 'product_category.name', 12 | 'product_category.is_deleted' 13 | ); 14 | const totalCountQuery = db.table('product_category').count('* as count'); 15 | 16 | let pagination; 17 | if (filters.page && filters.size) { 18 | pagination = getPagination({ 19 | page: filters.page as number, 20 | size: filters.size as number, 21 | }); 22 | query.limit(pagination.limit).offset(pagination.offset); 23 | } 24 | 25 | if (filters.sort) { 26 | query.orderBy(filters.sort, filters.order || 'asc'); 27 | } else { 28 | query.orderBy('product_category.created_at', 'desc'); 29 | } 30 | 31 | if (filters.keyword) { 32 | query.whereILike('product_category.name', `%${filters.keyword}%`); 33 | totalCountQuery.whereILike('product_category.name', `%${filters.keyword}%`); 34 | } 35 | 36 | return getPaginatedData(query, totalCountQuery, filters, pagination); 37 | } 38 | 39 | export async function getOneProductCategoryService(id: string | number) { 40 | const product_category = await db 41 | .table('product_category') 42 | .select('id', 'name', 'is_deleted') 43 | .where('id', id); 44 | return product_category[0] || null; 45 | } 46 | 47 | export async function createOneProductCategoryService( 48 | data: Record, 49 | trx?: Knex.Transaction 50 | ) { 51 | const query = db.table('product_category').insert(data); 52 | if (trx) query.transacting(trx); 53 | await query; 54 | 55 | return data; 56 | } 57 | 58 | export async function createManyProductCategoriesService( 59 | data: Record[], 60 | trx?: Knex.Transaction 61 | ) { 62 | const query = db.table('product_category').insert(data); 63 | if (trx) query.transacting(trx); 64 | await query; 65 | 66 | return data; 67 | } 68 | 69 | export async function updateOneProductCategoryService( 70 | { 71 | id, 72 | data, 73 | }: { 74 | id: string | number; 75 | data: Record; 76 | }, 77 | trx?: Knex.Transaction 78 | ) { 79 | const query = db.table('product_category').update(data).where('id', id); 80 | 81 | if (trx) query.transacting(trx); 82 | 83 | return query; 84 | } 85 | 86 | export async function deleteOneProductCategoryService( 87 | id: string | number, 88 | trx?: Knex.Transaction 89 | ) { 90 | const toDelete = await db 91 | .table('product_category') 92 | .select('id', 'name', 'is_deleted') 93 | .where('id', id); 94 | 95 | const query = db.table('product_category').where('id', id).del(); 96 | if (trx) query.transacting(trx); 97 | await query; 98 | 99 | return toDelete[0] || null; 100 | } 101 | 102 | export async function deleteManyProductCategoriesService( 103 | ids: string[], 104 | trx?: Knex.Transaction 105 | ) { 106 | const toDelete = await db 107 | .table('product_category') 108 | .select('*') 109 | .whereIn('id', ids); 110 | 111 | const query = db.table('product_category').whereIn('id', ids).del(); 112 | if (trx) query.transacting(trx); 113 | await query; 114 | 115 | return toDelete; 116 | } 117 | 118 | export async function softDeleteOneProductCategoryService( 119 | id: string | number, 120 | trx?: Knex.Transaction 121 | ) { 122 | const query = db 123 | .table('product_category') 124 | .update({ is_deleted: true }) 125 | .where('id', id); 126 | 127 | if (trx) query.transacting(trx); 128 | await query; 129 | 130 | const toDelete = await db 131 | .table('product_category') 132 | .select('id', 'name', 'is_deleted') 133 | .where('id', id); 134 | 135 | return toDelete[0] || null; 136 | } 137 | 138 | export async function softDeleteManyProductCategoriesService( 139 | ids: string[] | number[], 140 | trx?: Knex.Transaction 141 | ) { 142 | const query = db 143 | .table('product_category') 144 | .update({ is_deleted: true }) 145 | .whereIn('id', ids); 146 | if (trx) query.transacting(trx); 147 | await query; 148 | 149 | const toDelete = await db 150 | .table('product_category') 151 | .select('id', 'name', 'is_deleted') 152 | .whereIn('id', ids); 153 | 154 | return toDelete || null; 155 | } 156 | 157 | export async function getExistingProductCategoryService( 158 | data: Record 159 | ) { 160 | const product_category = await db 161 | .table('product_category') 162 | .select('id', 'name', 'is_deleted') 163 | .where(data); 164 | return product_category[0] || null; 165 | } 166 | -------------------------------------------------------------------------------- /src/features/cached-product-category/cached-product-category.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import validator from './cached-product-category.validator'; 3 | import { validateRequest } from '../../middlewares/validation'; 4 | import { 5 | createManyCachedProductCategoriesController, 6 | createOneCachedProductCategoryController, 7 | deleteManyCachedProductCategoriesController, 8 | deleteOneCachedProductCategoryController, 9 | getAllCachedProductCategoriesController, 10 | getOneCachedProductCategoryController, 11 | softDeleteManyCachedProductCategoriesController, 12 | softDeleteOneCachedProductCategoryController, 13 | updateOneCachedProductCategoryController, 14 | clearProductCategoryCacheController, 15 | getProductCategoryCacheStatsController, 16 | } from './cached-product-category.controller'; 17 | import verifyRBAC from '../../middlewares/rbac'; 18 | import { ACTIONS, MODULES, ROLES, SUB_MODULES } from '../../configs/rbac'; 19 | 20 | const cachedProductCategoryRoutes = Router(); 21 | 22 | // Main CRUD routes 23 | cachedProductCategoryRoutes.get( 24 | '/cached-product-categories', 25 | verifyRBAC({ 26 | action: ACTIONS.VIEW, 27 | roles: [ROLES.ADMIN], 28 | module: MODULES.PRODUCT, 29 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 30 | }), 31 | validateRequest(validator.select), 32 | getAllCachedProductCategoriesController 33 | ); 34 | 35 | cachedProductCategoryRoutes.get( 36 | '/cached-product-categories/:id', 37 | verifyRBAC({ 38 | action: ACTIONS.VIEW, 39 | roles: [ROLES.ADMIN], 40 | module: MODULES.PRODUCT, 41 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 42 | }), 43 | validateRequest(validator.detail), 44 | getOneCachedProductCategoryController 45 | ); 46 | 47 | cachedProductCategoryRoutes.post( 48 | '/cached-product-categories', 49 | verifyRBAC({ 50 | action: ACTIONS.CREATE, 51 | roles: [ROLES.ADMIN], 52 | module: MODULES.PRODUCT, 53 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 54 | }), 55 | validateRequest(validator.create), 56 | createOneCachedProductCategoryController 57 | ); 58 | 59 | cachedProductCategoryRoutes.post( 60 | '/cached-product-categories/create-multi', 61 | verifyRBAC({ 62 | action: ACTIONS.CREATE, 63 | roles: [ROLES.ADMIN], 64 | module: MODULES.PRODUCT, 65 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 66 | }), 67 | validateRequest(validator.createMany), 68 | createManyCachedProductCategoriesController 69 | ); 70 | 71 | cachedProductCategoryRoutes.patch( 72 | '/cached-product-categories/:id', 73 | verifyRBAC({ 74 | action: ACTIONS.UPDATE, 75 | roles: [ROLES.ADMIN], 76 | module: MODULES.PRODUCT, 77 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 78 | }), 79 | validateRequest(validator.update), 80 | updateOneCachedProductCategoryController 81 | ); 82 | 83 | cachedProductCategoryRoutes.delete( 84 | '/cached-product-categories/:id', 85 | verifyRBAC({ 86 | action: ACTIONS.DELETE, 87 | roles: [ROLES.ADMIN], 88 | module: MODULES.PRODUCT, 89 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 90 | }), 91 | validateRequest(validator.delete), 92 | deleteOneCachedProductCategoryController 93 | ); 94 | 95 | cachedProductCategoryRoutes.post( 96 | '/cached-product-categories/delete-multi', 97 | verifyRBAC({ 98 | action: ACTIONS.DELETE, 99 | roles: [ROLES.ADMIN], 100 | module: MODULES.PRODUCT, 101 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 102 | }), 103 | validateRequest(validator.deleteMany), 104 | deleteManyCachedProductCategoriesController 105 | ); 106 | 107 | cachedProductCategoryRoutes.delete( 108 | '/cached-product-categories/soft-delete/:id', 109 | verifyRBAC({ 110 | action: ACTIONS.DELETE, 111 | roles: [ROLES.ADMIN], 112 | module: MODULES.PRODUCT, 113 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 114 | }), 115 | validateRequest(validator.delete), 116 | softDeleteOneCachedProductCategoryController 117 | ); 118 | 119 | cachedProductCategoryRoutes.post( 120 | '/cached-product-categories/soft-delete-multi', 121 | verifyRBAC({ 122 | action: ACTIONS.DELETE, 123 | roles: [ROLES.ADMIN], 124 | module: MODULES.PRODUCT, 125 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 126 | }), 127 | validateRequest(validator.deleteMany), 128 | softDeleteManyCachedProductCategoriesController 129 | ); 130 | 131 | // Cache management routes 132 | cachedProductCategoryRoutes.delete( 133 | '/cached-product-categories/cache/clear', 134 | verifyRBAC({ 135 | action: ACTIONS.DELETE, 136 | roles: [ROLES.ADMIN], 137 | module: MODULES.PRODUCT, 138 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 139 | }), 140 | clearProductCategoryCacheController 141 | ); 142 | 143 | cachedProductCategoryRoutes.get( 144 | '/cached-product-categories/cache/stats', 145 | verifyRBAC({ 146 | action: ACTIONS.VIEW, 147 | roles: [ROLES.ADMIN], 148 | module: MODULES.PRODUCT, 149 | subModule: SUB_MODULES.PRODUCT_CATEGORY, 150 | }), 151 | getProductCategoryCacheStatsController 152 | ); 153 | 154 | export default cachedProductCategoryRoutes; 155 | -------------------------------------------------------------------------------- /src/features/auth/README.md: -------------------------------------------------------------------------------- 1 | # Authentication API Documentation 2 | 3 | This document provides comprehensive API documentation for the Authentication service, including user login, token refresh, and permission management. 4 | 5 | ## Table of Contents 6 | 7 | 1. [Authentication](#authentication) 8 | 2. [User Login](#user-login) 9 | 3. [Token Refresh](#token-refresh) 10 | 4. [Error Handling](#error-handling) 11 | 5. [Security](#security) 12 | 13 | ## Authentication 14 | 15 | All endpoints require JWT authentication except for the login endpoint. Include the token in the Authorization header: 16 | 17 | ``` 18 | Authorization: Bearer 19 | ``` 20 | 21 | ## User Login 22 | 23 | ### Login User 24 | 25 | **POST** `/api/auth/login` 26 | 27 | Authenticates a user and returns access token, refresh token, and user permissions. 28 | 29 | **Request Body:** 30 | 31 | ```json 32 | { 33 | "username": "john_doe", 34 | "password": "securepassword123" 35 | } 36 | ``` 37 | 38 | **Response:** 39 | 40 | ```json 41 | { 42 | "status": 200, 43 | "message": "Login successful", 44 | "data": { 45 | "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", 46 | "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", 47 | "id": "550e8400-e29b-41d4-a716-446655440000", 48 | "username": "john_doe", 49 | "first_name": "John", 50 | "last_name": "Doe", 51 | "email": "john@example.com", 52 | "phone1": "+1234567890", 53 | "phone2": "+1234567891", 54 | "phone3": "+1234567892", 55 | "address1": "123 Main St", 56 | "address2": "Apt 4B", 57 | "img": "uploads/user-images/john_doe.jpg", 58 | "role_id": "role-uuid-here", 59 | "permissions": [ 60 | { 61 | "action": "VIEW", 62 | "module": "USER_MANAGEMENT", 63 | "sub_module": "USER" 64 | }, 65 | { 66 | "action": "CREATE", 67 | "module": "PRODUCT", 68 | "sub_module": "PRODUCT" 69 | } 70 | ] 71 | } 72 | } 73 | ``` 74 | 75 | **Error Responses:** 76 | 77 | ```json 78 | { 79 | "status": 400, 80 | "message": "User not found", 81 | "data": null 82 | } 83 | ``` 84 | 85 | ```json 86 | { 87 | "status": 400, 88 | "message": "Invalid credentials", 89 | "data": null 90 | } 91 | ``` 92 | 93 | ## Token Refresh 94 | 95 | ### Refresh Access Token 96 | 97 | **POST** `/api/auth/refresh-token` 98 | 99 | Refreshes the access token using a valid refresh token. 100 | 101 | **Headers:** 102 | 103 | ``` 104 | Authorization: Bearer 105 | ``` 106 | 107 | **Response:** 108 | 109 | ```json 110 | { 111 | "status": 200, 112 | "message": "Created successfully", 113 | "data": { 114 | "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", 115 | "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." 116 | } 117 | } 118 | ``` 119 | 120 | **Error Response:** 121 | 122 | ```json 123 | { 124 | "status": 400, 125 | "message": "User not found", 126 | "data": null 127 | } 128 | ``` 129 | 130 | ## Error Handling 131 | 132 | All endpoints return consistent error responses: 133 | 134 | ```json 135 | { 136 | "status": 400, 137 | "message": "Error description", 138 | "data": null 139 | } 140 | ``` 141 | 142 | Common HTTP status codes: 143 | 144 | - `200` - Success 145 | - `400` - Bad Request (validation errors, invalid credentials) 146 | - `401` - Unauthorized (missing or invalid token) 147 | - `500` - Internal Server Error 148 | 149 | ## Security 150 | 151 | - Passwords are hashed using bcrypt before storage 152 | - JWT tokens are used for authentication 153 | - Refresh tokens are used for token renewal 154 | - Input validation is performed on all endpoints 155 | - User permissions are returned with login response for RBAC implementation 156 | 157 | ## Token Management 158 | 159 | ### Access Token 160 | 161 | - Short-lived token for API access 162 | - Contains user information and permissions 163 | - Used for all authenticated API calls 164 | 165 | ### Refresh Token 166 | 167 | - Long-lived token for token renewal 168 | - Used to obtain new access tokens 169 | - Should be stored securely 170 | 171 | ## Permission System 172 | 173 | The authentication system integrates with the RBAC (Role-Based Access Control) system: 174 | 175 | - User permissions are fetched based on their role 176 | - Permissions include action, module, and sub-module information 177 | - Used by the RBAC middleware to control access to protected endpoints 178 | 179 | ## Testing 180 | 181 | ### Login Testing 182 | 183 | 1. **Valid Credentials:** 184 | 185 | ```bash 186 | curl -X POST http://localhost:3000/api/auth/login \ 187 | -H "Content-Type: application/json" \ 188 | -d '{"username": "admin", "password": "admin123"}' 189 | ``` 190 | 191 | 2. **Invalid Credentials:** 192 | ```bash 193 | curl -X POST http://localhost:3000/api/auth/login \ 194 | -H "Content-Type: application/json" \ 195 | -d '{"username": "admin", "password": "wrongpassword"}' 196 | ``` 197 | 198 | ### Token Refresh Testing 199 | 200 | ```bash 201 | curl -X POST http://localhost:3000/api/auth/refresh-token \ 202 | -H "Authorization: Bearer " 203 | ``` 204 | 205 | ## Support 206 | 207 | For technical support or questions about the Authentication API, please contact the development team. 208 | -------------------------------------------------------------------------------- /src/features/rbac/sub-module/sub-module.service.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from 'knex'; 2 | import db from '../../../db/db'; 3 | import { getPaginatedData, getPagination } from '../../../utils/common'; 4 | import { ListQuery } from '../../../types/types'; 5 | 6 | export async function getAllSubModulesService( 7 | filters: ListQuery & { 8 | channel_id: string; 9 | module_id: string; 10 | } 11 | ) { 12 | const query = db 13 | .table('sub_module') 14 | .select( 15 | 'sub_module.id', 16 | 'sub_module.name', 17 | 'sub_module.is_deleted', 18 | db.raw(`JSON_OBJECT('id', channel.id, 'name', channel.name) as channel`), 19 | db.raw(`JSON_OBJECT('id', module.id, 'name', module.name) as module`) 20 | ) 21 | .leftJoin('channel', 'channel.id', 'sub_module.channel_id') 22 | .leftJoin('module', 'module.id', 'sub_module.module_id') 23 | .where('sub_module.is_deleted', 0); 24 | 25 | const totalCountQuery = db.table('sub_module').count('* as count'); 26 | 27 | let pagination; 28 | if (filters.page && filters.size) { 29 | pagination = getPagination({ 30 | page: filters.page as number, 31 | size: filters.size as number, 32 | }); 33 | query.limit(pagination.limit).offset(pagination.offset); 34 | } 35 | 36 | if (filters.sort) { 37 | query.orderBy(filters.sort, filters.order || 'asc'); 38 | } else { 39 | query.orderBy('sub_module.created_at', 'desc'); 40 | } 41 | 42 | if (filters.keyword) { 43 | query.whereILike('sub_module.name', `%${filters.keyword}%`); 44 | totalCountQuery.whereILike('sub_module.name', `%${filters.keyword}%`); 45 | } 46 | 47 | if (filters.channel_id) { 48 | query.whereILike('sub_module.channel_id', `${filters.channel_id}`); 49 | totalCountQuery.whereILike( 50 | 'sub_module.channel_id', 51 | `${filters.channel_id}` 52 | ); 53 | } 54 | if (filters.module_id) { 55 | query.whereILike('sub_module.module_id', `${filters.module_id}`); 56 | totalCountQuery.whereILike('sub_module.module_id', `${filters.module_id}`); 57 | } 58 | 59 | return getPaginatedData(query, totalCountQuery, filters, pagination); 60 | } 61 | 62 | export async function getOneSubModuleService(id: string | number) { 63 | const subModule = await db 64 | .table('sub_module') 65 | .select('id', 'name', 'is_deleted') 66 | .where('id', id); 67 | return subModule[0] || null; 68 | } 69 | 70 | export async function createOneSubModuleService( 71 | data: Record, 72 | trx?: Knex.Transaction 73 | ) { 74 | const query = db.table('sub_module').insert(data); 75 | if (trx) query.transacting(trx); 76 | await query; 77 | 78 | return data; 79 | } 80 | 81 | export async function createManySubModulesService( 82 | data: Record[], 83 | trx?: Knex.Transaction 84 | ) { 85 | const query = db.table('sub_module').insert(data); 86 | if (trx) query.transacting(trx); 87 | await query; 88 | 89 | return data; 90 | } 91 | 92 | export async function updateOneSubModuleService( 93 | { 94 | id, 95 | data, 96 | }: { 97 | id: string | number; 98 | data: Record; 99 | }, 100 | trx?: Knex.Transaction 101 | ) { 102 | const query = db.table('sub_module').update(data).where('id', id); 103 | 104 | if (trx) query.transacting(trx); 105 | 106 | return query; 107 | } 108 | 109 | export async function deleteSubModuleService( 110 | id: string | number, 111 | trx?: Knex.Transaction 112 | ) { 113 | const toDelete = await db.table('sub_module').select('*').where('id', id); 114 | 115 | const query = db.table('sub_module').where('id', id).del(); 116 | if (trx) query.transacting(trx); 117 | await query; 118 | 119 | return toDelete[0] || null; 120 | } 121 | 122 | export async function deleteManySubModulesService( 123 | ids: string[], 124 | trx?: Knex.Transaction 125 | ) { 126 | const toDelete = await db.table('sub_module').select('*').whereIn('id', ids); 127 | 128 | const query = db.table('sub_module').whereIn('id', ids).del(); 129 | if (trx) query.transacting(trx); 130 | await query; 131 | 132 | return toDelete || null; 133 | } 134 | 135 | export async function softDeleteOneSubModuleService( 136 | id: string | number, 137 | trx?: Knex.Transaction 138 | ) { 139 | const toDelete = await db.table('sub_module').select('*').where('id', id); 140 | 141 | const query = db 142 | .table('sub_module') 143 | .update({ is_deleted: true }) 144 | .where('id', id); 145 | if (trx) query.transacting(trx); 146 | await query; 147 | 148 | return toDelete[0] || null; 149 | } 150 | 151 | export async function softDeleteManySubModulesService( 152 | ids: string[] | number[], 153 | trx?: Knex.Transaction 154 | ) { 155 | const toDelete = await db.table('sub_module').select('*').whereIn('id', ids); 156 | 157 | const query = db 158 | .table('sub_module') 159 | .update({ is_deleted: true }) 160 | .whereIn('id', ids); 161 | if (trx) query.transacting(trx); 162 | await query; 163 | 164 | return toDelete || null; 165 | } 166 | 167 | export async function getExistingSubModuleService( 168 | data: Record 169 | ) { 170 | const subModule = await db 171 | .table('sub_module') 172 | .select('id', 'name', 'is_deleted') 173 | .where(data); 174 | return subModule[0] || null; 175 | } 176 | -------------------------------------------------------------------------------- /src/middlewares/README.md: -------------------------------------------------------------------------------- 1 | # Middleware Documentation 2 | 3 | This directory contains all the middleware used in the RBAC Express.js application. 4 | 5 | ## HPP (HTTP Parameter Pollution) Middleware 6 | 7 | ### Overview 8 | 9 | The HPP middleware prevents HTTP Parameter Pollution attacks by ensuring only the first occurrence of a parameter is used in requests. 10 | 11 | ### What is HTTP Parameter Pollution? 12 | 13 | HTTP Parameter Pollution occurs when an attacker sends multiple values for the same parameter, which can lead to unexpected behavior in the application. 14 | 15 | **Example Attack:** 16 | 17 | ``` 18 | GET /api/users?user=john&user=admin 19 | ``` 20 | 21 | **Without HPP:** 22 | 23 | ```javascript 24 | req.query.user = ['john', 'admin']; // Array with multiple values 25 | ``` 26 | 27 | **With HPP:** 28 | 29 | ```javascript 30 | req.query.user = 'john'; // Only first value is used 31 | ``` 32 | 33 | ### Configuration 34 | 35 | The HPP middleware is configured in `src/middlewares/hpp-config.ts` with the following settings: 36 | 37 | #### Whitelisted Parameters 38 | 39 | The following parameters are whitelisted and allowed to have multiple values: 40 | 41 | **Query Parameters:** 42 | 43 | - `category_id` - Product category filtering 44 | - `sortBy` - Sorting field 45 | - `sortOrder` - Sort direction 46 | - `ids` - Bulk operations 47 | 48 | **Body Parameters:** 49 | 50 | - `productCategories` - Bulk category operations 51 | - `products` - Bulk product operations 52 | - `roles` - Bulk role operations 53 | - `actions` - Bulk action operations 54 | - `modules` - Bulk module operations 55 | - `subModules` - Bulk sub-module operations 56 | - `permissions` - Bulk permission operations 57 | - `channels` - Bulk channel operations 58 | 59 | #### Configuration Options 60 | 61 | ```typescript 62 | const hppOptions = { 63 | whitelist, // Query parameters that can have multiple values 64 | checkQuery: true, // Check query string parameters 65 | checkBody: true, // Check body parameters 66 | checkBodyOnlyForContentTypes: ['application/x-www-form-urlencoded', 'application/json'], 67 | whitelistBody: [...] // Body parameters that can have multiple values 68 | }; 69 | ``` 70 | 71 | ### Usage 72 | 73 | The HPP middleware is automatically applied to all routes in `src/app.ts`: 74 | 75 | ```typescript 76 | import hppMiddleware from './middlewares/hpp-config'; 77 | 78 | // Apply HPP middleware after helmet and before CORS 79 | app.use(helmet()); 80 | app.use(hppMiddleware); 81 | app.use(cors()); 82 | ``` 83 | 84 | ### Security Benefits 85 | 86 | 1. **Prevents Parameter Pollution Attacks**: Stops attackers from manipulating parameter values 87 | 2. **Consistent Parameter Handling**: Ensures predictable parameter behavior 88 | 3. **Reduces Attack Surface**: Minimizes potential security vulnerabilities 89 | 4. **Maintains Functionality**: Whitelist allows legitimate multi-value parameters 90 | 91 | ### Testing 92 | 93 | You can test the HPP middleware with the following examples: 94 | 95 | #### Test Parameter Pollution Prevention 96 | 97 | ```bash 98 | # This should only use the first 'user' parameter 99 | curl "http://localhost:3000/api/users?user=john&user=admin" 100 | ``` 101 | 102 | #### Test Whitelisted Parameters 103 | 104 | ```bash 105 | # These parameters are allowed to have multiple values 106 | curl "http://localhost:3000/api/products?category_id=1&category_id=2" 107 | ``` 108 | 109 | #### Test Bulk Operations 110 | 111 | ```bash 112 | # Body parameters for bulk operations are whitelisted 113 | curl -X POST "http://localhost:3000/api/products/create-many" \ 114 | -H "Content-Type: application/json" \ 115 | -d '{ 116 | "products": [ 117 | {"name": "Product 1", "price": 10}, 118 | {"name": "Product 2", "price": 20} 119 | ] 120 | }' 121 | ``` 122 | 123 | ### Customization 124 | 125 | To modify the HPP configuration: 126 | 127 | 1. Edit `src/middlewares/hpp-config.ts` 128 | 2. Add or remove parameters from the `whitelist` array 129 | 3. Modify the `hppOptions` configuration 130 | 4. Restart the application 131 | 132 | ### Best Practices 133 | 134 | 1. **Whitelist Only Necessary Parameters**: Only whitelist parameters that legitimately need multiple values 135 | 2. **Regular Review**: Periodically review the whitelist to ensure it's still necessary 136 | 3. **Documentation**: Keep the whitelist documented for team awareness 137 | 4. **Testing**: Test both whitelisted and non-whitelisted parameters 138 | 139 | ### Troubleshooting 140 | 141 | If you encounter issues with the HPP middleware: 142 | 143 | 1. **Check Whitelist**: Ensure the parameter is in the whitelist if it needs multiple values 144 | 2. **Content Type**: Verify the request content type is supported 145 | 3. **Middleware Order**: Ensure HPP is applied before route handlers 146 | 4. **Logs**: Check application logs for HPP-related errors 147 | 148 | ## Other Middleware 149 | 150 | ### Error Handler 151 | 152 | - `error-handler.ts` - Centralized error handling middleware 153 | 154 | ### Authentication 155 | 156 | - `jwt.ts` - JWT token verification middleware 157 | 158 | ### RBAC 159 | 160 | - `rbac.ts` - Role-based access control middleware 161 | 162 | ### Validation 163 | 164 | - `validation.ts` - Request validation middleware 165 | 166 | ### File Upload 167 | 168 | - `multer-upload.ts` - File upload handling middleware 169 | 170 | ### Audit Logging 171 | 172 | - `audit-log.ts` - Request audit logging middleware 173 | -------------------------------------------------------------------------------- /seeds/05_permissions.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from 'knex'; 2 | 3 | export async function seed(knex: Knex): Promise { 4 | // Delete existing data 5 | await knex('permission').del(); 6 | 7 | // Constants 8 | const webChannelId = '6682d258-05e0-11f0-9bc1-32adce0096f0'; 9 | const adminRoleId = '91e945da-0a45-11f0-9bc1-32adce0096f0'; 10 | const superAdminRoleId = 'bfbdd16a-05e0-11f0-9bc1-32adce0096f0'; 11 | 12 | // Action IDs 13 | const createActionId = '242b2ed2-0757-11f0-9bc1-32adce0096f0'; 14 | const deleteActionId = '242b6262-0757-11f0-9bc1-32adce0096f0'; 15 | const viewActionId = '9e50ed1a-075b-11f0-9bc1-32adce0096f0'; 16 | const updateActionId = 'a18f576e-075b-11f0-9bc1-32adce0096f0'; 17 | 18 | // Module IDs 19 | const userManagementModuleId = '2059b11a-05dc-11f0-9bc1-32adce0096f0'; 20 | const productModuleId = '83732965-a8aa-44a4-b1d5-30b2ef267d2a'; 21 | 22 | // Sub-module IDs 23 | const userSubModuleId = '78a5a376-1e8f-11f0-b5b5-df40a1682685'; 24 | const userRoleAssignSubModuleId = '4ddd5b28-05e6-11f0-9bc1-32adce0096f0'; 25 | const productSubModuleId = '86b59139-2c35-440e-9c1a-5004b2ff3996'; 26 | const productCategorySubModuleId = '1976b4cc-f6c6-4529-b628-6cd03c8c2616'; 27 | 28 | // Permissions for Admin role - User Management Module 29 | const adminPermissions = [ 30 | // User sub-module permissions 31 | { 32 | module_id: userManagementModuleId, 33 | sub_module_id: userSubModuleId, 34 | channel_id: webChannelId, 35 | role_id: adminRoleId, 36 | action_id: viewActionId, 37 | is_deleted: false, 38 | }, 39 | { 40 | module_id: userManagementModuleId, 41 | sub_module_id: userSubModuleId, 42 | channel_id: webChannelId, 43 | role_id: adminRoleId, 44 | action_id: createActionId, 45 | is_deleted: false, 46 | }, 47 | { 48 | module_id: userManagementModuleId, 49 | sub_module_id: userSubModuleId, 50 | channel_id: webChannelId, 51 | role_id: adminRoleId, 52 | action_id: updateActionId, 53 | is_deleted: false, 54 | }, 55 | { 56 | module_id: userManagementModuleId, 57 | sub_module_id: userSubModuleId, 58 | channel_id: webChannelId, 59 | role_id: adminRoleId, 60 | action_id: deleteActionId, 61 | is_deleted: false, 62 | }, 63 | // User Role Assign sub-module permissions 64 | { 65 | module_id: userManagementModuleId, 66 | sub_module_id: userRoleAssignSubModuleId, 67 | channel_id: webChannelId, 68 | role_id: adminRoleId, 69 | action_id: viewActionId, 70 | is_deleted: false, 71 | }, 72 | { 73 | module_id: userManagementModuleId, 74 | sub_module_id: userRoleAssignSubModuleId, 75 | channel_id: webChannelId, 76 | role_id: adminRoleId, 77 | action_id: createActionId, 78 | is_deleted: false, 79 | }, 80 | { 81 | module_id: userManagementModuleId, 82 | sub_module_id: userRoleAssignSubModuleId, 83 | channel_id: webChannelId, 84 | role_id: adminRoleId, 85 | action_id: updateActionId, 86 | is_deleted: false, 87 | }, 88 | { 89 | module_id: userManagementModuleId, 90 | sub_module_id: userRoleAssignSubModuleId, 91 | channel_id: webChannelId, 92 | role_id: adminRoleId, 93 | action_id: deleteActionId, 94 | is_deleted: false, 95 | }, 96 | // Product sub-module permissions 97 | { 98 | module_id: productModuleId, 99 | sub_module_id: productSubModuleId, 100 | channel_id: webChannelId, 101 | role_id: adminRoleId, 102 | action_id: viewActionId, 103 | is_deleted: false, 104 | }, 105 | { 106 | module_id: productModuleId, 107 | sub_module_id: productSubModuleId, 108 | channel_id: webChannelId, 109 | role_id: adminRoleId, 110 | action_id: createActionId, 111 | is_deleted: false, 112 | }, 113 | { 114 | module_id: productModuleId, 115 | sub_module_id: productSubModuleId, 116 | channel_id: webChannelId, 117 | role_id: adminRoleId, 118 | action_id: updateActionId, 119 | is_deleted: false, 120 | }, 121 | { 122 | module_id: productModuleId, 123 | sub_module_id: productSubModuleId, 124 | channel_id: webChannelId, 125 | role_id: adminRoleId, 126 | action_id: deleteActionId, 127 | is_deleted: false, 128 | }, 129 | // Product Category sub-module permissions 130 | { 131 | module_id: productModuleId, 132 | sub_module_id: productCategorySubModuleId, 133 | channel_id: webChannelId, 134 | role_id: adminRoleId, 135 | action_id: viewActionId, 136 | is_deleted: false, 137 | }, 138 | { 139 | module_id: productModuleId, 140 | sub_module_id: productCategorySubModuleId, 141 | channel_id: webChannelId, 142 | role_id: adminRoleId, 143 | action_id: createActionId, 144 | is_deleted: false, 145 | }, 146 | { 147 | module_id: productModuleId, 148 | sub_module_id: productCategorySubModuleId, 149 | channel_id: webChannelId, 150 | role_id: adminRoleId, 151 | action_id: updateActionId, 152 | is_deleted: false, 153 | }, 154 | { 155 | module_id: productModuleId, 156 | sub_module_id: productCategorySubModuleId, 157 | channel_id: webChannelId, 158 | role_id: adminRoleId, 159 | action_id: deleteActionId, 160 | is_deleted: false, 161 | }, 162 | ]; 163 | 164 | // Insert permissions 165 | await knex('permission').insert(adminPermissions); 166 | 167 | console.log('✅ Seeded permissions'); 168 | } 169 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import app from './app'; 2 | import dotenv from 'dotenv'; 3 | import db from './db/db'; 4 | import redisClient from './services/redis'; 5 | import { 6 | worker, 7 | emailQueue, 8 | connection as emailQueueConnection, 9 | } from './queues/email-queue'; 10 | import { 11 | startCronJobs, 12 | stopCronJobs, 13 | getCronTask, 14 | } from './cron-jobs/sample-cron'; 15 | import { Server } from 'http'; 16 | 17 | // Load environment variables from the .env file 18 | dotenv.config(); 19 | 20 | // Start cron jobs only in development mode (when running with npm run dev) 21 | // In production (npm start), NODE_ENV should be set to 'production' 22 | const isDevelopment = 23 | !process.env.NODE_ENV || process.env.NODE_ENV === 'development'; 24 | if (isDevelopment) { 25 | startCronJobs(); 26 | console.log('✅ Cron jobs enabled (development mode)'); 27 | } 28 | 29 | // Define the server port: use the PORT from environment variables or default to 3000 30 | const PORT = process.env.PORT || 3000; 31 | 32 | // Start the Express server and listen on the specified port 33 | const server: Server = app.listen(PORT, () => { 34 | // Log a message once the server is successfully running 35 | console.log(`🚀 Server running on port ${PORT}`); 36 | }); 37 | 38 | // Graceful shutdown configuration 39 | const SHUTDOWN_TIMEOUT = parseInt(process.env.SHUTDOWN_TIMEOUT || '10000', 10); // Default 10 seconds 40 | 41 | // Graceful shutdown function 42 | async function gracefulShutdown(signal: string): Promise { 43 | console.log(`\n${signal} received. Starting graceful shutdown...`); 44 | 45 | // Set a flag to prevent accepting new requests 46 | server.close(() => { 47 | console.log('✅ HTTP server closed'); 48 | }); 49 | 50 | // Create a promise that resolves when all cleanup is done 51 | const cleanupPromises: Promise[] = []; 52 | 53 | // Close BullMQ worker 54 | cleanupPromises.push( 55 | worker 56 | .close() 57 | .then(() => { 58 | console.log('✅ BullMQ worker closed'); 59 | }) 60 | .catch((err) => { 61 | console.error('❌ Error closing BullMQ worker:', err); 62 | }) 63 | ); 64 | 65 | // Close BullMQ queue 66 | cleanupPromises.push( 67 | emailQueue 68 | .close() 69 | .then(() => { 70 | console.log('✅ BullMQ queue closed'); 71 | }) 72 | .catch((err) => { 73 | console.error('❌ Error closing BullMQ queue:', err); 74 | }) 75 | ); 76 | 77 | // Close BullMQ Redis connection 78 | cleanupPromises.push( 79 | emailQueueConnection 80 | .quit() 81 | .then(() => { 82 | console.log('✅ BullMQ Redis connection closed'); 83 | }) 84 | .catch((err) => { 85 | console.error('❌ Error closing BullMQ Redis connection:', err); 86 | }) 87 | ); 88 | 89 | // Stop cron jobs (only if they were started) 90 | try { 91 | const cronTask = getCronTask(); 92 | if (cronTask) { 93 | stopCronJobs(); 94 | } 95 | } catch (err) { 96 | console.error('❌ Error stopping cron jobs:', err); 97 | } 98 | 99 | // Close Redis connection 100 | cleanupPromises.push( 101 | redisClient 102 | .quit() 103 | .then(() => { 104 | console.log('✅ Redis connection closed'); 105 | }) 106 | .catch((err) => { 107 | console.error('❌ Error closing Redis connection:', err); 108 | }) 109 | ); 110 | 111 | // Close database connection 112 | cleanupPromises.push( 113 | db 114 | .destroy() 115 | .then(() => { 116 | console.log('✅ Database connection closed'); 117 | }) 118 | .catch((err: any) => { 119 | // Ignore "aborted" errors during shutdown as they're expected when destroying the pool 120 | if (err?.message === 'aborted' || err?.code === 'ABORT_ERR') { 121 | console.log( 122 | '✅ Database connection pool aborted (expected during shutdown)' 123 | ); 124 | } else { 125 | console.error('❌ Error closing database connection:', err); 126 | } 127 | }) 128 | ); 129 | 130 | // Wait for all cleanup operations to complete 131 | try { 132 | await Promise.all(cleanupPromises); 133 | console.log('✅ Graceful shutdown completed'); 134 | process.exit(0); 135 | } catch (err) { 136 | console.error('❌ Error during graceful shutdown:', err); 137 | process.exit(1); 138 | } 139 | } 140 | 141 | // Set up timeout for forced shutdown 142 | let shutdownTimer: NodeJS.Timeout | null = null; 143 | 144 | function startShutdownTimer(): void { 145 | if (shutdownTimer) return; 146 | 147 | shutdownTimer = setTimeout(() => { 148 | console.error('⚠️ Shutdown timeout reached. Forcing exit...'); 149 | process.exit(1); 150 | }, SHUTDOWN_TIMEOUT); 151 | } 152 | 153 | // Handle termination signals 154 | process.on('SIGTERM', () => { 155 | startShutdownTimer(); 156 | gracefulShutdown('SIGTERM').catch((err) => { 157 | console.error('❌ Fatal error during shutdown:', err); 158 | process.exit(1); 159 | }); 160 | }); 161 | 162 | process.on('SIGINT', () => { 163 | startShutdownTimer(); 164 | gracefulShutdown('SIGINT').catch((err) => { 165 | console.error('❌ Fatal error during shutdown:', err); 166 | process.exit(1); 167 | }); 168 | }); 169 | 170 | // Handle uncaught exceptions and unhandled rejections 171 | process.on('uncaughtException', (err) => { 172 | console.error('❌ Uncaught Exception:', err); 173 | gracefulShutdown('uncaughtException').catch(() => { 174 | process.exit(1); 175 | }); 176 | }); 177 | 178 | process.on('unhandledRejection', (reason, promise) => { 179 | console.error('❌ Unhandled Rejection at:', promise, 'reason:', reason); 180 | gracefulShutdown('unhandledRejection').catch(() => { 181 | process.exit(1); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /src/docs/tech_docs.md: -------------------------------------------------------------------------------- 1 | # Technical Documentation 2 | 3 | Welcome to the **RBAC Express.js Starter Template** – a robust and scalable foundation for building secure RESTful APIs with **Role-Based Access Control (RBAC)**. 4 | 5 | This template is crafted with best practices in mind, using **Node.js**, **Express.js**, **TypeScript**, **MySQL**, and **Knex**, and is ideal for building admin panels, internal tools, or any application requiring fine-grained permission control. 6 | 7 | This documentation includes the following key areas: 8 | 9 | - [API Documentation](#api-documentation) 10 | - [ERD](#erd) 11 | - [Architecture](#architecture) 12 | - [Folder Structure](#folder-structure) 13 | - [RBAC Implementation](#rbac-implementation) 14 | - [Logging](#logging) 15 | - [Integrating With External Service APIs](#integrating-with-external-service-apis) 16 | 17 | --- 18 | 19 | ## API Documentation 20 | 21 | Postman collection is provided in `src/docs/rbac_express.postman_collection.json`. 22 | 23 | ## ERD 24 | 25 | 🔗 [View on dbdiagram.io](https://dbdiagram.io/d/680675261ca52373f5c46e4d) 26 | 27 | 🗃️ Get a SQL file named `rbac_express.sql` in `src/docs` folder. 28 | 29 | ![ERD](./erd.png) 30 | 31 | ## Architecture 32 | 33 | This project follows a **Feature-Based Architecture**, organizing code by business features rather than technical concerns (e.g., routes, controllers, models, etc.). You can find the folder structure in the [Folder Structure](#folder-structure) section. 34 | 35 | ### Why Feature-Based? 36 | 37 | #### 📈 **High Scalability** 38 | 39 | - Easy to scale and manage large codebases. 40 | - Teams can work on separate features independently without conflicts. 41 | 42 | #### 🛠️ **Better Maintainability** 43 | 44 | - Easier to locate, update, and test business logic per domain. 45 | - Simplifies bug tracking and debugging. 46 | 47 | #### 🧱 **Separation of Concerns** 48 | 49 | - Reduces coupling between unrelated parts of the codebase. 50 | 51 | #### 🚀 **Improved Developer Productivity** 52 | 53 | - Developers can focus on isolated features. 54 | - Easier onboarding for new developers. 55 | 56 | #### 🧩 **Modularity & Reusability** 57 | 58 | - Promotes reusable and encapsulated modules. 59 | - Easier to extract features into packages or microservices. 60 | 61 | ## Folder Structure 62 | 63 | ``` 64 | 📁 rbac-expressjs-starter 65 | ├── 📁 src 66 | │ ├── 📁 config 67 | │ ├── 📁 cron-jobs 68 | │ ├── 📁 docs 69 | │ ├── 📁 external-services 70 | │ ├── 📁 middlewares 71 | │ │ ├── 📝 audit-log.ts 72 | │ │ ├── 📝 error-handler.ts 73 | │ │ ├── 📝 jwt.ts 74 | │ │ ├── 📝 multer-upload.ts 75 | │ │ ├── 📝 rbac.ts 76 | │ │ ├── 📝 validation.ts 77 | │ ├── 📁 features 78 | │ │ ├── 📁 product 79 | │ │ │ ├── 📝 route.ts 80 | │ │ │ ├── 📝 controller.ts 81 | │ │ │ ├── 📝 service.ts 82 | │ │ │ ├── 📝 validator.ts 83 | │ │ ├── 📁 ... 84 | │ ├── 📁 storage 85 | │ │ ├── 📁 logs 86 | │ │ │ ├── 📝 audit.log 87 | │ │ ├── 📁 uploads 88 | │ ├── 📁 types 89 | │ ├── 📁 utils 90 | │ │ ├── 📝 common.ts 91 | │ │ ├── 📝 http.ts 92 | │ │ ├── 📝 log.ts 93 | │ │ ├── 📝 node-mailer.ts 94 | │ ├── 📝 app.ts 95 | │ ├── 📝 api-client.ts 96 | │ ├── 📝 routes.ts 97 | │ └── 📝 server.ts 98 | ├── 📝 .dockerignore 99 | ├── 📝 .env 100 | ├── 📝 .gitignore 101 | ├── 📝 .prettierrc.json 102 | ├── 📝 Dockerfile 103 | ├── 📝 eslint.config.cjs 104 | ├── 📝 nodemon.json 105 | ├── 📝 package.json 106 | ├── 📝 tsconfig.json 107 | └── 📝 README.md 108 | 109 | ``` 110 | 111 | ## RBAC Implementation 112 | 113 | This project implements **Role-Based Access Control (RBAC)** to ensure users only access what they are authorized for. 114 | 115 | RBAC is structured around **Roles**, **Modules**, **Sub-Modules**, **Actions**, and **Channels**, enabling fine-grained control across features. 116 | 117 | 🟢 On login, the user’s configured permissions are included in the response. 118 | 119 | ### 🔍 RBAC Middleware 120 | 121 | - Located in: `src/middlewares/rbac.ts` 122 | - Apply it to each protected route. 123 | 124 | ### ⚙️ RBAC Configs 125 | 126 | - Found in: `src/configs/rbac.ts` 127 | 128 | ### ✏️ Updating User Permissions 129 | 130 | To update user permissions, call the `/api/permissions` endpoint using the **PATCH** method with a predefined payload structure. 131 | 132 | > ⚠️ CRUD operations can be performed on roles, modules, sub-modules, channels, and actions – but remember to update the configurations accordingly afterward. 133 | 134 | ## Logging 135 | 136 | This project uses **two types of logging**: 137 | 138 | ### 1. 🛣️ Access Logging (via Morgan) 139 | 140 | - **Purpose:** Automatically records all incoming HTTP requests. 141 | - **Setup:** Standard Morgan. 142 | - **Output:** Console logs. 143 | 144 | ### 2. 🧾 Audit Logging (Custom) 145 | 146 | - **Purpose:** Tracks sensitive or critical actions like: 147 | - API calls 148 | - User logins 149 | - Data changes 150 | 151 | #### 📁 File Location 152 | 153 | - Stored in: `src/storage/logs/audit.log` 154 | 155 | #### 🧱 Format Definition 156 | 157 | - Defined in: `config/log-format.ts` 158 | 159 | #### ⚙️ How It Works 160 | 161 | - Custom middleware captures audit logs. 162 | - Use `logAudit` (from `utils/log.ts`) to manually log events. 163 | 164 | ## Integrating With External Service APIs 165 | 166 | - Uses a custom **Axios instance**: `apiClient` (in `src/api-client.ts`) 167 | - Already integrated with **audit logging**. 168 | - Ensures all external API interactions are traceable. 169 | 170 | ## 👨‍💻 Author 171 | 172 | **Sai Min Pyae Kyaw** 173 | 174 | 💼 Passionate Full Stack Developer | Node.js | TypeScript | React | MySQL 175 | 📍 Based in Myanmar 176 | 177 | ### 🌐 Connect with me 178 | 179 | - 💼 [LinkedIn](https://www.linkedin.com/in/sai-min-pyae-kyaw-369005200/) 180 | - 💻 [GitHub](https://github.com/MinPyaeKyaw) 181 | - 🌍 [Facebook](https://www.facebook.com/minpyae.kyaw.73) 182 | 183 | --- 184 | 185 | Made with ❤️ by Sai Min Pyae Kyaw 186 | -------------------------------------------------------------------------------- /src/features/rbac/permission/permission.service.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from 'knex'; 2 | import db from '../../../db/db'; 3 | import { getPaginatedData, getPagination } from '../../../utils/common'; 4 | import { ListQuery } from '../../../types/types'; 5 | 6 | export async function getRolesOnChannelDataService( 7 | filters: ListQuery & { role_id?: string; channel_id?: string } 8 | ) { 9 | const pagination = getPagination({ 10 | page: filters.page as number, 11 | size: filters.size as number, 12 | }); 13 | 14 | const query = db('permission') 15 | .select( 16 | 'permission.role_id', 17 | 'role.name as role', 18 | 'permission.channel_id', 19 | 'channel.name as channel' 20 | ) 21 | .leftJoin('channel', 'channel.id', 'permission.channel_id') 22 | .leftJoin('role', 'role.id', 'permission.role_id') 23 | .groupBy( 24 | 'permission.role_id', 25 | 'role.name', 26 | 'permission.channel_id', 27 | 'channel.name' 28 | ) 29 | .limit(pagination.limit) 30 | .offset(pagination.offset); 31 | const totalCountQuery = db.table('permission').count('* as count'); 32 | 33 | if (filters.channel_id) query.where('channel.id', '=', filters.channel_id); 34 | if (filters.role_id) query.where('role.id', '=', filters.role_id); 35 | 36 | return getPaginatedData(query, totalCountQuery, filters, pagination); 37 | } 38 | 39 | export async function getAllPermissionsService( 40 | filters: Record 41 | ) { 42 | const query = db 43 | .table('permission') 44 | .select( 45 | 'permission.id', 46 | 'permission.module_id', 47 | 'module.name as module', 48 | 'permission.sub_module_id', 49 | 'sub_module.name as sub_module', 50 | 'permission.role_id', 51 | 'role.name as role', 52 | 'permission.channel_id', 53 | 'channel.name as channel', 54 | db.raw(` 55 | JSON_ARRAYAGG( 56 | JSON_OBJECT('id', action.id, 'name', action.name) 57 | ) as actions 58 | `) 59 | ) 60 | .leftJoin('user', 'user.role_id', 'permission.role_id') 61 | .leftJoin('channel', 'channel.id', 'permission.channel_id') 62 | .leftJoin('module', 'module.id', 'permission.module_id') 63 | .leftJoin('sub_module', 'sub_module.id', 'permission.sub_module_id') 64 | .leftJoin('role', 'role.id', 'permission.role_id') 65 | .leftJoin('action', 'action.id', 'permission.action_id') 66 | .groupBy( 67 | 'permission.id', 68 | 'permission.module_id', 69 | 'module.name', 70 | 'permission.sub_module_id', 71 | 'sub_module.name', 72 | 'permission.role_id', 73 | 'role.name', 74 | 'permission.channel_id', 75 | 'channel.name' 76 | ); 77 | 78 | if (filters.user_id) query.where('user.id', '=', filters.user_id); 79 | if (filters.rold_id) query.where('role.id', '=', filters.rold_id); 80 | 81 | return query; 82 | } 83 | 84 | export async function getPermissionsByUserService(userId: string) { 85 | const permissions = await db 86 | .table('permission') 87 | .select( 88 | 'permission.module_id', 89 | 'module.name as module', 90 | 'permission.sub_module_id', 91 | 'sub_module.name as sub_module', 92 | 'permission.role_id', 93 | 'role.name as role', 94 | 'permission.channel_id', 95 | 'channel.name as channel', 96 | db.raw(` 97 | JSON_ARRAYAGG( 98 | JSON_OBJECT('id', action.id, 'name', action.name) 99 | ) as actions 100 | `) 101 | ) 102 | .leftJoin('user', 'user.role_id', 'permission.role_id') 103 | .leftJoin('channel', 'channel.id', 'permission.channel_id') 104 | .leftJoin('module', 'module.id', 'permission.module_id') 105 | .leftJoin('sub_module', 'sub_module.id', 'permission.sub_module_id') 106 | .leftJoin('role', 'role.id', 'permission.role_id') 107 | .leftJoin('action', 'action.id', 'permission.action_id') 108 | .where('user.id', '=', userId) 109 | .groupBy( 110 | 'permission.module_id', 111 | 'module.name', 112 | 'permission.sub_module_id', 113 | 'sub_module.name', 114 | 'permission.role_id', 115 | 'role.name', 116 | 'permission.channel_id', 117 | 'channel.name' 118 | ); 119 | 120 | return permissions; 121 | } 122 | 123 | export async function getOnePermissionService(id: string | number) { 124 | const permission = await db 125 | .table('permission') 126 | .select('id', 'name', 'is_deleted') 127 | .where('id', id); 128 | return permission[0] || null; 129 | } 130 | 131 | export async function createOnePermissionService( 132 | data: Record, 133 | trx?: Knex.Transaction 134 | ) { 135 | const query = db.table('permission').insert(data); 136 | 137 | if (trx) query.transacting(trx); 138 | 139 | return query; 140 | } 141 | 142 | export async function createManyPermissionsService( 143 | data: Record[], 144 | trx?: Knex.Transaction 145 | ) { 146 | const query = db.table('permission').insert(data); 147 | 148 | if (trx) query.transacting(trx); 149 | 150 | return query; 151 | } 152 | 153 | export async function updateOnePermissionService( 154 | { 155 | id, 156 | data, 157 | }: { 158 | id: string | number; 159 | data: Record; 160 | }, 161 | trx?: Knex.Transaction 162 | ) { 163 | const query = db.table('permission').update(data).where('id', id); 164 | 165 | if (trx) query.transacting(trx); 166 | 167 | return query; 168 | } 169 | 170 | export async function deleteOnePermissionService( 171 | id: string | number, 172 | trx?: Knex.Transaction 173 | ) { 174 | const query = db.table('permission').where('id', id).del(); 175 | 176 | if (trx) query.transacting(trx); 177 | 178 | return query; 179 | } 180 | 181 | export async function deleteManyPermissionsService( 182 | conds: Record, 183 | trx?: Knex.Transaction 184 | ) { 185 | const query = db.table('permission').where(conds).del(); 186 | 187 | if (trx) query.transacting(trx); 188 | 189 | return query; 190 | } 191 | 192 | export async function getExistingPermissionService( 193 | data: Record 194 | ) { 195 | const permission = await db 196 | .table('permission') 197 | .select('id', 'is_deleted') 198 | .where(data); 199 | return permission[0] || null; 200 | } 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExpressJS Starter Template For RBAC Systems 2 | 3 | A scalable Express.js project with TypeScript featuring CRUD with pagination, filtering, sorting, file uploads, soft delete, RBAC, JWT authentication, access and audit logs, and cron jobs for scheduled tasks. 4 | 5 | ![Cover Image](./src/docs/cover.png) 6 | 7 | ## Features 8 | 9 | - **Role-Based Access Control (RBAC)** – Fine-grained access control for different user roles and permissions. 10 | - **CRUD Operations** – Create, Read, Update, Delete endpoints out of the box. 11 | - **Pagination, Filtering, Sorting, Searching** – Easily manage large datasets with built-in pagination, query-based filtering, and sorting mechanisms. 12 | - **File Upload** – Upload and manage files using multer. 13 | - **Soft Delete** – Soft-delete support using timestamps instead of permanently removing data. 14 | - **Multi Delete & Multi Create** – Perform bulk operations with ease. 15 | - **Authentication & Login** – Token-based login system using JWT. 16 | - **Access Logs** – Track all incoming requests for monitoring and debugging. 17 | - **Audit Logs** – Record data changes with before/after snapshots for critical actions. 18 | - **Cron Jobs** – Scheduled background tasks using node-cron. 19 | - **Job Queue** – Queued background tasks using Redis & BullMQ. 20 | 21 | ## 🧱 Tech Stack 22 | 23 | - **Express.js** – Web framework 24 | - **TypeScript** – Static type-checking 25 | - **MySQL** – Database 26 | - **Knex** – Query Builder 27 | - **JWT** – Authentication 28 | - **Multer** – File uploads 29 | - **Node-Cron** – Scheduled jobs 30 | - **Redis & BullMQ** – Job Queue 31 | - **Morgan** – Logging 32 | - **Docker** - Containerization 33 | - **ESLint, Prettier** - Controlling code quality 34 | - **Husky** - Git hook 35 | 36 | ## 📦 Use Case Ideas 37 | 38 | - Admin dashboards 39 | - Internal tools 40 | - APIs for web/mobile apps 41 | - SaaS backends 42 | 43 | ## 🚀 Quick Start 44 | 45 | ### Prerequisites 46 | 47 | - **Docker** - [Download Docker](https://www.docker.com/products/docker-desktop/) 🐳 48 | - **Docker Compose** - Usually comes with Docker Desktop 49 | 50 | ### Clone the repository 51 | 52 | ```bash 53 | git clone https://github.com/MinPyaeKyaw/rbac-expressjs-starter.git 54 | cd rbac-expressjs-starter 55 | ``` 56 | 57 | ### Start the Application with Docker 58 | 59 | 1. **Configure database credentials** (Optional but recommended): 60 | 61 | Before starting, you may want to update the database credentials in `docker-compose.yml` for security: 62 | 63 | - Open `docker-compose.yml` 64 | - Update the MySQL service environment variables: 65 | - `MYSQL_ROOT_PASSWORD` - Set your MySQL root password 66 | - `MYSQL_PASSWORD` - Set your MySQL user password (should match `MYSQL_ROOT_PASSWORD` if using root) 67 | - Update the app service environment variables to match: 68 | - `DB_USER` - MySQL username (default: `root`) 69 | - `DB_PASSWORD` - MySQL password (must match `MYSQL_ROOT_PASSWORD`) 70 | - Update the MySQL healthcheck password to match your `MYSQL_ROOT_PASSWORD` 71 | 72 | **Example:** 73 | 74 | ```yaml 75 | mysql: 76 | environment: 77 | - MYSQL_ROOT_PASSWORD=your_secure_password 78 | - MYSQL_PASSWORD=your_secure_password 79 | 80 | app: 81 | environment: 82 | - DB_USER=root 83 | - DB_PASSWORD=your_secure_password 84 | ``` 85 | 86 | 2. **Build and start all services** (MySQL, Redis, and the Express app): 87 | 88 | ```bash 89 | docker compose up --build 90 | ``` 91 | 92 | This will automatically: 93 | 94 | - Start a MySQL 8.0 database 95 | - Start a Redis server for job queues 96 | - Wait for MySQL to be ready (healthcheck) 97 | - **Automatically run database migrations** to create all tables 98 | - **Automatically run database seeds** to populate initial data 99 | - Build and start the Express.js application 100 | 101 | 3. **Access the application**: 102 | 103 | - API Server: http://localhost:3000 104 | - MySQL Database: localhost:3306 105 | - Redis: localhost:6379 106 | 107 | 4. **Log in with these credentials** (from seed data): 108 | 109 | The seeds run automatically, so you can immediately log in with any of these test users: 110 | 111 | - **Admin User** 112 | - Username: `admin` 113 | - Password: `admin123` 114 | - **Super Admin User** 115 | - Username: `superadmin` 116 | - Password: `admin123` 117 | - **Developer User** 118 | - Username: `developer` 119 | - Password: `password123` 120 | - **Test User** 121 | - Username: `testuser` 122 | - Password: `password123` 123 | 124 | ### Development Mode (Optional) 125 | 126 | If you prefer to run without Docker for development: 127 | 128 | 1. **Install dependencies**: 129 | 130 | ```bash 131 | npm install 132 | ``` 133 | 134 | 2. **Set up environment variables**: 135 | 136 | - Copy `.env.example` to `.env` (if available) 137 | - Configure your local MySQL and Redis connections 138 | 139 | 3. **Run database migrations and seeds**: 140 | 141 | ```bash 142 | # Run migrations to create database tables 143 | npm run db:migrate 144 | 145 | # Run seeds to populate initial data 146 | npm run db:seed 147 | ``` 148 | 149 | 4. **Start the development server**: 150 | 151 | ```bash 152 | npm run dev 153 | ``` 154 | 155 | **Note**: 156 | 157 | - When using Docker, migrations and seeds run automatically on container startup 158 | - Do not run SQL schema files directly. Always use migrations to manage your database schema 159 | 160 | ### 📄 API Documentation 161 | 162 | - **API Documentation**: Read README.md in feature folders 163 | - **Technical Documentation**: Read [here](src/docs/tech_docs.md) 164 | 165 | ### 📄 Additional Resources 166 | 167 | 1. 📄 Postman collection can be found in `src/docs` folder. Get [here](src/docs/rbac_express.postman_collection. 168 | 2. 📝 Read detailed technical documentation [here](src/docs/tech_docs.md) 169 | 170 | ## 👨‍💻 Author 171 | 172 | **Sai Min Pyae Kyaw** 173 | 174 | 💼 Passionate Full Stack Developer | Node.js | TypeScript | React | MySQL 175 | 📍 Based in Myanmar 176 | 177 | ### 🌐 Connect with me 178 | 179 | - 💼 [LinkedIn](https://www.linkedin.com/in/sai-min-pyae-kyaw-369005200/) 180 | - 💻 [GitHub](https://github.com/MinPyaeKyaw) 181 | - 🌍 [Facebook](https://www.facebook.com/minpyae.kyaw.73) 182 | 183 | --- 184 | 185 | Made with ❤️ by Sai Min Pyae Kyaw 186 | -------------------------------------------------------------------------------- /src/features/rbac/role/role.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { AppError, responseData } from '../../../utils/http'; 3 | import { MESSAGES } from '../../../configs/messages'; 4 | import { 5 | getAllRolesService, 6 | getOneRoleService, 7 | getExistingRoleService, 8 | createOneRoleService, 9 | createManyRolesService, 10 | updateOneRoleService, 11 | deleteOneRoleService, 12 | deleteManyRolesService, 13 | softDeleteOneRoleService, 14 | softDeleteManyRolesService, 15 | } from './role.service'; 16 | import db from '../../../db/db'; 17 | import { Knex } from 'knex'; 18 | import { ListQuery } from '../../../types/types'; 19 | import { v4 as uuidv4 } from 'uuid'; 20 | 21 | export async function getAllRolesController( 22 | req: Request, 23 | res: Response, 24 | next: NextFunction 25 | ) { 26 | try { 27 | const result = await getAllRolesService(req.query as unknown as ListQuery); 28 | 29 | responseData({ 30 | res, 31 | status: 200, 32 | message: MESSAGES.SUCCESS.RETRIVE, 33 | data: result, 34 | }); 35 | } catch (error) { 36 | next(error); 37 | } 38 | } 39 | 40 | export async function getOneRoleController( 41 | req: Request, 42 | res: Response, 43 | next: NextFunction 44 | ) { 45 | try { 46 | const product = await getOneRoleService(req.params.id); 47 | 48 | responseData({ 49 | res, 50 | status: 200, 51 | message: MESSAGES.SUCCESS.RETRIVE, 52 | data: product, 53 | }); 54 | } catch (error) { 55 | next(error); 56 | } 57 | } 58 | 59 | export async function createOneRoleController( 60 | req: Request, 61 | res: Response, 62 | next: NextFunction 63 | ) { 64 | const trx: Knex.Transaction = await db.transaction(); 65 | try { 66 | const existingRole = await getExistingRoleService({ 67 | name: req.body.name, 68 | }); 69 | if (existingRole) 70 | throw new AppError(`${req.body.name} is already existed!`, 400); 71 | 72 | const payload = { 73 | id: uuidv4(), 74 | name: req.body.name, 75 | created_by: req.body.user.id, 76 | }; 77 | const createdRole = await createOneRoleService(payload, trx); 78 | 79 | await trx.commit(); 80 | 81 | responseData({ 82 | res, 83 | status: 200, 84 | message: MESSAGES.SUCCESS.CREATE, 85 | data: createdRole, 86 | }); 87 | } catch (error) { 88 | await trx.rollback(); 89 | next(error); 90 | } 91 | } 92 | 93 | export async function createManyRolesController( 94 | req: Request, 95 | res: Response, 96 | next: NextFunction 97 | ) { 98 | const trx: Knex.Transaction = await db.transaction(); 99 | try { 100 | const payload = req.body.roles.map((action: Record) => ({ 101 | id: uuidv4(), 102 | name: action.name, 103 | created_by: req.body.user.id, 104 | })); 105 | await createManyRolesService(payload, trx); 106 | 107 | await trx.commit(); 108 | 109 | responseData({ 110 | res, 111 | status: 200, 112 | message: MESSAGES.SUCCESS.CREATE, 113 | data: null, 114 | }); 115 | } catch (error) { 116 | await trx.rollback(); 117 | next(error); 118 | } 119 | } 120 | 121 | export async function updateOneRoleController( 122 | req: Request, 123 | res: Response, 124 | next: NextFunction 125 | ) { 126 | const trx: Knex.Transaction = await db.transaction(); 127 | try { 128 | const payload = { 129 | name: req.body.name, 130 | updated_by: req.body.user.id, 131 | }; 132 | const updatedRole = await updateOneRoleService( 133 | { 134 | id: req.params.id, 135 | data: payload, 136 | }, 137 | trx 138 | ); 139 | 140 | await trx.commit(); 141 | 142 | responseData({ 143 | res, 144 | status: 200, 145 | message: MESSAGES.SUCCESS.UPDATE, 146 | data: updatedRole, 147 | }); 148 | } catch (error) { 149 | await trx.rollback(); 150 | next(error); 151 | } 152 | } 153 | 154 | export async function deleteOneRoleController( 155 | req: Request, 156 | res: Response, 157 | next: NextFunction 158 | ) { 159 | const trx: Knex.Transaction = await db.transaction(); 160 | try { 161 | const isExistedRole = await getExistingRoleService({ 162 | id: req.params.id, 163 | }); 164 | 165 | if (!isExistedRole) throw new AppError(MESSAGES.ERROR.BAD_REQUEST, 400); 166 | 167 | const deletedRole = await deleteOneRoleService(req.params.id); 168 | 169 | await trx.commit(); 170 | 171 | responseData({ 172 | res, 173 | status: 200, 174 | message: MESSAGES.SUCCESS.DELETE, 175 | data: deletedRole, 176 | }); 177 | } catch (error) { 178 | await trx.rollback(); 179 | next(error); 180 | } 181 | } 182 | 183 | export async function deleteManyRolesController( 184 | req: Request, 185 | res: Response, 186 | next: NextFunction 187 | ) { 188 | const trx: Knex.Transaction = await db.transaction(); 189 | try { 190 | await deleteManyRolesService(req.body.ids, trx); 191 | 192 | await trx.commit(); 193 | 194 | responseData({ 195 | res, 196 | status: 200, 197 | message: MESSAGES.SUCCESS.DELETE, 198 | data: null, 199 | }); 200 | } catch (error) { 201 | await trx.rollback(); 202 | next(error); 203 | } 204 | } 205 | 206 | export async function softDeleteOneRoleController( 207 | req: Request, 208 | res: Response, 209 | next: NextFunction 210 | ) { 211 | const trx: Knex.Transaction = await db.transaction(); 212 | try { 213 | const isExistedRole = await getExistingRoleService({ 214 | id: req.params.id, 215 | }); 216 | 217 | if (!isExistedRole) throw new AppError(MESSAGES.ERROR.BAD_REQUEST, 400); 218 | 219 | const deletedRole = await softDeleteOneRoleService(req.params.id); 220 | 221 | await trx.commit(); 222 | 223 | responseData({ 224 | res, 225 | status: 200, 226 | message: MESSAGES.SUCCESS.DELETE, 227 | data: deletedRole, 228 | }); 229 | } catch (error) { 230 | await trx.rollback(); 231 | next(error); 232 | } 233 | } 234 | 235 | export async function softDeleteManyRolesController( 236 | req: Request, 237 | res: Response, 238 | next: NextFunction 239 | ) { 240 | const trx: Knex.Transaction = await db.transaction(); 241 | try { 242 | await softDeleteManyRolesService(req.body.ids, trx); 243 | 244 | await trx.commit(); 245 | 246 | responseData({ 247 | res, 248 | status: 200, 249 | message: MESSAGES.SUCCESS.DELETE, 250 | data: null, 251 | }); 252 | } catch (error) { 253 | await trx.rollback(); 254 | next(error); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/features/rbac/action/action.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { AppError, responseData } from '../../../utils/http'; 3 | import { MESSAGES } from '../../../configs/messages'; 4 | import { 5 | getAllActionsService, 6 | getOneActionService, 7 | getExistingActionService, 8 | createOneActionService, 9 | createManyActionsService, 10 | updateOneActionService, 11 | deleteOneActionService, 12 | deleteManyActionsService, 13 | softDeleteOneActionService, 14 | softDeleteManyActionsService, 15 | } from './action.service'; 16 | import db from '../../../db/db'; 17 | import { Knex } from 'knex'; 18 | import { ListQuery } from '../../../types/types'; 19 | import { v4 as uuidv4 } from 'uuid'; 20 | 21 | export async function getAllActionsController( 22 | req: Request, 23 | res: Response, 24 | next: NextFunction 25 | ) { 26 | try { 27 | const result = await getAllActionsService( 28 | req.query as unknown as ListQuery 29 | ); 30 | 31 | responseData({ 32 | res, 33 | status: 200, 34 | message: MESSAGES.SUCCESS.RETRIVE, 35 | data: result, 36 | }); 37 | } catch (error) { 38 | next(error); 39 | } 40 | } 41 | 42 | export async function getOneActionController( 43 | req: Request, 44 | res: Response, 45 | next: NextFunction 46 | ) { 47 | try { 48 | const product = await getOneActionService(req.params.id); 49 | 50 | responseData({ 51 | res, 52 | status: 200, 53 | message: MESSAGES.SUCCESS.RETRIVE, 54 | data: product, 55 | }); 56 | } catch (error) { 57 | next(error); 58 | } 59 | } 60 | 61 | export async function createOneActionController( 62 | req: Request, 63 | res: Response, 64 | next: NextFunction 65 | ) { 66 | const trx: Knex.Transaction = await db.transaction(); 67 | try { 68 | const existingAction = await getExistingActionService({ 69 | name: req.body.name, 70 | }); 71 | if (existingAction) 72 | throw new AppError(`${req.body.name} is already existed!`, 400); 73 | 74 | const payload = { 75 | id: uuidv4(), 76 | name: req.body.name, 77 | created_by: req.body.user.id, 78 | }; 79 | const createdAction = await createOneActionService(payload, trx); 80 | 81 | await trx.commit(); 82 | 83 | responseData({ 84 | res, 85 | status: 200, 86 | message: MESSAGES.SUCCESS.CREATE, 87 | data: createdAction, 88 | }); 89 | } catch (error) { 90 | await trx.rollback(); 91 | next(error); 92 | } 93 | } 94 | 95 | export async function createManyActionsController( 96 | req: Request, 97 | res: Response, 98 | next: NextFunction 99 | ) { 100 | const trx: Knex.Transaction = await db.transaction(); 101 | try { 102 | const payload = req.body.actions.map((action: Record) => ({ 103 | id: uuidv4(), 104 | name: action.name, 105 | created_by: req.body.user.id, 106 | })); 107 | await createManyActionsService(payload, trx); 108 | 109 | await trx.commit(); 110 | 111 | responseData({ 112 | res, 113 | status: 200, 114 | message: MESSAGES.SUCCESS.CREATE, 115 | data: null, 116 | }); 117 | } catch (error) { 118 | await trx.rollback(); 119 | next(error); 120 | } 121 | } 122 | 123 | export async function updateOneActionController( 124 | req: Request, 125 | res: Response, 126 | next: NextFunction 127 | ) { 128 | const trx: Knex.Transaction = await db.transaction(); 129 | try { 130 | const payload = { 131 | name: req.body.name, 132 | updated_by: req.body.user.id, 133 | }; 134 | const updatedAction = await updateOneActionService( 135 | { 136 | id: req.params.id, 137 | data: payload, 138 | }, 139 | trx 140 | ); 141 | 142 | await trx.commit(); 143 | 144 | responseData({ 145 | res, 146 | status: 200, 147 | message: MESSAGES.SUCCESS.UPDATE, 148 | data: updatedAction, 149 | }); 150 | } catch (error) { 151 | await trx.rollback(); 152 | next(error); 153 | } 154 | } 155 | 156 | export async function deleteOneActionController( 157 | req: Request, 158 | res: Response, 159 | next: NextFunction 160 | ) { 161 | const trx: Knex.Transaction = await db.transaction(); 162 | try { 163 | const isExistedAction = await getExistingActionService({ 164 | id: req.params.id, 165 | }); 166 | 167 | if (!isExistedAction) throw new AppError(MESSAGES.ERROR.BAD_REQUEST, 400); 168 | 169 | const deletedAction = await deleteOneActionService(req.params.id); 170 | 171 | await trx.commit(); 172 | 173 | responseData({ 174 | res, 175 | status: 200, 176 | message: MESSAGES.SUCCESS.DELETE, 177 | data: deletedAction, 178 | }); 179 | } catch (error) { 180 | await trx.rollback(); 181 | next(error); 182 | } 183 | } 184 | 185 | export async function deleteManyActionsController( 186 | req: Request, 187 | res: Response, 188 | next: NextFunction 189 | ) { 190 | const trx: Knex.Transaction = await db.transaction(); 191 | try { 192 | await deleteManyActionsService(req.body.ids, trx); 193 | 194 | await trx.commit(); 195 | 196 | responseData({ 197 | res, 198 | status: 200, 199 | message: MESSAGES.SUCCESS.DELETE, 200 | data: null, 201 | }); 202 | } catch (error) { 203 | await trx.rollback(); 204 | next(error); 205 | } 206 | } 207 | 208 | export async function softDeleteOneActionController( 209 | req: Request, 210 | res: Response, 211 | next: NextFunction 212 | ) { 213 | const trx: Knex.Transaction = await db.transaction(); 214 | try { 215 | const isExistedAction = await getExistingActionService({ 216 | id: req.params.id, 217 | }); 218 | 219 | if (!isExistedAction) throw new AppError(MESSAGES.ERROR.BAD_REQUEST, 400); 220 | 221 | const deletedAction = await softDeleteOneActionService(req.params.id); 222 | 223 | await trx.commit(); 224 | 225 | responseData({ 226 | res, 227 | status: 200, 228 | message: MESSAGES.SUCCESS.DELETE, 229 | data: deletedAction, 230 | }); 231 | } catch (error) { 232 | await trx.rollback(); 233 | next(error); 234 | } 235 | } 236 | 237 | export async function softDeleteManyActionsController( 238 | req: Request, 239 | res: Response, 240 | next: NextFunction 241 | ) { 242 | const trx: Knex.Transaction = await db.transaction(); 243 | try { 244 | await softDeleteManyActionsService(req.body.ids, trx); 245 | 246 | await trx.commit(); 247 | 248 | responseData({ 249 | res, 250 | status: 200, 251 | message: MESSAGES.SUCCESS.DELETE, 252 | data: null, 253 | }); 254 | } catch (error) { 255 | await trx.rollback(); 256 | next(error); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/features/cached-product-category/cached-product-category.service.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from 'knex'; 2 | import db from '../../db/db'; 3 | import redisClient, { invalidateCache } from '../../services/redis'; 4 | import { getPaginatedData, getPagination } from '../../utils/common'; 5 | import { ListQuery } from '../../types/types'; 6 | 7 | const CACHE_TTL = 3600; 8 | const CACHE_KEY = 'cached_product_category'; 9 | 10 | export async function getAllCachedProductCategoriesService(filters: ListQuery) { 11 | // Try to get from cache first 12 | const cachedData = await redisClient.get(CACHE_KEY); 13 | if (cachedData) { 14 | return JSON.parse(cachedData); 15 | } 16 | 17 | // If not in cache, fetch from database 18 | const query = db 19 | .table('product_category') 20 | .select( 21 | 'product_category.id', 22 | 'product_category.name', 23 | 'product_category.is_deleted' 24 | ); 25 | const totalCountQuery = db.table('product_category').count('* as count'); 26 | 27 | let pagination; 28 | if (filters.page && filters.size) { 29 | pagination = getPagination({ 30 | page: filters.page as number, 31 | size: filters.size as number, 32 | }); 33 | query.limit(pagination.limit).offset(pagination.offset); 34 | } 35 | 36 | if (filters.sort) { 37 | query.orderBy(filters.sort, filters.order || 'asc'); 38 | } else { 39 | query.orderBy('product_category.created_at', 'desc'); 40 | } 41 | 42 | if (filters.keyword) { 43 | query.whereILike('product_category.name', `%${filters.keyword}%`); 44 | totalCountQuery.whereILike('product_category.name', `%${filters.keyword}%`); 45 | } 46 | 47 | const result = await getPaginatedData( 48 | query, 49 | totalCountQuery, 50 | filters, 51 | pagination 52 | ); 53 | 54 | // Store in cache 55 | await redisClient.setex(CACHE_KEY, CACHE_TTL, JSON.stringify(result)); 56 | 57 | return result; 58 | } 59 | 60 | export async function getOneCachedProductCategoryService(id: string | number) { 61 | // If not in cache, fetch from database 62 | const product_category = await db 63 | .table('product_category') 64 | .select('id', 'name', 'is_deleted') 65 | .where('id', id); 66 | 67 | const result = product_category[0] || null; 68 | 69 | return result; 70 | } 71 | 72 | export async function createOneCachedProductCategoryService( 73 | data: Record, 74 | trx?: Knex.Transaction 75 | ) { 76 | const query = db.table('product_category').insert(data); 77 | if (trx) query.transacting(trx); 78 | await query; 79 | 80 | // Invalidate cache after creation 81 | await invalidateCache(CACHE_KEY); 82 | 83 | return data; 84 | } 85 | 86 | export async function createManyCachedProductCategoriesService( 87 | data: Record[], 88 | trx?: Knex.Transaction 89 | ) { 90 | const query = db.table('product_category').insert(data); 91 | if (trx) query.transacting(trx); 92 | await query; 93 | 94 | // Invalidate cache after creation 95 | await invalidateCache(CACHE_KEY); 96 | 97 | return data; 98 | } 99 | 100 | export async function updateOneCachedProductCategoryService( 101 | { 102 | id, 103 | data, 104 | }: { 105 | id: string | number; 106 | data: Record; 107 | }, 108 | trx?: Knex.Transaction 109 | ) { 110 | const query = db.table('product_category').update(data).where('id', id); 111 | 112 | if (trx) query.transacting(trx); 113 | await query; 114 | 115 | // Invalidate cache after update 116 | await invalidateCache(CACHE_KEY); 117 | 118 | return query; 119 | } 120 | 121 | export async function deleteOneCachedProductCategoryService( 122 | id: string | number, 123 | trx?: Knex.Transaction 124 | ) { 125 | const toDelete = await db 126 | .table('product_category') 127 | .select('id', 'name', 'is_deleted') 128 | .where('id', id); 129 | 130 | const query = db.table('product_category').where('id', id).del(); 131 | if (trx) query.transacting(trx); 132 | await query; 133 | 134 | // Invalidate cache after deletion 135 | await invalidateCache(CACHE_KEY); 136 | 137 | return toDelete[0] || null; 138 | } 139 | 140 | export async function deleteManyCachedProductCategoriesService( 141 | ids: string[], 142 | trx?: Knex.Transaction 143 | ) { 144 | const toDelete = await db 145 | .table('product_category') 146 | .select('*') 147 | .whereIn('id', ids); 148 | 149 | const query = db.table('product_category').whereIn('id', ids).del(); 150 | if (trx) query.transacting(trx); 151 | await query; 152 | 153 | // Invalidate cache after deletion 154 | await invalidateCache(CACHE_KEY); 155 | 156 | return toDelete; 157 | } 158 | 159 | export async function softDeleteOneCachedProductCategoryService( 160 | id: string | number, 161 | trx?: Knex.Transaction 162 | ) { 163 | const query = db 164 | .table('product_category') 165 | .update({ is_deleted: true }) 166 | .where('id', id); 167 | 168 | if (trx) query.transacting(trx); 169 | await query; 170 | 171 | const toDelete = await db 172 | .table('product_category') 173 | .select('id', 'name', 'is_deleted') 174 | .where('id', id); 175 | 176 | // Invalidate cache after soft deletion 177 | await invalidateCache(CACHE_KEY); 178 | 179 | return toDelete[0] || null; 180 | } 181 | 182 | export async function softDeleteManyCachedProductCategoriesService( 183 | ids: string[] | number[], 184 | trx?: Knex.Transaction 185 | ) { 186 | const query = db 187 | .table('product_category') 188 | .update({ is_deleted: true }) 189 | .whereIn('id', ids); 190 | if (trx) query.transacting(trx); 191 | await query; 192 | 193 | const toDelete = await db 194 | .table('product_category') 195 | .select('id', 'name', 'is_deleted') 196 | .whereIn('id', ids); 197 | 198 | // Invalidate cache after soft deletion 199 | await invalidateCache(CACHE_KEY); 200 | 201 | return toDelete || null; 202 | } 203 | 204 | export async function getExistingCachedProductCategoryService( 205 | data: Record 206 | ) { 207 | const product_category = await db 208 | .table('product_category') 209 | .select('id', 'name', 'is_deleted') 210 | .where(data); 211 | return product_category[0] || null; 212 | } 213 | 214 | // Cache management functions 215 | export async function clearProductCategoryCache() { 216 | await invalidateCache(CACHE_KEY); 217 | return { message: 'Product category cache cleared successfully' }; 218 | } 219 | 220 | export async function getProductCategoryCacheStats() { 221 | const keys = await redisClient.keys(`${CACHE_KEY}:*`); 222 | const stats = { 223 | totalKeys: keys.length, 224 | listKeys: keys.filter((key) => key.includes(':list:')).length, 225 | detailKeys: keys.filter((key) => key.includes(':detail:')).length, 226 | }; 227 | return stats; 228 | } 229 | -------------------------------------------------------------------------------- /src/features/rbac/channel/channel.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { AppError, responseData } from '../../../utils/http'; 3 | import { MESSAGES } from '../../../configs/messages'; 4 | import { 5 | getAllChannelsService, 6 | getOneChannelService, 7 | getExistingChannelService, 8 | createOneChannelService, 9 | createManyChannelsService, 10 | updateOneChannelService, 11 | deleteOneChannelService, 12 | deleteManyChannelsService, 13 | softDeleteOneChannelService, 14 | softDeleteManyChannelsService, 15 | } from './channel.service'; 16 | import db from '../../../db/db'; 17 | import { Knex } from 'knex'; 18 | import { ListQuery } from '../../../types/types'; 19 | import { v4 as uuidv4 } from 'uuid'; 20 | 21 | export async function getAllChannelsController( 22 | req: Request, 23 | res: Response, 24 | next: NextFunction 25 | ) { 26 | try { 27 | const result = await getAllChannelsService( 28 | req.query as unknown as ListQuery 29 | ); 30 | 31 | responseData({ 32 | res, 33 | status: 200, 34 | message: MESSAGES.SUCCESS.RETRIVE, 35 | data: result, 36 | }); 37 | } catch (error) { 38 | next(error); 39 | } 40 | } 41 | 42 | export async function getOneChannelController( 43 | req: Request, 44 | res: Response, 45 | next: NextFunction 46 | ) { 47 | try { 48 | const product = await getOneChannelService(req.params.id); 49 | 50 | responseData({ 51 | res, 52 | status: 200, 53 | message: MESSAGES.SUCCESS.RETRIVE, 54 | data: product, 55 | }); 56 | } catch (error) { 57 | next(error); 58 | } 59 | } 60 | 61 | export async function createOneChannelController( 62 | req: Request, 63 | res: Response, 64 | next: NextFunction 65 | ) { 66 | const trx: Knex.Transaction = await db.transaction(); 67 | try { 68 | const existingChannel = await getExistingChannelService({ 69 | name: req.body.name, 70 | }); 71 | if (existingChannel) 72 | throw new AppError(`${req.body.name} is already existed!`, 400); 73 | 74 | const payload = { 75 | id: uuidv4(), 76 | name: req.body.name, 77 | created_by: req.body.user.id, 78 | }; 79 | const createdChannel = await createOneChannelService(payload, trx); 80 | 81 | await trx.commit(); 82 | 83 | responseData({ 84 | res, 85 | status: 200, 86 | message: MESSAGES.SUCCESS.CREATE, 87 | data: createdChannel, 88 | }); 89 | } catch (error) { 90 | await trx.rollback(); 91 | next(error); 92 | } 93 | } 94 | 95 | export async function createManyChannelsController( 96 | req: Request, 97 | res: Response, 98 | next: NextFunction 99 | ) { 100 | const trx: Knex.Transaction = await db.transaction(); 101 | try { 102 | const payload = req.body.channel.map((action: Record) => ({ 103 | id: uuidv4(), 104 | name: action.name, 105 | created_by: req.body.user.id, 106 | })); 107 | await createManyChannelsService(payload, trx); 108 | 109 | await trx.commit(); 110 | 111 | responseData({ 112 | res, 113 | status: 200, 114 | message: MESSAGES.SUCCESS.CREATE, 115 | data: null, 116 | }); 117 | } catch (error) { 118 | await trx.rollback(); 119 | next(error); 120 | } 121 | } 122 | 123 | export async function updateOneChannelController( 124 | req: Request, 125 | res: Response, 126 | next: NextFunction 127 | ) { 128 | const trx: Knex.Transaction = await db.transaction(); 129 | try { 130 | const payload = { 131 | name: req.body.name, 132 | updated_by: req.body.user.id, 133 | }; 134 | const updatedChannel = await updateOneChannelService( 135 | { 136 | id: req.params.id, 137 | data: payload, 138 | }, 139 | trx 140 | ); 141 | 142 | await trx.commit(); 143 | 144 | responseData({ 145 | res, 146 | status: 200, 147 | message: MESSAGES.SUCCESS.UPDATE, 148 | data: updatedChannel, 149 | }); 150 | } catch (error) { 151 | await trx.rollback(); 152 | next(error); 153 | } 154 | } 155 | 156 | export async function deleteOneChannelController( 157 | req: Request, 158 | res: Response, 159 | next: NextFunction 160 | ) { 161 | const trx: Knex.Transaction = await db.transaction(); 162 | try { 163 | const isExistedChannel = await getExistingChannelService({ 164 | id: req.params.id, 165 | }); 166 | 167 | if (!isExistedChannel) throw new AppError(MESSAGES.ERROR.BAD_REQUEST, 400); 168 | 169 | const deletedChannel = await deleteOneChannelService(req.params.id); 170 | 171 | await trx.commit(); 172 | 173 | responseData({ 174 | res, 175 | status: 200, 176 | message: MESSAGES.SUCCESS.DELETE, 177 | data: deletedChannel, 178 | }); 179 | } catch (error) { 180 | await trx.rollback(); 181 | next(error); 182 | } 183 | } 184 | 185 | export async function deleteManyChannelsController( 186 | req: Request, 187 | res: Response, 188 | next: NextFunction 189 | ) { 190 | const trx: Knex.Transaction = await db.transaction(); 191 | try { 192 | await deleteManyChannelsService(req.body.ids, trx); 193 | 194 | await trx.commit(); 195 | 196 | responseData({ 197 | res, 198 | status: 200, 199 | message: MESSAGES.SUCCESS.DELETE, 200 | data: null, 201 | }); 202 | } catch (error) { 203 | await trx.rollback(); 204 | next(error); 205 | } 206 | } 207 | 208 | export async function softDeleteOneChannelController( 209 | req: Request, 210 | res: Response, 211 | next: NextFunction 212 | ) { 213 | const trx: Knex.Transaction = await db.transaction(); 214 | try { 215 | const isExistedChannel = await getExistingChannelService({ 216 | id: req.params.id, 217 | }); 218 | 219 | if (!isExistedChannel) throw new AppError(MESSAGES.ERROR.BAD_REQUEST, 400); 220 | 221 | const deletedChannel = await softDeleteOneChannelService(req.params.id); 222 | 223 | await trx.commit(); 224 | 225 | responseData({ 226 | res, 227 | status: 200, 228 | message: MESSAGES.SUCCESS.DELETE, 229 | data: deletedChannel, 230 | }); 231 | } catch (error) { 232 | await trx.rollback(); 233 | next(error); 234 | } 235 | } 236 | 237 | export async function softDeleteManyChannelsController( 238 | req: Request, 239 | res: Response, 240 | next: NextFunction 241 | ) { 242 | const trx: Knex.Transaction = await db.transaction(); 243 | try { 244 | await softDeleteManyChannelsService(req.body.ids, trx); 245 | 246 | await trx.commit(); 247 | 248 | responseData({ 249 | res, 250 | status: 200, 251 | message: MESSAGES.SUCCESS.DELETE, 252 | data: null, 253 | }); 254 | } catch (error) { 255 | await trx.rollback(); 256 | next(error); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/features/product/product.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { AppError, responseData } from '../../utils/http'; 3 | import { MESSAGES } from '../../configs/messages'; 4 | import { 5 | createManyProductsService, 6 | createOneProductService, 7 | deleteManyProductsService, 8 | deleteOneProductService, 9 | getAllProductsService, 10 | getExistingProductService, 11 | getOneProductService, 12 | softDeleteManyProductsService, 13 | softDeleteOneProductService, 14 | updateOneProductService, 15 | } from './product.service'; 16 | import db from '../../db/db'; 17 | import { Knex } from 'knex'; 18 | import { ListQuery } from '../../types/types'; 19 | import { v4 as uuidv4 } from 'uuid'; 20 | 21 | export async function getAllProductsController( 22 | req: Request, 23 | res: Response, 24 | next: NextFunction 25 | ) { 26 | try { 27 | const result = await getAllProductsService( 28 | req.query as unknown as ListQuery 29 | ); 30 | 31 | responseData({ 32 | res, 33 | status: 200, 34 | message: MESSAGES.SUCCESS.RETRIVE, 35 | data: result, 36 | }); 37 | } catch (error) { 38 | next(error); 39 | } 40 | } 41 | 42 | export async function getOneProductController( 43 | req: Request, 44 | res: Response, 45 | next: NextFunction 46 | ) { 47 | try { 48 | const product = await getOneProductService(req.params.id); 49 | 50 | responseData({ 51 | res, 52 | status: 200, 53 | message: MESSAGES.SUCCESS.RETRIVE, 54 | data: product, 55 | }); 56 | } catch (error) { 57 | next(error); 58 | } 59 | } 60 | 61 | export async function createOneProductController( 62 | req: Request, 63 | res: Response, 64 | next: NextFunction 65 | ) { 66 | const trx: Knex.Transaction = await db.transaction(); 67 | try { 68 | const existingProduct = await getExistingProductService({ 69 | name: req.body.name, 70 | }); 71 | if (existingProduct) 72 | throw new AppError(`${req.body.name} is already existed!`, 400); 73 | 74 | const payload = { 75 | id: uuidv4(), 76 | name: req.body.name, 77 | price: req.body.price, 78 | category_id: req.body.category_id, 79 | created_by: req.body.user.id, 80 | }; 81 | const createdProduct = await createOneProductService(payload, trx); 82 | 83 | await trx.commit(); 84 | 85 | responseData({ 86 | res, 87 | status: 200, 88 | message: MESSAGES.SUCCESS.CREATE, 89 | data: createdProduct, 90 | }); 91 | } catch (error) { 92 | await trx.rollback(); 93 | next(error); 94 | } 95 | } 96 | 97 | export async function createManyProductsController( 98 | req: Request, 99 | res: Response, 100 | next: NextFunction 101 | ) { 102 | const trx: Knex.Transaction = await db.transaction(); 103 | try { 104 | const payload = req.body.products.map((pd: Record) => ({ 105 | id: uuidv4(), 106 | name: pd.name, 107 | price: pd.price, 108 | category_id: pd.category_id, 109 | created_by: req.body.user.id, 110 | })); 111 | const createdProducts = await createManyProductsService(payload, trx); 112 | 113 | await trx.commit(); 114 | 115 | responseData({ 116 | res, 117 | status: 200, 118 | message: MESSAGES.SUCCESS.CREATE, 119 | data: createdProducts, 120 | }); 121 | } catch (error) { 122 | await trx.rollback(); 123 | next(error); 124 | } 125 | } 126 | 127 | export async function updateOneProductController( 128 | req: Request, 129 | res: Response, 130 | next: NextFunction 131 | ) { 132 | const trx: Knex.Transaction = await db.transaction(); 133 | try { 134 | const payload = { 135 | name: req.body.name, 136 | price: req.body.price, 137 | category_id: req.body.category_id, 138 | updated_by: req.body.user.id, 139 | }; 140 | const updatedProduct = await updateOneProductService( 141 | { 142 | id: req.params.id, 143 | data: payload, 144 | }, 145 | trx 146 | ); 147 | 148 | await trx.commit(); 149 | 150 | responseData({ 151 | res, 152 | status: 200, 153 | message: MESSAGES.SUCCESS.UPDATE, 154 | data: updatedProduct, 155 | }); 156 | } catch (error) { 157 | await trx.rollback(); 158 | next(error); 159 | } 160 | } 161 | 162 | export async function deleteOneProductController( 163 | req: Request, 164 | res: Response, 165 | next: NextFunction 166 | ) { 167 | const trx: Knex.Transaction = await db.transaction(); 168 | try { 169 | const deletedProduct = await deleteOneProductService(req.params.id, trx); 170 | 171 | await trx.commit(); 172 | 173 | responseData({ 174 | res, 175 | status: 200, 176 | message: MESSAGES.SUCCESS.DELETE, 177 | data: deletedProduct, 178 | }); 179 | } catch (error) { 180 | await trx.rollback(); 181 | next(error); 182 | } 183 | } 184 | 185 | export async function deleteManyProductsController( 186 | req: Request, 187 | res: Response, 188 | next: NextFunction 189 | ) { 190 | const trx: Knex.Transaction = await db.transaction(); 191 | try { 192 | const deletedProducts = await deleteManyProductsService(req.body.ids, trx); 193 | 194 | await trx.commit(); 195 | 196 | responseData({ 197 | res, 198 | status: 200, 199 | message: MESSAGES.SUCCESS.DELETE, 200 | data: deletedProducts, 201 | }); 202 | } catch (error) { 203 | await trx.rollback(); 204 | next(error); 205 | } 206 | } 207 | 208 | export async function softDeleteOneProductController( 209 | req: Request, 210 | res: Response, 211 | next: NextFunction 212 | ) { 213 | const trx: Knex.Transaction = await db.transaction(); 214 | try { 215 | const isExistedProduct = await getExistingProductService({ 216 | id: req.params.id, 217 | }); 218 | 219 | if (!isExistedProduct) throw new AppError(MESSAGES.ERROR.BAD_REQUEST, 400); 220 | 221 | const deletedProduct = await softDeleteOneProductService(req.params.id); 222 | 223 | await trx.commit(); 224 | 225 | responseData({ 226 | res, 227 | status: 200, 228 | message: MESSAGES.SUCCESS.DELETE, 229 | data: deletedProduct, 230 | }); 231 | } catch (error) { 232 | await trx.rollback(); 233 | next(error); 234 | } 235 | } 236 | 237 | export async function softDeleteManyProductsController( 238 | req: Request, 239 | res: Response, 240 | next: NextFunction 241 | ) { 242 | const trx: Knex.Transaction = await db.transaction(); 243 | try { 244 | const deletedProducts = await softDeleteManyProductsService( 245 | req.body.ids, 246 | trx 247 | ); 248 | 249 | await trx.commit(); 250 | 251 | responseData({ 252 | res, 253 | status: 200, 254 | message: MESSAGES.SUCCESS.DELETE, 255 | data: deletedProducts, 256 | }); 257 | } catch (error) { 258 | await trx.rollback(); 259 | next(error); 260 | } 261 | } 262 | --------------------------------------------------------------------------------