├── .envexample ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── README.md ├── keys └── instruction.md ├── package-lock.json ├── package.json ├── src ├── .DS_Store ├── app.ts ├── auth │ ├── authUtils.ts │ ├── authentication.ts │ ├── authorization.ts │ └── schema.ts ├── config.js ├── config.ts ├── controllers │ ├── .DS_Store │ ├── authController.ts │ ├── profileController.ts │ └── userController.ts ├── core │ ├── ApiError.ts │ ├── ApiResponse.ts │ ├── JWT.ts │ └── Logger.ts ├── database │ ├── index.ts │ ├── model │ │ ├── Keystore.ts │ │ ├── Role.ts │ │ └── User.ts │ └── repository │ │ ├── KeystoreRepo.ts │ │ ├── RoleRepo.ts │ │ └── UserRepo.ts ├── helpers │ ├── apiFeatures.ts │ ├── asyncHandler.ts │ ├── role.ts │ ├── seeder.ts │ └── validator.ts ├── routes │ └── v1 │ │ ├── access │ │ ├── access.ts │ │ └── schema.ts │ │ ├── index.ts │ │ └── user │ │ ├── profile.ts │ │ ├── schema.ts │ │ └── user.ts ├── server.ts └── types │ └── app-request.d.ts └── tsconfig.json /.envexample: -------------------------------------------------------------------------------- 1 | # .env.example 2 | 3 | # Environment Name 4 | NODE_ENV=development 5 | 6 | # Server listen to this port 7 | PORT=6060 8 | 9 | # Base Url 10 | BASE_URL=http://localhost:6060 11 | 12 | #Cors 13 | CORS_URL=* 14 | 15 | # Databse 16 | # YOUR_MONGO_DB_NAME 17 | DB_NAME=starter-db 18 | 19 | #localhost or IP of the server 20 | # If using the docker installation then use 'mongo' for host name else localhost or ip or db server 21 | #YOUR_MONGO_DB_HOST_NAME 22 | DB_HOST=localhost 23 | 24 | DB_PORT=27017 25 | 26 | # Log 27 | # Example '/home/node/logs' 28 | # DEFAUlT is this project's directory 29 | LOG_DIR='' 30 | 31 | # Token Info 32 | ACCESS_TOKEN_VALIDITY_DAYS=30 33 | REFRESH_TOKEN_VALIDITY_DAYS=120 34 | TOKEN_ISSUER=softylines.com 35 | TOKEN_AUDIENCE=softylines.com 36 | 37 | ADMIN_NAME=admin 38 | ADMIN_EMAIL=admin@admin.com 39 | ADMIN_PASS=admin1234 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | 3 | node_modules/ 4 | 5 | # Build products 6 | build/ 7 | dist/ 8 | tools/ -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "@typescript-eslint/naming-convention": "off", 4 | "@typescript-eslint/ban-ts-comment": "off", 5 | "@typescript-eslint/no-explicit-any": "off", 6 | "@typescript-eslint/explicit-function-return-types": "off", 7 | "@typescript-eslint/explicit-module-boundary-types": "off" 8 | }, 9 | "extends": [ 10 | "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin 11 | "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 12 | "plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 13 | ], 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": 2020, // Allows for the parsing of modern ECMAScript features 17 | "sourceType": "module" // Allows for the use of imports 18 | }, 19 | 20 | "env": { 21 | "node": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Add any directories, files, or patterns you don't want to be tracked by version control 2 | .idea 3 | 4 | # ignore vs code project config files 5 | .vs 6 | 7 | # ignore logs 8 | logs 9 | *.log 10 | 11 | # ignore 3rd party lib 12 | node_modules 13 | 14 | # ignore key 15 | *.pem 16 | 17 | # Ignore built files 18 | build 19 | 20 | # ignore test converage 21 | coverage 22 | 23 | # Environment varibles 24 | *.env 25 | *.env.* 26 | 27 | .DS_Store 28 | 29 | 30 | 31 | ~/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | coverage/ 3 | keys/ 4 | logs/ 5 | node_modules/ 6 | *.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "semi": true, 4 | "trailingComma": "all", 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "tabWidth": 2 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 13 4 | services: 5 | - docker 6 | before_install: 7 | - cp .env.example .env 8 | - cp keys/public.pem.example keys/public.pem 9 | - cp keys/private.pem.example keys/private.pem 10 | - cp tests/.env.test.example tests/.env.test 11 | - docker-compose up -d 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "2.0.0", 6 | "configurations": [ 7 | { 8 | 9 | "type": "node", 10 | "request": "launch", 11 | "name": "Launch Program", 12 | "program": "${workspaceFolder}/src/server.ts", 13 | "preLaunchTask": "tsc: build - tsconfig.json", 14 | "runtimeArgs": [ 15 | "-r", 16 | "dotenv/config" 17 | ], 18 | "outFiles": [ 19 | "${workspaceFolder}/build/*.js", 20 | "${workspaceFolder}/build/**/*.js", 21 | "${workspaceFolder}/build/**/**/*.js", 22 | "${workspaceFolder}/build/**/**/**/*.js" 23 | ] 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": true, 3 | "typescript.preferences.importModuleSpecifier": "relative", 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "editor.insertSpaces": true, 6 | "editor.detectIndentation": false, 7 | "editor.tabSize": 2, 8 | "typescript.validate.enable": true, 9 | "typescript.tsdk": "node_modules/typescript/lib" 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js Backend Starter Typescript Project 2 | # Database configuration 3 | 1- Execute npm i 4 | 5 | 2- Execute npm run seed 6 | 7 | 3- execute commands under keys/instruction.md 8 | 9 | 4- this basic archticture contains login / logout apis and refresh token api. 10 | 11 | 5- this basic archtecture contains also : 12 | 13 | a) handle of authentication, authorisation. 14 | 15 | b) handle of apiError and apiResponse and configuration and handle of jwt and logger. 16 | 17 | c) contains basic models and repositories. 18 | 19 | d) contains helpers to prevent using try catch (async handler), (role) to use the role as middleware, and configuration of (validator joi). 20 | 21 | e) contains (app / config / server).ts files. 22 | 23 | # you should copy envexemple to .env 24 | # you should create public and private RSA keys under keys folder 25 | 26 | # Improvements 27 | This starter is from : 28 | 29 | https://github.com/janishar/nodejs-backend-architecture-typescript 30 | and the improvments are the following : 31 | 32 | 1- Add the controller layer. 33 | 34 | 2- Add custom soft delete. 35 | 36 | 3- Add pagination on getAll Api. 37 | 38 | 4- Add and Refactor ApiFeature. 39 | 40 | 5- Add seeder. 41 | 42 | 6- Add Swagger Documentation. 43 | 44 | -------------------------------------------------------------------------------- /keys/instruction.md: -------------------------------------------------------------------------------- 1 | private.pem 2 | public.pem 3 | 4 | commands : 5 | openssl genrsa -out private.pem 2048 6 | openssl rsa -in private.pem -outform PEM -pubout -out public.pem -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "progress-v2-takiacademy", 3 | "version": "1.0.2", 4 | "description": "The architecture for nodejs backend application. It is build on top of expressjs using typescript.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run build && npm run serve", 8 | "start:dev": "ts-node-dev -r dotenv/config src/server.ts", 9 | "serve": "node -r dotenv/config build/server.js", 10 | "build": "npm run clean && npm run build-ts", 11 | "watch": "concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold,green.bold\" \"npm run watch-ts\" \"npm run watch-node\"", 12 | "watch-node": "nodemon -r dotenv/config build/server.js", 13 | "clean": "rimraf ./build", 14 | "build-ts": "tsc", 15 | "watch-ts": "tsc -w", 16 | "eslint": "eslint . --ext .js,.ts", 17 | "upgrade": "npm update --save-dev && npm update --save", 18 | "test": "jest --forceExit --detectOpenHandles --coverage --verbose", 19 | "seed": "ts-node-dev -r dotenv/config src/helpers/seeder.ts" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://gitlab.com/takiacademy/progress-v2" 24 | }, 25 | "author": "TakiAcademy", 26 | "license": "Apache-2.0", 27 | "dependencies": { 28 | "@hapi/joi": "^17.1.1", 29 | "@types/bcryptjs": "^2.4.2", 30 | "@types/mongoose": "^5.11.97", 31 | "axios": "^0.21.1", 32 | "bcryptjs": "^2.4.3", 33 | "body-parser": "^1.19.0", 34 | "cookie-parser": "^1.4.5", 35 | "cors": "^2.8.5", 36 | "express": "^4.17.1", 37 | "helmet": "^4.1.1", 38 | "jsonwebtoken": "^8.5.1", 39 | "lodash": "^4.17.20", 40 | "moment": "^2.29.1", 41 | "mongoose": "^6.7.1", 42 | "mongoose-paginate-ts": "^1.2.7", 43 | "swagger-jsdoc": "^6.2.5", 44 | "swagger-ui-express": "^4.6.0", 45 | "tedis": "^0.1.12", 46 | "ts-node-dev": "^2.0.0", 47 | "typescript": "^4.8.4", 48 | "uuid4": "^2.0.3", 49 | "winston": "^3.3.3", 50 | "winston-daily-rotate-file": "^4.5.0" 51 | }, 52 | "devDependencies": { 53 | "@types/body-parser": "^1.19.0", 54 | "@types/cookie-parser": "^1.4.2", 55 | "@types/cors": "^2.8.8", 56 | "@types/express": "^4.17.8", 57 | "@types/hapi__joi": "^17.1.6", 58 | "@types/helmet": "^4.0.0", 59 | "@types/jest": "^26.0.15", 60 | "@types/jsonwebtoken": "^8.5.0", 61 | "@types/lodash": "^4.14.163", 62 | "@types/morgan": "^1.9.2", 63 | "@types/node": "^14.14.6", 64 | "@types/supertest": "^2.0.10", 65 | "@types/swagger-jsdoc": "^6.0.1", 66 | "@types/swagger-ui-express": "^4.1.3", 67 | "@types/uuid4": "^2.0.0", 68 | "@typescript-eslint/eslint-plugin": "^4.6.0", 69 | "@typescript-eslint/parser": "^4.6.0", 70 | "colors": "^1.4.0", 71 | "concurrently": "^5.3.0", 72 | "dotenv": "^8.2.0", 73 | "eslint": "^7.12.1", 74 | "jest": "^26.6.1", 75 | "nodemon": "^2.0.6", 76 | "supertest": "^6.0.0", 77 | "ts-jest": "^26.4.3", 78 | "ts-node": "^9.0.0", 79 | "tslint": "^6.1.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dearraed/starter-node-typescript/898a63072fb4640a5ea2e0d1a1d182fee0e23fe5/src/.DS_Store -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response, NextFunction } from 'express'; 2 | import Logger from './core/Logger'; 3 | import bodyParser from 'body-parser'; 4 | import cors from 'cors'; 5 | import { corsUrl, environment, baseUrl } from './config'; 6 | import './database'; // initialize database 7 | import { NotFoundError, ApiError, InternalError } from './core/ApiError'; 8 | import swaggerUI from 'swagger-ui-express'; 9 | import swaggerJsDoc from 'swagger-jsdoc'; 10 | import routesV1 from './routes/v1'; 11 | 12 | process.on('uncaughtException', (e) => { 13 | Logger.error(e); 14 | }); 15 | 16 | const app = express(); 17 | 18 | app.use(bodyParser.json({ limit: '10mb' })); 19 | app.use(bodyParser.urlencoded({ limit: '10mb', extended: true, parameterLimit: 50000 })); 20 | app.use(cors({ origin: corsUrl, optionsSuccessStatus: 200, credentials: true })); 21 | 22 | const options = { 23 | definition: { 24 | openapi: "3.0.0", 25 | info: { 26 | title: "Library API", 27 | version: "1.0.0", 28 | description: "A simple Express Library API", 29 | }, 30 | servers: [ 31 | { 32 | url: baseUrl, 33 | }, 34 | ], 35 | }, 36 | apis: ["./src/routes/v1/**/*.ts", "./src/routes/v1/*.ts"], 37 | //apis: ["./src/routes/v1/**/*.ts"] 38 | }; 39 | 40 | const specs = swaggerJsDoc(options); 41 | 42 | app.use("/api-docs", swaggerUI.serve, swaggerUI.setup(specs)); 43 | 44 | // Routes 45 | app.use('/api/v1', routesV1); 46 | 47 | // catch 404 and forward to error handler 48 | app.use((req, res, next) => next(new NotFoundError())); 49 | 50 | // Middleware Error Handler 51 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 52 | app.use((err: Error, req: Request, res: Response, next: NextFunction) => { 53 | if (err instanceof ApiError) { 54 | ApiError.handle(err, res); 55 | } else { 56 | if (environment === 'development') { 57 | Logger.error(err); 58 | return res.status(500).send({status: "fail", message : err.message}); 59 | } 60 | ApiError.handle(new InternalError(), res); 61 | } 62 | }); 63 | 64 | export default app; 65 | -------------------------------------------------------------------------------- /src/auth/authUtils.ts: -------------------------------------------------------------------------------- 1 | import { Tokens } from 'app-request'; 2 | import { AuthFailureError, InternalError } from '../core/ApiError'; 3 | import JWT, { JwtPayload } from '../core/JWT'; 4 | import { Types } from 'mongoose'; 5 | import User from '../database/model/User'; 6 | import { tokenInfo } from '../config'; 7 | 8 | export const getAccessToken = (authorization?: string) => { 9 | if (!authorization) throw new AuthFailureError('Invalid Authorization'); 10 | if (!authorization.startsWith('Bearer ')) throw new AuthFailureError('Invalid Authorization'); 11 | return authorization.split(' ')[1]; 12 | }; 13 | 14 | export const validateTokenData = (payload: JwtPayload): boolean => { 15 | if ( 16 | !payload || 17 | !payload.iss || 18 | !payload.sub || 19 | !payload.aud || 20 | !payload.prm || 21 | payload.iss !== tokenInfo.issuer || 22 | payload.aud !== tokenInfo.audience || 23 | !Types.ObjectId.isValid(payload.sub) 24 | ) 25 | throw new AuthFailureError('Invalid Access Token'); 26 | return true; 27 | }; 28 | 29 | export const createTokens = async ( 30 | user: User, 31 | accessTokenKey: string, 32 | refreshTokenKey: string, 33 | ): Promise => { 34 | const accessToken = await JWT.encode( 35 | new JwtPayload( 36 | tokenInfo.issuer, 37 | tokenInfo.audience, 38 | user._id.toString(), 39 | accessTokenKey, 40 | tokenInfo.accessTokenValidityDays, 41 | ), 42 | ); 43 | if (!accessToken) throw new InternalError(); 44 | 45 | const refreshToken = await JWT.encode( 46 | new JwtPayload( 47 | tokenInfo.issuer, 48 | tokenInfo.audience, 49 | user._id.toString(), 50 | refreshTokenKey, 51 | tokenInfo.refreshTokenValidityDays, 52 | ), 53 | ); 54 | 55 | if (!refreshToken) throw new InternalError(); 56 | 57 | return { 58 | accessToken: accessToken, 59 | refreshToken: refreshToken, 60 | } as Tokens; 61 | }; 62 | -------------------------------------------------------------------------------- /src/auth/authentication.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { ProtectedRequest } from 'app-request'; 3 | import UserRepo from '../database/repository/UserRepo'; 4 | import { AuthFailureError, AccessTokenError, TokenExpiredError } from '../core/ApiError'; 5 | import JWT from '../core/JWT'; 6 | import KeystoreRepo from '../database/repository/KeystoreRepo'; 7 | import { Types } from 'mongoose'; 8 | import { getAccessToken, validateTokenData } from './authUtils'; 9 | import validator, { ValidationSource } from '../helpers/validator'; 10 | import schema from './schema'; 11 | import asyncHandler from '../helpers/asyncHandler'; 12 | 13 | const router = express.Router(); 14 | 15 | export default router.use( 16 | validator(schema.auth, ValidationSource.HEADER), 17 | asyncHandler(async (req: ProtectedRequest, res, next) => { 18 | req.accessToken = getAccessToken(req.headers.authorization); // Express headers are auto converted to lowercase 19 | 20 | try { 21 | const payload = await JWT.validate(req.accessToken); 22 | validateTokenData(payload); 23 | 24 | const user = await UserRepo.findById(new Types.ObjectId(payload.sub)); 25 | if (!user) throw new AuthFailureError('User not registered'); 26 | req.user = user; 27 | 28 | const keystore = await KeystoreRepo.findforKey(req.user._id, payload.prm); 29 | if (!keystore) throw new AuthFailureError('Invalid access token'); 30 | req.keystore = keystore; 31 | 32 | return next(); 33 | } catch (e) { 34 | if (e instanceof TokenExpiredError) throw new AccessTokenError(e.message); 35 | throw e; 36 | } 37 | }), 38 | ); 39 | -------------------------------------------------------------------------------- /src/auth/authorization.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { ProtectedRequest } from 'app-request'; 3 | import { AuthFailureError } from '../core/ApiError'; 4 | import RoleRepo from '../database/repository/RoleRepo'; 5 | import asyncHandler from '../helpers/asyncHandler'; 6 | 7 | const router = express.Router(); 8 | 9 | export default router.use( 10 | asyncHandler(async (req: ProtectedRequest, res, next) => { 11 | if (!req.user || !req.user.roles || !req.currentRoleCode) 12 | throw new AuthFailureError('Permission denied'); 13 | 14 | const role = await RoleRepo.findByCode(req.currentRoleCode); 15 | if (!role) throw new AuthFailureError('Permission denied'); 16 | 17 | const validRoles = req.user.roles.filter( 18 | (userRole) => userRole._id.toHexString() === role._id.toHexString(), 19 | ); 20 | 21 | if (!validRoles || validRoles.length == 0) throw new AuthFailureError('Permission denied'); 22 | 23 | return next(); 24 | }), 25 | ); 26 | -------------------------------------------------------------------------------- /src/auth/schema.ts: -------------------------------------------------------------------------------- 1 | import Joi from '@hapi/joi'; 2 | import { JoiAuthBearer } from '../helpers/validator'; 3 | 4 | export default { 5 | apiKey: Joi.object() 6 | .keys({ 7 | 'x-api-key': Joi.string().required(), 8 | }) 9 | .unknown(true), 10 | auth: Joi.object() 11 | .keys({ 12 | authorization: JoiAuthBearer().required(), 13 | }) 14 | .unknown(true), 15 | }; 16 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.seeder = exports.logDirectory = exports.tokenInfo = exports.corsUrl = exports.db = exports.port = exports.environment = void 0; 4 | // Mapper for environment variables 5 | exports.environment = process.env.NODE_ENV; 6 | exports.port = process.env.PORT; 7 | exports.db = { 8 | name: process.env.DB_NAME || '', 9 | host: process.env.DB_HOST || '', 10 | port: process.env.DB_PORT || '' 11 | }; 12 | exports.corsUrl = process.env.CORS_URL; 13 | exports.tokenInfo = { 14 | accessTokenValidityDays: parseInt(process.env.ACCESS_TOKEN_VALIDITY_DAYS || '0'), 15 | refreshTokenValidityDays: parseInt(process.env.REFRESH_TOKEN_VALIDITY_DAYS || '0'), 16 | issuer: process.env.TOKEN_ISSUER || '', 17 | audience: process.env.TOKEN_AUDIENCE || '' 18 | }; 19 | exports.logDirectory = process.env.LOG_DIR; 20 | exports.seeder = { 21 | adminName: process.env.ADMIN_NAME || 'admin2', 22 | adminEmail: process.env.ADMIN_EMAIL || 'admin2@admin.com', 23 | adminPass: process.env.ADMIN_PASS || 'admin21234' 24 | }; 25 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | // Mapper for environment variables 2 | export const environment = process.env.NODE_ENV; 3 | export const port = process.env.PORT; 4 | export const baseUrl = process.env.BASE_URL; 5 | 6 | export const db = { 7 | name: process.env.DB_NAME || '', 8 | host: process.env.DB_HOST || '', 9 | port: process.env.DB_PORT || '' 10 | }; 11 | 12 | export const corsUrl = process.env.CORS_URL; 13 | 14 | export const tokenInfo = { 15 | accessTokenValidityDays: parseInt(process.env.ACCESS_TOKEN_VALIDITY_DAYS || '0'), 16 | refreshTokenValidityDays: parseInt(process.env.REFRESH_TOKEN_VALIDITY_DAYS || '0'), 17 | issuer: process.env.TOKEN_ISSUER || '', 18 | audience: process.env.TOKEN_AUDIENCE || '', 19 | }; 20 | 21 | export const logDirectory = process.env.LOG_DIR; 22 | 23 | export const seeder = { 24 | adminName: process.env.ADMIN_NAME || 'admin2', 25 | adminEmail: process.env.ADMIN_EMAIL || 'admin2@admin.com', 26 | adminPass: process.env.ADMIN_PASS || 'admin21234' 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/controllers/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dearraed/starter-node-typescript/898a63072fb4640a5ea2e0d1a1d182fee0e23fe5/src/controllers/.DS_Store -------------------------------------------------------------------------------- /src/controllers/authController.ts: -------------------------------------------------------------------------------- 1 | import { SuccessResponse, SuccessMsgResponse, TokenRefreshResponse } from '../core/ApiResponse'; 2 | import { ProtectedRequest, RoleRequest } from 'app-request'; 3 | import crypto from 'crypto'; 4 | import JWT from '../core/JWT'; 5 | import User from '../database/model/User'; 6 | import { RoleCode } from '../database/model/Role'; 7 | import UserRepo from '../database/repository/UserRepo'; 8 | import { BadRequestError, AuthFailureError } from '../core/ApiError'; 9 | import KeystoreRepo from '../database/repository/KeystoreRepo'; 10 | import { validateTokenData, createTokens, getAccessToken } from '../auth/authUtils'; 11 | import asyncHandler from '../helpers/asyncHandler'; 12 | import bcryptjs from 'bcryptjs'; 13 | import { Types } from 'mongoose'; 14 | import bcrypt from 'bcryptjs'; 15 | import _ from 'lodash'; 16 | 17 | export const login = asyncHandler(async (req, res) => { 18 | let user = await UserRepo.findByEmail(req.body.email); 19 | if (!user) throw new BadRequestError('User not registered'); 20 | if (!user.password) throw new BadRequestError('Credential not set'); 21 | 22 | const match = await bcryptjs.compare(req.body.password, user.password); 23 | 24 | if (!match) throw new AuthFailureError('Authentication failure'); 25 | 26 | const accessTokenKey = crypto.randomBytes(64).toString('hex'); 27 | const refreshTokenKey = crypto.randomBytes(64).toString('hex'); 28 | 29 | await KeystoreRepo.create(user._id, accessTokenKey, refreshTokenKey); 30 | const tokens = await createTokens(user, accessTokenKey, refreshTokenKey); 31 | const { password, ...filtredUser } = user; 32 | new SuccessResponse('Login Success', { 33 | filtredUser, 34 | tokens: tokens, 35 | }).send(res); 36 | }) 37 | 38 | export const logout = asyncHandler(async (req: ProtectedRequest, res) => { 39 | await KeystoreRepo.remove(req.keystore._id); 40 | new SuccessMsgResponse('Logout success').send(res); 41 | }) 42 | 43 | export const signup = asyncHandler(async (req: RoleRequest, res) => { 44 | let user = await UserRepo.findByEmail(req.body.email); 45 | if (user) throw new BadRequestError('User already registered'); 46 | 47 | const accessTokenKey = crypto.randomBytes(64).toString('hex'); 48 | const refreshTokenKey = crypto.randomBytes(64).toString('hex'); 49 | 50 | const hashedPassword = await bcrypt.hash(req.body.password, 12); 51 | const { user: createdUser, keystore } = await UserRepo.create( 52 | { 53 | ...req.body, 54 | password: hashedPassword 55 | } as User, 56 | accessTokenKey, 57 | refreshTokenKey, 58 | RoleCode.USER, 59 | ); 60 | 61 | const tokens = await createTokens(createdUser, keystore.primaryKey, keystore.secondaryKey); 62 | new SuccessResponse('Signup Successful', { 63 | user: _.pick(createdUser, ['_id', 'name', 'email', 'roles', 'profilePicUrl']), 64 | tokens: tokens, 65 | }).send(res); 66 | }) 67 | 68 | export const refreshToken = asyncHandler(async (req: ProtectedRequest, res) => { 69 | req.accessToken = getAccessToken(req.headers.authorization); // Express headers are auto converted to lowercase 70 | 71 | const accessTokenPayload = await JWT.decode(req.accessToken); 72 | validateTokenData(accessTokenPayload); 73 | 74 | const user = await UserRepo.findById(new Types.ObjectId(accessTokenPayload.sub)); 75 | if (!user) throw new AuthFailureError('User not registered'); 76 | req.user = user; 77 | 78 | const refreshTokenPayload = await JWT.validate(req.body.refreshToken); 79 | validateTokenData(refreshTokenPayload); 80 | 81 | if (accessTokenPayload.sub !== refreshTokenPayload.sub) 82 | throw new AuthFailureError('Invalid access token'); 83 | 84 | const keystore = await KeystoreRepo.find( 85 | req.user._id, 86 | accessTokenPayload.prm, 87 | refreshTokenPayload.prm, 88 | ); 89 | 90 | if (!keystore) throw new AuthFailureError('Invalid access token'); 91 | await KeystoreRepo.remove(keystore._id); 92 | 93 | const accessTokenKey = crypto.randomBytes(64).toString('hex'); 94 | const refreshTokenKey = crypto.randomBytes(64).toString('hex'); 95 | 96 | await KeystoreRepo.create(req.user._id, accessTokenKey, refreshTokenKey); 97 | const tokens = await createTokens(req.user, accessTokenKey, refreshTokenKey); 98 | 99 | new TokenRefreshResponse('Token Issued', tokens.accessToken, tokens.refreshToken).send(res); 100 | }); 101 | -------------------------------------------------------------------------------- /src/controllers/profileController.ts: -------------------------------------------------------------------------------- 1 | import { SuccessResponse } from '../core/ApiResponse'; 2 | import UserRepo from '../database/repository/UserRepo'; 3 | import { ProtectedRequest } from 'app-request'; 4 | import { BadRequestError } from '../core/ApiError'; 5 | import { Types } from 'mongoose'; 6 | import asyncHandler from '../helpers/asyncHandler'; 7 | import _ from 'lodash'; 8 | 9 | 10 | export const getMyProfile = asyncHandler(async (req: ProtectedRequest, res) => { 11 | const user = await UserRepo.findProfileById(req.user._id); 12 | if (!user) throw new BadRequestError('User not registered'); 13 | return new SuccessResponse('success', _.pick(user, ['name', 'profilePicUrl', 'roles'])).send( 14 | res, 15 | ); 16 | }); 17 | 18 | 19 | export const updateProfile = asyncHandler(async (req: ProtectedRequest, res) => { 20 | const user = await UserRepo.findProfileById(req.user._id); 21 | if (!user) throw new BadRequestError('User not registered'); 22 | 23 | if (req.body.name) user.name = req.body.name; 24 | if (req.body.profilePicUrl) user.profilePicUrl = req.body.profilePicUrl; 25 | 26 | await UserRepo.updateInfo(user); 27 | return new SuccessResponse( 28 | 'Profile updated', 29 | _.pick(user, ['name', 'profilePicUrl', 'roles']), 30 | ).send(res); 31 | }); -------------------------------------------------------------------------------- /src/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | import asyncHandler from "../helpers/asyncHandler"; 2 | import { ProtectedRequest } from "app-request"; 3 | import UserRepo from "../database/repository/UserRepo"; 4 | import KeystoreRepo from "../database/repository/KeystoreRepo"; 5 | import { SuccessResponse } from "../core/ApiResponse"; 6 | import { BadRequestError } from '../core/ApiError'; 7 | import { Types } from 'mongoose'; 8 | 9 | 10 | 11 | export const getAllUsers = asyncHandler(async (req: ProtectedRequest, res) => { 12 | const { page, perPage, deleted } = req.query; 13 | const options = { 14 | page: parseInt(page as string, 10) || 1, 15 | limit: parseInt(perPage as string, 10) || 10, 16 | }; 17 | 18 | const users = await UserRepo.findAll(options, req.query, { 19 | isPaging: true, 20 | deleted: deleted == 'true' ? true : false 21 | }); 22 | 23 | const { docs, ...meta } = users; 24 | new SuccessResponse('All users returned successfuly', { 25 | docs, 26 | meta 27 | }).send(res); 28 | 29 | }); 30 | 31 | export const deleteUser = asyncHandler(async (req: ProtectedRequest, res) => { 32 | const user = await UserRepo.findByObj({ _id: new Types.ObjectId(req.params.id), status: true, deletedAt: null}); 33 | if (!user) throw new BadRequestError('User not registered or deleted'); 34 | 35 | await KeystoreRepo.remove(user._id); 36 | let deletedUser = await UserRepo.deleteUser(user); 37 | return new SuccessResponse( 38 | 'User Deleted', 39 | deletedUser, 40 | ).send(res); 41 | }); -------------------------------------------------------------------------------- /src/core/ApiError.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | import { environment } from '../config'; 3 | import { 4 | AuthFailureResponse, 5 | AccessTokenErrorResponse, 6 | InternalErrorResponse, 7 | NotFoundResponse, 8 | BadRequestResponse, 9 | ForbiddenResponse, 10 | } from './ApiResponse'; 11 | 12 | enum ErrorType { 13 | BAD_TOKEN = 'BadTokenError', 14 | TOKEN_EXPIRED = 'TokenExpiredError', 15 | UNAUTHORIZED = 'AuthFailureError', 16 | ACCESS_TOKEN = 'AccessTokenError', 17 | INTERNAL = 'InternalError', 18 | NOT_FOUND = 'NotFoundError', 19 | NO_ENTRY = 'NoEntryError', 20 | NO_DATA = 'NoDataError', 21 | BAD_REQUEST = 'BadRequestError', 22 | FORBIDDEN = 'ForbiddenError', 23 | } 24 | 25 | export abstract class ApiError extends Error { 26 | constructor(public type: ErrorType, public message: string = 'error') { 27 | super(type); 28 | } 29 | 30 | public static handle(err: ApiError, res: Response): Response { 31 | switch (err.type) { 32 | case ErrorType.BAD_TOKEN: 33 | case ErrorType.TOKEN_EXPIRED: 34 | case ErrorType.UNAUTHORIZED: 35 | return new AuthFailureResponse(err.message).send(res); 36 | case ErrorType.ACCESS_TOKEN: 37 | return new AccessTokenErrorResponse(err.message).send(res); 38 | case ErrorType.INTERNAL: 39 | return new InternalErrorResponse(err.message).send(res); 40 | case ErrorType.NOT_FOUND: 41 | case ErrorType.NO_ENTRY: 42 | case ErrorType.NO_DATA: 43 | return new NotFoundResponse(err.message).send(res); 44 | case ErrorType.BAD_REQUEST: 45 | return new BadRequestResponse(err.message).send(res); 46 | case ErrorType.FORBIDDEN: 47 | return new ForbiddenResponse(err.message).send(res); 48 | default: { 49 | let message = err.message; 50 | // Do not send failure message in production as it may send sensitive data 51 | if (environment === 'production') message = 'Something wrong happened.'; 52 | return new InternalErrorResponse(message).send(res); 53 | } 54 | } 55 | } 56 | } 57 | 58 | export class AuthFailureError extends ApiError { 59 | constructor(message = 'Invalid Credentials') { 60 | super(ErrorType.UNAUTHORIZED, message); 61 | } 62 | } 63 | 64 | export class InternalError extends ApiError { 65 | constructor(message = 'Internal error') { 66 | super(ErrorType.INTERNAL, message); 67 | } 68 | } 69 | 70 | export class BadRequestError extends ApiError { 71 | constructor(message = 'Bad Request') { 72 | super(ErrorType.BAD_REQUEST, message); 73 | } 74 | } 75 | 76 | export class NotFoundError extends ApiError { 77 | constructor(message = 'Not Found') { 78 | super(ErrorType.NOT_FOUND, message); 79 | } 80 | } 81 | 82 | export class ForbiddenError extends ApiError { 83 | constructor(message = 'Permission denied') { 84 | super(ErrorType.FORBIDDEN, message); 85 | } 86 | } 87 | 88 | export class NoEntryError extends ApiError { 89 | constructor(message = "Entry don't exists") { 90 | super(ErrorType.NO_ENTRY, message); 91 | } 92 | } 93 | 94 | export class BadTokenError extends ApiError { 95 | constructor(message = 'Token is not valid') { 96 | super(ErrorType.BAD_TOKEN, message); 97 | } 98 | } 99 | 100 | export class TokenExpiredError extends ApiError { 101 | constructor(message = 'Token is expired') { 102 | super(ErrorType.TOKEN_EXPIRED, message); 103 | } 104 | } 105 | 106 | export class NoDataError extends ApiError { 107 | constructor(message = 'No data available') { 108 | super(ErrorType.NO_DATA, message); 109 | } 110 | } 111 | 112 | export class AccessTokenError extends ApiError { 113 | constructor(message = 'Invalid access token') { 114 | super(ErrorType.ACCESS_TOKEN, message); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/core/ApiResponse.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prettier/prettier */ 2 | import { Response } from 'express'; 3 | 4 | enum ResponseStatus { 5 | SUCCESS = 200, 6 | BAD_REQUEST = 400, 7 | UNAUTHORIZED = 401, 8 | FORBIDDEN = 403, 9 | NOT_FOUND = 404, 10 | INTERNAL_ERROR = 500, 11 | } 12 | 13 | abstract class ApiResponse { 14 | constructor( 15 | protected statusCode: ResponseStatus, 16 | protected status: ResponseStatus, 17 | protected message: string, 18 | ) {} 19 | 20 | protected prepare(res: Response, response: T): Response { 21 | return res.status(this.status).json(ApiResponse.sanitize(response)); 22 | } 23 | 24 | public send(res: Response): Response { 25 | return this.prepare(res, this); 26 | } 27 | 28 | private static sanitize(response: T): T { 29 | const clone: T = {} as T; 30 | Object.assign(clone, response); 31 | // @ts-ignore 32 | delete clone.status; 33 | for (const i in clone) if (typeof clone[i] === 'undefined') delete clone[i]; 34 | return clone; 35 | } 36 | } 37 | 38 | export class AuthFailureResponse extends ApiResponse { 39 | constructor(message = 'Authentication Failure') { 40 | super(ResponseStatus.UNAUTHORIZED, ResponseStatus.UNAUTHORIZED, message); 41 | } 42 | } 43 | 44 | export class NotFoundResponse extends ApiResponse { 45 | private url: string | undefined; 46 | 47 | constructor(message = 'Not Found') { 48 | super(ResponseStatus.NOT_FOUND, ResponseStatus.NOT_FOUND, message); 49 | } 50 | 51 | send(res: Response): Response { 52 | this.url = res.req?.originalUrl; 53 | return super.prepare(res, this); 54 | } 55 | } 56 | 57 | export class ForbiddenResponse extends ApiResponse { 58 | constructor(message = 'Forbidden') { 59 | super(ResponseStatus.FORBIDDEN, ResponseStatus.FORBIDDEN, message); 60 | } 61 | } 62 | 63 | export class BadRequestResponse extends ApiResponse { 64 | constructor(message = 'Bad Parameters') { 65 | super(ResponseStatus.BAD_REQUEST, ResponseStatus.BAD_REQUEST, message); 66 | } 67 | } 68 | 69 | export class InternalErrorResponse extends ApiResponse { 70 | constructor(message = 'Internal Error') { 71 | super(ResponseStatus.INTERNAL_ERROR, ResponseStatus.INTERNAL_ERROR, message); 72 | } 73 | } 74 | 75 | export class SuccessMsgResponse extends ApiResponse { 76 | constructor(message: string) { 77 | super(ResponseStatus.SUCCESS, ResponseStatus.SUCCESS, message); 78 | } 79 | } 80 | 81 | export class FailureMsgResponse extends ApiResponse { 82 | constructor(message: string) { 83 | super(ResponseStatus.SUCCESS, ResponseStatus.SUCCESS, message); 84 | } 85 | } 86 | 87 | export class SuccessResponse extends ApiResponse { 88 | constructor(message: string, private data: T) { 89 | super(ResponseStatus.SUCCESS, ResponseStatus.SUCCESS, message); 90 | } 91 | 92 | send(res: Response): Response { 93 | return super.prepare>(res, this); 94 | } 95 | } 96 | 97 | export class AccessTokenErrorResponse extends ApiResponse { 98 | private instruction = 'refresh_token'; 99 | 100 | constructor(message = 'Access token invalid') { 101 | super(ResponseStatus.UNAUTHORIZED, ResponseStatus.UNAUTHORIZED, message); 102 | } 103 | 104 | send(res: Response): Response { 105 | res.setHeader('instruction', this.instruction); 106 | return super.prepare(res, this); 107 | } 108 | } 109 | 110 | export class TokenRefreshResponse extends ApiResponse { 111 | constructor(message: string, private accessToken: string, private refreshToken: string) { 112 | super(ResponseStatus.SUCCESS, ResponseStatus.SUCCESS, message); 113 | } 114 | 115 | send(res: Response): Response { 116 | return super.prepare(res, this); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/core/JWT.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { readFile } from 'fs'; 3 | import { promisify } from 'util'; 4 | import { sign, verify } from 'jsonwebtoken'; 5 | import { InternalError, BadTokenError, TokenExpiredError } from './ApiError'; 6 | import Logger from './Logger'; 7 | 8 | /* 9 | * issuer  — Software organization who issues the token. 10 | * subject  — Intended user of the token. 11 | * audience  — Basically identity of the intended recipient of the token. 12 | * expiresIn — Expiration time after which the token will be invalid. 13 | * algorithm  — Encryption algorithm to be used to protect the token. 14 | */ 15 | 16 | export default class JWT { 17 | private static readPublicKey(): Promise { 18 | return promisify(readFile)(path.join(__dirname, '../../keys/public.pem'), 'utf8'); 19 | } 20 | 21 | private static readPrivateKey(): Promise { 22 | return promisify(readFile)(path.join(__dirname, '../../keys/private.pem'), 'utf8'); 23 | } 24 | 25 | public static async encode(payload: JwtPayload): Promise { 26 | const cert = await this.readPrivateKey(); 27 | if (!cert) throw new InternalError('Token generation failure'); 28 | // @ts-ignore 29 | return promisify(sign)({ ...payload }, cert, { algorithm: 'RS256' }); 30 | } 31 | 32 | /** 33 | * This method checks the token and returns the decoded data when token is valid in all respect 34 | */ 35 | public static async validate(token: string): Promise { 36 | const cert = await this.readPublicKey(); 37 | try { 38 | // @ts-ignore 39 | return (await promisify(verify)(token, cert)) as JwtPayload; 40 | } catch (e: any) { 41 | Logger.debug(e); 42 | if (e && e.name === 'TokenExpiredError') throw new TokenExpiredError(); 43 | // throws error if the token has not been encrypted by the private key 44 | throw new BadTokenError(); 45 | } 46 | } 47 | 48 | /** 49 | * Returns the decoded payload if the signature is valid even if it is expired 50 | */ 51 | public static async decode(token: string): Promise { 52 | const cert = await this.readPublicKey(); 53 | try { 54 | // @ts-ignore 55 | return (await promisify(verify)(token, cert, { ignoreExpiration: true })) as JwtPayload; 56 | } catch (e) { 57 | Logger.debug(e); 58 | throw new BadTokenError(); 59 | } 60 | } 61 | } 62 | 63 | export class JwtPayload { 64 | aud: string; 65 | sub: string; 66 | iss: string; 67 | iat: number; 68 | exp: number; 69 | prm: string; 70 | 71 | constructor(issuer: string, audience: string, subject: string, param: string, validity: number) { 72 | this.iss = issuer; 73 | this.aud = audience; 74 | this.sub = subject; 75 | this.iat = Math.floor(Date.now() / 1000); 76 | this.exp = this.iat + validity * 24 * 60 * 60; 77 | this.prm = param; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/core/Logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, transports, format } from 'winston'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import DailyRotateFile from 'winston-daily-rotate-file'; 5 | import { environment, logDirectory } from '../config'; 6 | 7 | let dir = logDirectory; 8 | if (!dir) dir = path.resolve('logs'); 9 | 10 | // create directory if it is not present 11 | if (!fs.existsSync(dir)) { 12 | // Create the directory if it does not exist 13 | fs.mkdirSync(dir); 14 | } 15 | 16 | const logLevel = environment === 'development' ? 'debug' : 'warn'; 17 | 18 | const options = { 19 | file: { 20 | level: logLevel, 21 | filename: dir + '/%DATE%.log', 22 | datePattern: 'YYYY-MM-DD', 23 | zippedArchive: true, 24 | timestamp: true, 25 | handleExceptions: true, 26 | humanReadableUnhandledException: true, 27 | prettyPrint: true, 28 | json: true, 29 | maxSize: '20m', 30 | colorize: true, 31 | maxFiles: '14d', 32 | }, 33 | }; 34 | 35 | export default createLogger({ 36 | transports: [ 37 | new transports.Console({ 38 | level: logLevel, 39 | format: format.combine(format.errors({ stack: true }), format.prettyPrint()), 40 | }), 41 | ], 42 | exceptionHandlers: [new DailyRotateFile(options.file)], 43 | exitOnError: false, // do not exit on handled exceptions 44 | }); 45 | -------------------------------------------------------------------------------- /src/database/index.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import Logger from '../core/Logger'; 3 | import { db } from '../config'; 4 | 5 | // Build the connection string 6 | const dbURI = `mongodb://${db.host}:${db.port}/${ 7 | db.name 8 | }`; 9 | 10 | Logger.debug(dbURI); 11 | 12 | // Create the database connection 13 | mongoose 14 | .connect(dbURI) 15 | .then(() => { 16 | Logger.info('Mongoose connection done'); 17 | }) 18 | .catch((e) => { 19 | Logger.info('Mongoose connection error'); 20 | Logger.error(e); 21 | }); 22 | 23 | // CONNECTION EVENTS 24 | // When successfully connected 25 | mongoose.connection.on('connected', () => { 26 | Logger.info('Mongoose default connection open to ' + dbURI); 27 | }); 28 | 29 | // If the connection throws an error 30 | mongoose.connection.on('error', (err) => { 31 | Logger.error('Mongoose default connection error: ' + err); 32 | }); 33 | 34 | // When the connection is disconnected 35 | mongoose.connection.on('disconnected', () => { 36 | Logger.info('Mongoose default connection disconnected'); 37 | }); 38 | 39 | // If the Node process ends, close the Mongoose connection 40 | process.on('SIGINT', () => { 41 | mongoose.connection.close(() => { 42 | Logger.info('Mongoose default connection disconnected through app termination'); 43 | process.exit(0); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/database/model/Keystore.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prettier/prettier */ 2 | import { Schema, model, Document } from 'mongoose'; 3 | import User from './User'; 4 | 5 | export const DOCUMENT_NAME = 'Keystore'; 6 | export const COLLECTION_NAME = 'keystores'; 7 | 8 | export default interface Keystore extends Document { 9 | client: User; 10 | primaryKey: string; 11 | secondaryKey: string; 12 | status?: boolean; 13 | createdAt?: Date; 14 | updatedAt?: Date; 15 | } 16 | 17 | const schema = new Schema( 18 | { 19 | client: { 20 | type: Schema.Types.ObjectId, 21 | required: true, 22 | ref: 'User', 23 | }, 24 | primaryKey: { 25 | type: Schema.Types.String, 26 | required: true, 27 | }, 28 | secondaryKey: { 29 | type: Schema.Types.String, 30 | required: true, 31 | }, 32 | status: { 33 | type: Schema.Types.Boolean, 34 | default: true, 35 | }, 36 | createdAt: { 37 | type: Date, 38 | required: true, 39 | select: false, 40 | }, 41 | updatedAt: { 42 | type: Date, 43 | required: true, 44 | select: false, 45 | }, 46 | }, 47 | { 48 | versionKey: false, 49 | }, 50 | ); 51 | 52 | schema.index({ client: 1, primaryKey: 1 }); 53 | schema.index({ client: 1, primaryKey: 1, secondaryKey: 1 }); 54 | 55 | export const KeystoreModel = model(DOCUMENT_NAME, schema, COLLECTION_NAME); 56 | -------------------------------------------------------------------------------- /src/database/model/Role.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, Document } from 'mongoose'; 2 | 3 | export const DOCUMENT_NAME = 'Role'; 4 | export const COLLECTION_NAME = 'roles'; 5 | 6 | export const enum RoleCode { 7 | ADMIN = 'ADMIN', 8 | USER = 'USER' 9 | } 10 | 11 | export default interface Role extends Document { 12 | code: string; 13 | status?: boolean; 14 | createdAt?: Date; 15 | updatedAt?: Date; 16 | } 17 | 18 | const schema = new Schema( 19 | { 20 | code: { 21 | type: Schema.Types.String, 22 | required: true, 23 | enum: [RoleCode.ADMIN, RoleCode.USER], 24 | }, 25 | status: { 26 | type: Schema.Types.Boolean, 27 | default: true, 28 | }, 29 | createdAt: { 30 | type: Date, 31 | required: true, 32 | select: false, 33 | }, 34 | updatedAt: { 35 | type: Date, 36 | required: true, 37 | select: false, 38 | }, 39 | }, 40 | { 41 | versionKey: false, 42 | }, 43 | ); 44 | 45 | export const RoleModel = model(DOCUMENT_NAME, schema, COLLECTION_NAME); 46 | -------------------------------------------------------------------------------- /src/database/model/User.ts: -------------------------------------------------------------------------------- 1 | import { model, Schema, Document } from 'mongoose'; 2 | import Role from './Role'; 3 | import { mongoosePagination, Pagination } from 'mongoose-paginate-ts'; 4 | import bcrypt from 'bcryptjs'; 5 | 6 | 7 | export const DOCUMENT_NAME = 'User'; 8 | export const COLLECTION_NAME = 'users'; 9 | 10 | export default interface User extends Document { 11 | name: string; 12 | lastName: string, 13 | email?: string; 14 | password: string; 15 | profilePicUrl?: string; 16 | roles: Role[]; 17 | verified?: boolean; 18 | status?: boolean; 19 | createdAt?: Date; 20 | updatedAt?: Date; 21 | } 22 | 23 | const schema = new Schema( 24 | { 25 | name: Schema.Types.String, 26 | lastame: Schema.Types.String, 27 | email: { 28 | type: Schema.Types.String, 29 | unique: true, 30 | trim: true, 31 | }, 32 | password: { 33 | type: Schema.Types.String, 34 | select: false, 35 | }, 36 | profilePicUrl: { 37 | type: Schema.Types.String, 38 | trim: true, 39 | }, 40 | roles: { 41 | type: [ 42 | { 43 | type: Schema.Types.ObjectId, 44 | ref: 'Role', 45 | }, 46 | ], 47 | select: false, 48 | }, 49 | verified: { 50 | type: Schema.Types.Boolean, 51 | default: false, 52 | }, 53 | status: { 54 | type: Schema.Types.Boolean, 55 | default: true, 56 | }, 57 | createdAt: { 58 | type: Date, 59 | required: true, 60 | select: false, 61 | }, 62 | updatedAt: { 63 | type: Date, 64 | required: true, 65 | select: false, 66 | }, 67 | deletedAt: { 68 | type: Date, 69 | select: true, 70 | }, 71 | }, 72 | { 73 | versionKey: false, 74 | }, 75 | ); 76 | 77 | schema.plugin(mongoosePagination); 78 | schema.pre("save", async function (this: User, next) { 79 | if(this.isModified("email")) 80 | this.email = this.email?.toLocaleLowerCase(); 81 | 82 | if (!this.isModified("password")) return next(); 83 | this.password = await bcrypt.hash(this.password, 12); 84 | next(); 85 | }); 86 | 87 | schema.methods.comparePassword = async function ( 88 | password: string 89 | ): Promise { 90 | return await bcrypt.compare(password, this.password); 91 | }; 92 | 93 | export const UserModel = model>(DOCUMENT_NAME, schema, COLLECTION_NAME); 94 | -------------------------------------------------------------------------------- /src/database/repository/KeystoreRepo.ts: -------------------------------------------------------------------------------- 1 | import Keystore, { KeystoreModel } from '../model/Keystore'; 2 | import { Types } from 'mongoose'; 3 | import User from '../model/User'; 4 | 5 | export default class KeystoreRepo { 6 | public static findforKey(client: User, key: string): Promise { 7 | return KeystoreModel.findOne({ client: client, primaryKey: key, status: true }).exec(); 8 | } 9 | 10 | public static remove(id: Types.ObjectId): Promise { 11 | return KeystoreModel.findByIdAndRemove(id).lean().exec(); 12 | } 13 | 14 | public static find( 15 | client: User, 16 | primaryKey: string, 17 | secondaryKey: string, 18 | ): Promise { 19 | return KeystoreModel.findOne({ 20 | client: client, 21 | primaryKey: primaryKey, 22 | secondaryKey: secondaryKey, 23 | }) 24 | .lean() 25 | .exec(); 26 | } 27 | 28 | public static async create( 29 | client: User, 30 | primaryKey: string, 31 | secondaryKey: string, 32 | ): Promise { 33 | const now = new Date(); 34 | const keystore = await KeystoreModel.create({ 35 | client: client, 36 | primaryKey: primaryKey, 37 | secondaryKey: secondaryKey, 38 | createdAt: now, 39 | updatedAt: now, 40 | } as Keystore); 41 | return keystore.toObject(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/database/repository/RoleRepo.ts: -------------------------------------------------------------------------------- 1 | import Role, { RoleModel } from '../model/Role'; 2 | 3 | export default class RoleRepo { 4 | public static findByCode(code: string): Promise { 5 | return RoleModel.findOne({ code: code, status: true }).lean().exec(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/database/repository/UserRepo.ts: -------------------------------------------------------------------------------- 1 | import User, { UserModel } from '../model/User'; 2 | import Role, { RoleModel } from '../model/Role'; 3 | import { InternalError } from '../../core/ApiError'; 4 | import { Types } from 'mongoose'; 5 | import KeystoreRepo from './KeystoreRepo'; 6 | import Keystore from '../model/Keystore'; 7 | import { PaginationModel } from 'mongoose-paginate-ts'; 8 | import APIFeatures from '../../helpers/apiFeatures'; 9 | import { ApiOptions } from "app-request"; 10 | import uuid4 from "uuid4"; 11 | 12 | 13 | 14 | type pagingObj = { 15 | limit: number, 16 | page: number 17 | } 18 | export default class UserRepo { 19 | // contains critical information of the user 20 | public static findById(id: Types.ObjectId): Promise { 21 | return UserModel.findOne({ _id: id, status: true }) 22 | .select('+email +password +roles') 23 | .populate({ 24 | path: 'roles', 25 | match: { status: true }, 26 | }) 27 | .lean() 28 | .exec(); 29 | } 30 | 31 | public static findByEmail(email: string): Promise { 32 | return UserModel.findOne({ email: email, status: true }) 33 | .select('+email +password +roles -verified -status') 34 | .populate({ 35 | path: 'roles', 36 | match: { status: true }, 37 | select: { code: 1 }, 38 | }) 39 | .lean() 40 | .exec(); 41 | } 42 | 43 | public static findProfileById(id: Types.ObjectId): Promise { 44 | return UserModel.findOne({ _id: id, status: true }) 45 | .select('+name +lastname +roles +email') 46 | .populate({ 47 | path: 'roles', 48 | match: { status: true }, 49 | select: { code: 1 }, 50 | }) 51 | .lean() 52 | .exec(); 53 | } 54 | 55 | public static findByObj(obj: object): Promise { 56 | return UserModel.findOne(obj) 57 | .select('+roles +email') 58 | .populate({ 59 | path: 'roles', 60 | match: { status: true }, 61 | select: { code: 1 }, 62 | }) 63 | .lean() 64 | .exec(); 65 | } 66 | 67 | public static async findAll(paging: pagingObj, query: object, apiOptions: ApiOptions): Promise> { 68 | let findAllQuery = apiOptions.deleted 69 | ? UserModel.find({ deletedAt: { $ne: null} }) 70 | : UserModel.find({ deletedAt: null }); 71 | 72 | const features = new APIFeatures( 73 | findAllQuery, 74 | query 75 | ) 76 | .filter() 77 | .sort() 78 | .limitFields() 79 | .search(["name", "email"]); 80 | 81 | const options = { 82 | query: features.query, 83 | limit: paging.limit ? paging.limit : null, 84 | page: paging.page ? paging.page : null, 85 | }; 86 | 87 | return await UserModel.paginate(options) as PaginationModel; 88 | 89 | } 90 | 91 | public static async create( 92 | user: User, 93 | accessTokenKey: string, 94 | refreshTokenKey: string, 95 | roleCode: string, 96 | ): Promise<{ user: User; keystore: Keystore }> { 97 | const now = new Date(); 98 | 99 | const role = await RoleModel.findOne({ code: roleCode }) 100 | .select('+email +password') 101 | .lean() 102 | .exec(); 103 | if (!role) throw new InternalError('Role must be defined'); 104 | 105 | user.roles = [role._id]; 106 | user.createdAt = user.updatedAt = now; 107 | const createdUser = await UserModel.create(user); 108 | const keystore = await KeystoreRepo.create(createdUser._id, accessTokenKey, refreshTokenKey); 109 | return { user: createdUser.toObject(), keystore: keystore }; 110 | } 111 | 112 | public static async update( 113 | user: User, 114 | accessTokenKey: string, 115 | refreshTokenKey: string, 116 | ): Promise<{ user: User; keystore: Keystore }> { 117 | user.updatedAt = new Date(); 118 | await UserModel.updateOne({ _id: user._id }, { $set: { ...user } }) 119 | .lean() 120 | .exec(); 121 | const keystore = await KeystoreRepo.create(user._id, accessTokenKey, refreshTokenKey); 122 | return { user: user, keystore: keystore }; 123 | } 124 | 125 | public static updateInfo(user: User): Promise { 126 | user.updatedAt = new Date(); 127 | return UserModel.updateOne({ _id: user._id }, { $set: { ...user } }) 128 | .lean() 129 | .exec(); 130 | } 131 | 132 | public static async deleteUser(user: User): Promise { 133 | user.updatedAt = new Date(); 134 | let email = user.email as string; 135 | let regex = '^old[0-9]+' + email; 136 | const deletedUsers = await UserModel.count({ email: { $regex: regex } }); 137 | return UserModel.findByIdAndUpdate( user._id, { $set: { email: `old${deletedUsers}${email}`, deletedAt: Date.now() } }, { new: true }) 138 | .exec(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/helpers/apiFeatures.ts: -------------------------------------------------------------------------------- 1 | class APIFeatures { 2 | query: any; 3 | queryString: any; 4 | constructor(query: any, queryString: any) { 5 | this.query = query; 6 | this.queryString = queryString; 7 | } 8 | 9 | filter() { 10 | const queryObj = { ...this.queryString }; 11 | const excludedFields = ['page', 'sort', 'limit', 'fields', 'deleted']; 12 | excludedFields.forEach(el => delete queryObj[el]); 13 | 14 | let queryStr = JSON.stringify(queryObj); 15 | 16 | queryStr = queryStr.replace(/\b(gte|gt|lte|lt)\b/g, match => `$${match}`); 17 | 18 | queryStr = JSON.parse(queryStr); 19 | 20 | if(Object.keys(queryStr).length){ 21 | let queryOption = Object.keys(queryStr).map((field: any) => ({ 22 | [field]: { $regex: queryStr[field], $options: 'i' }, 23 | })); 24 | 25 | this.query = this.query.find({ $or: queryOption }); 26 | } 27 | 28 | return this; 29 | } 30 | 31 | sort() { 32 | if (this.queryString.sort) { 33 | const sortBy = this.queryString.sort.split(',').join(' '); 34 | this.query = this.query.sort(sortBy); 35 | } else { 36 | this.query = this.query.sort('-createdAt'); 37 | } 38 | 39 | return this; 40 | } 41 | 42 | limitFields() { 43 | if (this.queryString.fields) { 44 | const fields = this.queryString.fields.split(',').join(' '); 45 | this.query = this.query.select(fields); 46 | } else { 47 | this.query = this.query.select('-__v'); 48 | } 49 | 50 | return this; 51 | } 52 | 53 | paginate() { 54 | const page = this.queryString.page * 1 || 1; 55 | const limit = this.queryString.limit * 1 || 100; 56 | const skip = (page - 1) * limit; 57 | 58 | this.query = this.query.skip(skip).limit(limit); 59 | 60 | return this; 61 | } 62 | 63 | search(searchFields: any) { 64 | if (this.queryString?.search) { 65 | const queryOption = searchFields.map((field: any) => ({ 66 | [field]: { $regex: this.queryString.search, $options: 'i' }, 67 | })); 68 | 69 | this.query = this.query.find({ $or: queryOption }); 70 | } 71 | return this; 72 | } 73 | } 74 | export default APIFeatures; 75 | -------------------------------------------------------------------------------- /src/helpers/asyncHandler.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | 3 | type AsyncFunction = (req: Request, res: Response, next: NextFunction) => Promise; 4 | 5 | export default (execution: AsyncFunction) => (req: Request, res: Response, next: NextFunction) => { 6 | execution(req, res, next).catch(next); 7 | }; 8 | -------------------------------------------------------------------------------- /src/helpers/role.ts: -------------------------------------------------------------------------------- 1 | import { RoleCode } from '../database/model/Role'; 2 | import { RoleRequest } from 'app-request'; 3 | import { Response, NextFunction } from 'express'; 4 | 5 | export default (roleCode: RoleCode) => (req: RoleRequest, res: Response, next: NextFunction) => { 6 | req.currentRoleCode = roleCode; 7 | next(); 8 | }; 9 | -------------------------------------------------------------------------------- /src/helpers/seeder.ts: -------------------------------------------------------------------------------- 1 | import '../database'; 2 | import { RoleCode, RoleModel } from '../database/model/Role'; 3 | import User, { UserModel } from '../database/model/User'; 4 | import bcryptjs from 'bcryptjs'; 5 | import { seeder } from '../config'; 6 | import mongoose from 'mongoose'; 7 | 8 | 9 | let seed = async () => { 10 | await seedDelete(); 11 | await seedRoles(); 12 | await seedAdmin(); 13 | process.exit(1); 14 | } 15 | 16 | let seedRoles = async () => { 17 | let roles = await RoleModel.find(); 18 | if(roles.length == 0){ 19 | await RoleModel.insertMany([ 20 | { code: RoleCode.ADMIN, status: true, createdAt: new Date(), updatedAt: new Date() }, 21 | { code: RoleCode.USER, status: true, createdAt: new Date(), updatedAt: new Date() } 22 | ]); 23 | console.log("Roles inserted successfully"); 24 | 25 | }else{ 26 | console.log("Roles already exists !") 27 | } 28 | } 29 | 30 | let seedAdmin = async () => { 31 | let roleAdmin = await RoleModel.findOne({code : RoleCode.ADMIN }); 32 | 33 | if(roleAdmin){ 34 | let admins = await UserModel.find({roles: roleAdmin._id}); 35 | 36 | if(admins.length > 0){ 37 | console.log("Admin user exist"); 38 | }else{ 39 | try{ 40 | let password = await bcryptjs.hash(seeder.adminPass, 12); 41 | let admin = { roles: [ roleAdmin ], verified : true, status : true, name : seeder.adminName, email : seeder.adminEmail, password : password, createdAt: new Date(), updatedAt: new Date() }; 42 | 43 | await UserModel.create(admin as User); 44 | 45 | console.log("Admin user added sucessfuly !"); 46 | }catch(error){ 47 | console.log("error : ", error); 48 | } 49 | } 50 | 51 | 52 | }else{ 53 | console.log("Role admin inexistant !") 54 | } 55 | } 56 | 57 | let seedDelete = async () => { 58 | // let collections = mongoose.models; 59 | // console.log(collections); 60 | // let keys = Object.keys(collections); 61 | // keys.forEach(async element => { 62 | // await collections[element].deleteMany({}).exec(); 63 | // }); 64 | 65 | const collections = mongoose.modelNames(); 66 | const deletedCollections = collections.map((collection) => 67 | mongoose.models[collection].deleteMany({}) 68 | ); 69 | await Promise.all(deletedCollections); 70 | console.log("Collections empty successfuly !"); 71 | 72 | } 73 | 74 | seed(); 75 | -------------------------------------------------------------------------------- /src/helpers/validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from '@hapi/joi'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | import Logger from '../core/Logger'; 4 | import { BadRequestError } from '../core/ApiError'; 5 | import { Types } from 'mongoose'; 6 | 7 | export enum ValidationSource { 8 | BODY = 'body', 9 | HEADER = 'headers', 10 | QUERY = 'query', 11 | PARAM = 'params', 12 | } 13 | 14 | export const JoiObjectId = () => 15 | Joi.string().custom((value: string, helpers) => { 16 | if (!Types.ObjectId.isValid(value)) return helpers.error('any.invalid'); 17 | return value; 18 | }, 'Object Id Validation'); 19 | 20 | export const JoiUrlEndpoint = () => 21 | Joi.string().custom((value: string, helpers) => { 22 | if (value.includes('://')) return helpers.error('any.invalid'); 23 | return value; 24 | }, 'Url Endpoint Validation'); 25 | 26 | export const JoiAuthBearer = () => 27 | Joi.string().custom((value: string, helpers) => { 28 | if (!value.startsWith('Bearer ')) return helpers.error('any.invalid'); 29 | if (!value.split(' ')[1]) return helpers.error('any.invalid'); 30 | return value; 31 | }, 'Authorization Header Validation'); 32 | 33 | export default (schema: Joi.ObjectSchema, source: ValidationSource = ValidationSource.BODY) => ( 34 | req: Request, 35 | res: Response, 36 | next: NextFunction, 37 | ) => { 38 | try { 39 | const { error } = schema.validate(req[source]); 40 | 41 | if (!error) return next(); 42 | 43 | const { details } = error; 44 | const message = details.map((i) => i.message.replace(/['"]+/g, '')).join(','); 45 | Logger.error(message); 46 | 47 | next(new BadRequestError(message)); 48 | } catch (error) { 49 | next(error); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /src/routes/v1/access/access.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import validator, { ValidationSource } from '../../../helpers/validator'; 3 | import schema from './schema'; 4 | import { login, logout, refreshToken, signup } from '../../../controllers/authController'; 5 | import authentication from '../../../auth/authentication'; 6 | 7 | const router = express.Router(); 8 | 9 | /** 10 | * @swagger 11 | * tags: 12 | * name: Access 13 | * description: The Access managing API 14 | */ 15 | 16 | /** 17 | * @swagger 18 | * /login: 19 | * post: 20 | * summary: User login 21 | * requestBody: 22 | * required: true 23 | * content: 24 | * application/json: 25 | * schema: 26 | * $ref: '#/components/schemas/Login' 27 | * tags: [Access] 28 | * responses: 29 | * 200: 30 | * description: Login 31 | * content: 32 | * application/json: 33 | * schema: 34 | * type: object 35 | * properties: 36 | * user: 37 | * $ref: '#/components/schemas/User' 38 | * tokens: 39 | * $ref: '#/components/schemas/Tokens' 40 | * 41 | */ 42 | router.post( 43 | '/login', 44 | validator(schema.userCredential), 45 | login 46 | ); 47 | 48 | /** 49 | * @swagger 50 | * /signup: 51 | * post: 52 | * summary: Register 53 | * requestBody: 54 | * required: true 55 | * content: 56 | * application/json: 57 | * schema: 58 | * $ref: '#/components/schemas/CreateUser' 59 | * tags: [Access] 60 | * responses: 61 | * 200: 62 | * description: The list of the register 63 | * content: 64 | * application/json: 65 | * schema: 66 | * type: object 67 | * properties: 68 | * user: 69 | * $ref: '#/components/schemas/User' 70 | * tokens: 71 | * $ref: '#/components/schemas/Tokens' 72 | * 73 | */ 74 | 75 | router.post( 76 | '/signup', 77 | validator(schema.signup), 78 | signup 79 | ); 80 | 81 | /** 82 | * @swagger 83 | * /refresh: 84 | * post: 85 | * summary: Register 86 | * tags: [Access] 87 | * requestBody: 88 | * required: true 89 | * content: 90 | * application/json: 91 | * schema: 92 | * $ref: '#/components/schemas/RefreshToken' 93 | * responses: 94 | * 200: 95 | * description: The list of the register 96 | * content: 97 | * application/json: 98 | * schema: 99 | * $ref: '#/components/schemas/Tokens' 100 | * security: 101 | * - bearerAuth: [] 102 | * 103 | */ 104 | 105 | /*-------------------------------------------------------------------------*/ 106 | // Below all APIs are private APIs protected for Access Token 107 | router.use('/', authentication); 108 | /*-------------------------------------------------------------------------*/ 109 | 110 | router.post( 111 | '/refresh', 112 | validator(schema.auth, ValidationSource.HEADER), 113 | validator(schema.refreshToken), 114 | refreshToken 115 | ); 116 | 117 | /** 118 | * @swagger 119 | * /logout: 120 | * delete: 121 | * summary: Logout 122 | * tags: [Access] 123 | * responses: 124 | * 200: 125 | * content: 126 | * application/json: 127 | * schema: 128 | * type: object 129 | * properties: 130 | * message: 131 | * default: Logout success 132 | * 133 | * security: 134 | * - bearerAuth: [] 135 | * 136 | */ 137 | router.delete( 138 | '/logout', 139 | logout 140 | ); 141 | 142 | export default router; 143 | -------------------------------------------------------------------------------- /src/routes/v1/access/schema.ts: -------------------------------------------------------------------------------- 1 | import Joi from '@hapi/joi'; 2 | import { JoiAuthBearer } from '../../../helpers/validator'; 3 | 4 | /** 5 | * @swagger 6 | * components: 7 | * securitySchemes: 8 | * bearerAuth: 9 | * type: http 10 | * scheme: bearer 11 | * bearerFormat: JWT 12 | */ 13 | /** 14 | * @swagger 15 | * components: 16 | * schemas: 17 | * Login: 18 | * type: object 19 | * required: 20 | * - email 21 | * - password 22 | * properties: 23 | * email: 24 | * type: string 25 | * description: Email address 26 | * password: 27 | * type: string 28 | * description: Minimum of 8 characters long 29 | * RefreshToken: 30 | * type: object 31 | * required: 32 | * - refreshToken 33 | * properties: 34 | * refreshToken: 35 | * type: string 36 | * description: refresh token 37 | * Tokens: 38 | * type: object 39 | * properties: 40 | * accessToken: 41 | * type: string 42 | * description: access token 43 | * refreshToken: 44 | * type: string 45 | * description: refreshToken 46 | * User: 47 | * type: object 48 | * properties: 49 | * name: 50 | * type: string 51 | * description: The username 52 | * lastname: 53 | * type: string 54 | * description: The last name 55 | * email: 56 | * type: string 57 | * description: Email address 58 | * profilePicUrl: 59 | * type: string 60 | * description: piture profile 61 | * verified: 62 | * type: string 63 | * description: verification of account 64 | * status: 65 | * type: string 66 | * description: user status active or not 67 | * CreateUser: 68 | * type: object 69 | * required: 70 | * - name 71 | * - lastname 72 | * - email 73 | * properties: 74 | * name: 75 | * type: string 76 | * description: The username 77 | * lastname: 78 | * type: string 79 | * description: The last name 80 | * email: 81 | * type: string 82 | * description: Email address 83 | * password: 84 | * type: string 85 | * description: Minimum of 8 characters long 86 | * profilePicUrl: 87 | * type: string 88 | * description: user picture profile 89 | */ 90 | 91 | export default { 92 | userCredential: Joi.object().keys({ 93 | email: Joi.string().required().email(), 94 | password: Joi.string().required().min(6), 95 | }), 96 | refreshToken: Joi.object().keys({ 97 | refreshToken: Joi.string().required().min(1), 98 | }), 99 | auth: Joi.object() 100 | .keys({ 101 | authorization: JoiAuthBearer().required(), 102 | }) 103 | .unknown(true), 104 | signup: Joi.object().keys({ 105 | name: Joi.string().required().min(1), 106 | lastname: Joi.string().required().min(1), 107 | email: Joi.string().required().email(), 108 | password: Joi.string().required().regex(/^[a-zA-Z0-9]{8,30}$/), 109 | profilePicUrl: Joi.string().optional().uri(), 110 | }), 111 | }; 112 | -------------------------------------------------------------------------------- /src/routes/v1/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import access from './access/access'; 3 | import profile from './user/profile'; 4 | import users from './user/user' 5 | 6 | 7 | const router = express.Router(); 8 | 9 | router.use('/', access); 10 | router.use('/profile', profile); 11 | router.use('/users', users) 12 | 13 | 14 | 15 | export default router; 16 | -------------------------------------------------------------------------------- /src/routes/v1/user/profile.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import validator, { ValidationSource } from '../../../helpers/validator'; 3 | import schema from './schema'; 4 | import role from '../../../helpers/role'; 5 | import _ from 'lodash'; 6 | import authentication from '../../../auth/authentication'; 7 | import authorization from '../../../auth/authorization'; 8 | import { getMyProfile, updateProfile } from '../../../controllers/profileController'; 9 | import { RoleCode } from '../../../database/model/Role'; 10 | import { getAllUsers } from '../../../controllers/userController'; 11 | 12 | const router = express.Router(); 13 | 14 | /*-------------------------------------------------------------------------*/ 15 | // Below all APIs are private APIs protected for Access Token 16 | router.use('/', authentication); 17 | /*-------------------------------------------------------------------------*/ 18 | 19 | router.get( 20 | '/my', 21 | getMyProfile 22 | ); 23 | 24 | router.put( 25 | '/', 26 | validator(schema.profile), 27 | updateProfile 28 | ); 29 | 30 | router.use('/', role(RoleCode.ADMIN), authorization); 31 | 32 | export default router; 33 | -------------------------------------------------------------------------------- /src/routes/v1/user/schema.ts: -------------------------------------------------------------------------------- 1 | import Joi from '@hapi/joi'; 2 | import { JoiObjectId } from '../../../helpers/validator'; 3 | 4 | 5 | export default { 6 | userId: Joi.object().keys({ 7 | id: JoiObjectId().required(), 8 | }), 9 | profile: Joi.object().keys({ 10 | name: Joi.string().optional().min(1).max(200), 11 | lastname: Joi.string().optional().min(1).max(200), 12 | profilePicUrl: Joi.string().optional().uri(), 13 | }), 14 | }; 15 | -------------------------------------------------------------------------------- /src/routes/v1/user/user.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import role from '../../../helpers/role'; 3 | import authentication from '../../../auth/authentication'; 4 | import authorization from '../../../auth/authorization'; 5 | import { RoleCode } from '../../../database/model/Role'; 6 | import { deleteUser, getAllUsers } from '../../../controllers/userController'; 7 | import validator, { ValidationSource } from '../../../helpers/validator'; 8 | import schema from './schema'; 9 | 10 | const router = express.Router(); 11 | 12 | router.use('/', authentication, role(RoleCode.ADMIN), authorization); 13 | 14 | /** 15 | * @swagger 16 | * /users/all: 17 | * get: 18 | * summary: Returns the list of all the users 19 | * tags: [User] 20 | * parameters: 21 | * - in: path 22 | * name: id 23 | * schema: 24 | * type: string 25 | * - in: query 26 | * name: name 27 | * schema: 28 | * type: string 29 | * - in: query 30 | * name: lastname 31 | * schema: 32 | * type: string 33 | * - in: query 34 | * name: email 35 | * schema: 36 | * type: string 37 | * - in: query 38 | * name: sort 39 | * schema: 40 | * type: string 41 | * - in: query 42 | * name: deleted 43 | * schema: 44 | * type: string 45 | * - in: query 46 | * name: page 47 | * schema: 48 | * type: integer 49 | * - in: query 50 | * name: perPage 51 | * schema: 52 | * type: integer 53 | * responses: 54 | * 200: 55 | * description: The list of the users 56 | * content: 57 | * application/json: 58 | * schema: 59 | * type: array 60 | * items: 61 | * $ref: '#/components/schemas/User' 62 | * security: 63 | * - bearerAuth: [] 64 | */ 65 | 66 | router.get('/all', 67 | getAllUsers 68 | ) 69 | 70 | /** 71 | * @swagger 72 | * /users/{id}: 73 | * delete: 74 | * summary: Delete user by id 75 | * tags: [User] 76 | * parameters: 77 | * - in: path 78 | * name: id 79 | * schema: 80 | * type: string 81 | * responses: 82 | * 200: 83 | * description: User deleted 84 | * content: 85 | * application/json: 86 | * schema: 87 | * $ref: '#/components/schemas/User' 88 | * 89 | * security: 90 | * - bearerAuth: [] 91 | */ 92 | router.delete('/:id', 93 | validator(schema.userId, ValidationSource.PARAM), 94 | deleteUser 95 | ) 96 | export default router; 97 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import Logger from './core/Logger'; 2 | import { port } from './config'; 3 | import app from './app'; 4 | 5 | app 6 | .listen(port, () => { 7 | Logger.info(`server running on port : ${port}`); 8 | }) 9 | .on('error', (e) => Logger.error(e)); 10 | -------------------------------------------------------------------------------- /src/types/app-request.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import User from '../database/model/User'; 3 | import Keystore from '../database/model/Keystore'; 4 | 5 | declare interface PublicRequest extends Request { 6 | apiKey: string; 7 | } 8 | 9 | declare interface RoleRequest extends PublicRequest { 10 | currentRoleCode: string; 11 | } 12 | 13 | declare interface ProtectedRequest extends RoleRequest { 14 | user: User; 15 | accessToken: string; 16 | keystore: Keystore; 17 | } 18 | 19 | declare interface Tokens { 20 | accessToken: string; 21 | refreshToken: string; 22 | } 23 | declare interface ApiOptions { 24 | deleted?: boolean; 25 | isPaging?: boolean; 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | // Import non-ES modules as default imports. 5 | "esModuleInterop": true, 6 | // "useUnknownInCatchVariables": false, 7 | // Target latest version of ECMAScript. 8 | "target": "ES2019", 9 | // Process & infer types from .js files. 10 | "allowJs": false, 11 | // Enable strictNullChecks & noImplicitAny. 12 | "strictNullChecks": true, 13 | "noImplicitAny": true, 14 | "strict": true, 15 | "strictFunctionTypes": false, 16 | "noImplicitThis": false, 17 | // Search under node_modules for non-relative imports. 18 | "moduleResolution": "node", 19 | // Import .json files 20 | "resolveJsonModule": true, 21 | "sourceMap": true, 22 | "outDir": "build", 23 | "baseUrl": ".", 24 | "typeRoots": [ 25 | "./custom_typings", 26 | "./node_modules/@types" 27 | ], 28 | "paths": { 29 | "*": ["node_modules/*", "src/types/*"] 30 | } 31 | }, 32 | "include": ["src/**/*"], 33 | "exclude": [".templates"] 34 | } 35 | --------------------------------------------------------------------------------