├── .babelrc ├── .env.example ├── .gitignore ├── .npmrc ├── README.md ├── knexfile.js ├── package.json ├── src ├── app │ ├── controllers │ │ ├── auth │ │ │ └── index.js │ │ ├── index.js │ │ └── users │ │ │ └── index.js │ ├── exceptions │ │ ├── NotFoundException.js │ │ ├── UnAuthorizedException.js │ │ ├── ValidationException.js │ │ └── index.js │ ├── middlewares │ │ ├── Auth.js │ │ ├── FormValidations.js │ │ └── index.js │ ├── models │ │ ├── index.js │ │ └── user │ │ │ ├── Accessors.js │ │ │ └── index.js │ ├── repositories │ │ ├── index.js │ │ └── user │ │ │ └── index.js │ ├── transformers │ │ ├── index.js │ │ └── user │ │ │ └── index.js │ └── validations │ │ ├── auth.validations.js │ │ └── users.validations.js ├── bootstrap │ └── app.js ├── config │ ├── app.config.js │ ├── auth.config.js │ ├── database.config.js │ └── index.js ├── constants │ ├── DBTables.js │ ├── HTTPCode.js │ ├── Pagination.js │ └── UserRoles.js ├── database │ ├── factories │ │ └── UserFactory.js │ ├── migrations │ │ └── 20190707103825_create_users_table.js │ └── seeds │ │ └── UserTableSeeder.js ├── helpers │ └── core.helper.js ├── libraries │ ├── FakerFactories │ │ ├── FakerFactory.js │ │ └── index.js │ ├── Repository │ │ ├── RepositoryMixin.js │ │ ├── exceptions │ │ │ ├── ModelNotFoundException.js │ │ │ └── index.js │ │ ├── helpers │ │ │ └── Pagination.js │ │ ├── index.js │ │ ├── model │ │ │ ├── index.js │ │ │ └── plugins │ │ │ │ ├── Authenticatable.js │ │ │ │ └── Password.js │ │ └── transformer │ │ │ └── index.js │ └── controller │ │ ├── Response.js │ │ ├── index.js │ │ └── router │ │ └── index.js ├── routes │ ├── auth.routes.js │ ├── index.js │ └── users.routes.js └── server.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | [ 7 | "@babel/plugin-proposal-decorators", 8 | { 9 | "legacy": true 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME="e-Sifaris" 2 | APP_PORT=5000 3 | APP_DEBUG=true 4 | JWT_SECRET= 5 | 6 | DB_HOST=127.0.0.1 7 | DB_PORT=5432 8 | DB_NAME= 9 | DB_USER= 10 | DB_PASS= 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # testing 5 | /coverage 6 | 7 | # production 8 | /build 9 | 10 | # misc 11 | .DS_Store 12 | .env.local 13 | .env.development.local 14 | .env.test.local 15 | .env.production.local 16 | 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | .idea 22 | .env 23 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js MVC 2 | 3 | Simple Node.js MVC architecture with class object implementation using babel. 4 | 5 | ## Design Pattern 6 | - Repository Design Pattern 7 | 8 | ## Requirement 9 | This boilerplate uses latest version of Node/NPM. i.e. node v12 and npm v6.9. 10 | 11 | ## Tech 12 | - [Node.js] - JavaScript Runtime. 13 | - [Express] - Web Application Framework. 14 | - [Knex.js] - A query builder for database 15 | - [ObjectionJs] - ORM for interacting with database on top of [Knex.js]. 16 | 17 | [Knex.js]: 18 | [Node.js]: 19 | [Express]: 20 | [ObjectionJs]: 21 | 22 | ## Installation 23 | 24 | 1. Clone the repository 25 | 2. Copy `.env.example` to `.env` 26 | ``` 27 | $ cp .env.example .env 28 | ``` 29 | 3. Update the information in `.env` file with your information such as database credentials etc. 30 | 4. Install npm packages. I recommend to use `yarn`. 31 | ``` 32 | $ yarn 33 | ``` 34 | or, 35 | ``` 36 | $ npm install 37 | ``` 38 | 5. Now, start server with following commands. 39 | ``` 40 | $ yarn start 41 | ``` 42 | 43 | ## Other important commands 44 | 1. Create database migration file 45 | ``` 46 | $ yarn make:migration create_users_table 47 | ``` 48 | 2. Run Migrations 49 | ``` 50 | $ yarn migrate 51 | ``` 52 | 3. Rollback migration 53 | ``` 54 | $ yarn migrate:rollback 55 | ``` 56 | 4. Create database seeder file 57 | ``` 58 | $ yarn make:seed UsersTableSeeder 59 | ``` 60 | 5. Run seeders 61 | ``` 62 | $ yarn db:seed 63 | ``` 64 | 65 | ## Ready for production 66 | 67 | To deploy in production, first build app with following command 68 | ``` 69 | $ yarn build 70 | ``` 71 | After building app, deploy `build` directory and run like any other node app. 72 | 73 | ## Support 74 | 75 | If you faced any issue with this boilerplate create an issue. Or if you manage to improve it, send pull request. -------------------------------------------------------------------------------- /knexfile.js: -------------------------------------------------------------------------------- 1 | import { DBConfig } from "./src/config" 2 | 3 | module.exports = { ...DBConfig } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e-sifaris-server", 3 | "version": "1.0.0", 4 | "description": "e-sifaris express server", 5 | "main": "src/server.js", 6 | "engines": { 7 | "node": "~v12", 8 | "npm": "~6.9" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "start": "nodemon --exec babel-node src/server.js", 13 | "build": "babel src --out-dir build", 14 | "knex": "babel-node node_modules/.bin/knex --knexfile knexfile.js --env knexDB", 15 | "make:migration": "yarn knex migrate:make", 16 | "migrate": "yarn knex migrate:latest", 17 | "migrate:rollback": "yarn knex migrate:rollback", 18 | "make:seed": "yarn knex seed:make", 19 | "db:seed": "yarn knex seed:run" 20 | }, 21 | "author": "puncoz ", 22 | "license": "ISC", 23 | "dependencies": { 24 | "bcrypt": "3.0.6", 25 | "body-parser": "1.19.0", 26 | "compression": "1.7.4", 27 | "cors": "2.8.5", 28 | "express": "4.17.1", 29 | "express-validator": "6.2.0", 30 | "faker": "4.1.0", 31 | "helmet": "3.21.2", 32 | "jsonwebtoken": "8.5.1", 33 | "knex": "0.20.0", 34 | "lodash": "4.17.15", 35 | "method-override": "3.0.0", 36 | "morgan": "1.9.1", 37 | "objection": "1.6.11", 38 | "passport": "0.4.0", 39 | "passport-jwt": "4.0.0", 40 | "passport-local": "1.0.0", 41 | "pg": "7.12.1" 42 | }, 43 | "devDependencies": { 44 | "@babel/cli": "7.6.4", 45 | "@babel/core": "7.6.4", 46 | "@babel/node": "7.6.3", 47 | "@babel/plugin-proposal-decorators": "7.6.0", 48 | "@babel/preset-env": "7.6.3", 49 | "dotenv": "8.2.0", 50 | "nodemon": "1.19.4" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/controllers/auth/index.js: -------------------------------------------------------------------------------- 1 | import passport from "passport" 2 | import Controller from "../../../libraries/controller" 3 | import { UnAuthorizedException } from "../../exceptions" 4 | import { UserTransformer } from "../../transformers" 5 | 6 | export default class AuthController extends Controller { 7 | 8 | async login(req, res, next) { 9 | passport.authenticate("local", { 10 | session: false, 11 | }, (err, user, message) => { 12 | if (!user) { 13 | return next(new UnAuthorizedException(message)) 14 | } 15 | 16 | const token = user.generateToken() 17 | user = UserTransformer.transform(user) 18 | user.token = token 19 | 20 | this.sendResponse(res, user, message) 21 | })(req, res) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/app/controllers/index.js: -------------------------------------------------------------------------------- 1 | export { default as UsersController } from "./users" 2 | export { default as AuthController } from "./auth" 3 | -------------------------------------------------------------------------------- /src/app/controllers/users/index.js: -------------------------------------------------------------------------------- 1 | import { matchedData } from "express-validator" 2 | import { PAGINATE_MD } from "../../../constants/Pagination" 3 | import Controller from "../../../libraries/controller" 4 | import { UserRepository } from "../../repositories" 5 | import { UserTransformer } from "../../transformers" 6 | 7 | export default class UserController extends Controller { 8 | 9 | async index(req, res) { 10 | const users = await UserRepository.setTransformer(UserTransformer).paginate(PAGINATE_MD, req.query.page) 11 | 12 | return this.sendResponse(res, users) 13 | } 14 | 15 | async show(req, res, next) { 16 | try { 17 | const user = await UserRepository.setTransformer(UserTransformer).find(req.params.userId) 18 | 19 | return this.sendResponse(res, user) 20 | } catch (e) { 21 | next(e) 22 | } 23 | } 24 | 25 | async create(req, res) { 26 | const user = await UserRepository.setTransformer(UserTransformer).create(matchedData(req)) 27 | 28 | this.sendResponse(res, user) 29 | } 30 | 31 | async update(req, res, next) { 32 | try { 33 | const user = await UserRepository.setTransformer(UserTransformer).update(matchedData(req), req.params.userId) 34 | 35 | return this.sendResponse(res, user) 36 | } catch (e) { 37 | next(e) 38 | } 39 | } 40 | 41 | async delete(req, res, next) { 42 | try { 43 | await UserRepository.delete(req.params.userId) 44 | 45 | return this.sendResponse(res, null, "User deleted successfully.") 46 | } catch (e) { 47 | next(e) 48 | } 49 | } 50 | 51 | async changePassword(req, res, next) { 52 | const { password } = matchedData(req) 53 | 54 | try { 55 | await UserRepository.update({ password }, req.params.userId) 56 | 57 | return this.sendResponse(res, null, "Password changed successfully.") 58 | } catch (e) { 59 | next(e) 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/app/exceptions/NotFoundException.js: -------------------------------------------------------------------------------- 1 | import { HTTP_NOT_FOUND } from "../../constants/HTTPCode" 2 | 3 | export default class NotFoundException extends Error { 4 | constructor(message) { 5 | super(message || "Not found.") 6 | 7 | this.status = HTTP_NOT_FOUND 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/exceptions/UnAuthorizedException.js: -------------------------------------------------------------------------------- 1 | import { HTTP_UNAUTHORIZED } from "../../constants/HTTPCode" 2 | 3 | export default class UnAuthorizedException extends Error { 4 | constructor(message) { 5 | super(message || "Unautorized") 6 | 7 | this.status = HTTP_UNAUTHORIZED 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/exceptions/ValidationException.js: -------------------------------------------------------------------------------- 1 | import { HTTP_UNPROCESSABLE_ENTITY } from "../../constants/HTTPCode" 2 | 3 | export default class ValidationException extends Error { 4 | constructor(errors) { 5 | super("Validation errors.") 6 | 7 | this.status = HTTP_UNPROCESSABLE_ENTITY 8 | this.errors = errors 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/exceptions/index.js: -------------------------------------------------------------------------------- 1 | export { default as NotFoundException } from "./NotFoundException" 2 | export { default as ValidationException } from "./ValidationException" 3 | export { default as UnAuthorizedException } from "./UnAuthorizedException" 4 | -------------------------------------------------------------------------------- /src/app/middlewares/Auth.js: -------------------------------------------------------------------------------- 1 | import passport from "passport" 2 | import { UnAuthorizedException } from "../exceptions" 3 | 4 | class Auth { 5 | handle(req, res, next) { 6 | passport.authenticate("jwt", { 7 | session: false, 8 | }, (err, user, info) => { 9 | if (!user) { 10 | return next(new UnAuthorizedException(info.message)) 11 | } 12 | 13 | req.user = user 14 | next() 15 | })(req, res, next) 16 | } 17 | } 18 | 19 | export default (new Auth()).handle 20 | -------------------------------------------------------------------------------- /src/app/middlewares/FormValidations.js: -------------------------------------------------------------------------------- 1 | import { validationResult } from "express-validator" 2 | import { ValidationException } from "../exceptions" 3 | 4 | export default (req, res, next) => { 5 | const errorFormatter = ({ location, msg, param, value, nestedErrors }) => msg 6 | 7 | const errors = validationResult(req).formatWith(errorFormatter) 8 | 9 | if (!errors.isEmpty()) { 10 | throw new ValidationException(errors.mapped()) 11 | } 12 | 13 | next() 14 | } 15 | -------------------------------------------------------------------------------- /src/app/middlewares/index.js: -------------------------------------------------------------------------------- 1 | export { default as FormValidations }from "./FormValidations" 2 | export { default as auth } from "./Auth" 3 | -------------------------------------------------------------------------------- /src/app/models/index.js: -------------------------------------------------------------------------------- 1 | export { default as UserModel } from "./user" 2 | -------------------------------------------------------------------------------- /src/app/models/user/Accessors.js: -------------------------------------------------------------------------------- 1 | export default base => class extends base { 2 | fullName() { 3 | if (!this.full_name) { 4 | return "" 5 | } 6 | 7 | const fullName = [] 8 | 9 | if (this.full_name.hasOwnProperty("first_name")) { 10 | fullName.push(this.full_name.first_name) 11 | } 12 | 13 | if (this.full_name.hasOwnProperty("last_name")) { 14 | fullName.push(this.full_name.last_name) 15 | } 16 | 17 | return fullName.join(" ") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/models/user/index.js: -------------------------------------------------------------------------------- 1 | import { USERS_TABLE } from "../../../constants/DBTables" 2 | import BaseModel from "../../../libraries/Repository/model" 3 | import Authenticatable from "../../../libraries/Repository/model/plugins/Authenticatable" 4 | import Password from "../../../libraries/Repository/model/plugins/Password" 5 | import UserAccessors from "./Accessors" 6 | 7 | @UserAccessors 8 | @Password 9 | @Authenticatable 10 | export default class User extends BaseModel { 11 | static get tableName() { 12 | return USERS_TABLE 13 | } 14 | 15 | static get idColumn() { 16 | return "id" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/repositories/index.js: -------------------------------------------------------------------------------- 1 | export { default as UserRepository } from "./user" 2 | -------------------------------------------------------------------------------- /src/app/repositories/user/index.js: -------------------------------------------------------------------------------- 1 | import { UserModel } from "../../models" 2 | import BaseRepository from "./../../../libraries/Repository" 3 | 4 | class UserRepository extends BaseRepository { 5 | constructor(props) { 6 | super(props) 7 | } 8 | 9 | model() { 10 | return UserModel 11 | } 12 | } 13 | 14 | export default new UserRepository() 15 | -------------------------------------------------------------------------------- /src/app/transformers/index.js: -------------------------------------------------------------------------------- 1 | export { default as UserTransformer } from "./user" 2 | -------------------------------------------------------------------------------- /src/app/transformers/user/index.js: -------------------------------------------------------------------------------- 1 | import BaseTransformer from "../../../libraries/Repository/transformer" 2 | 3 | class UserTransformer extends BaseTransformer { 4 | transform(user) { 5 | return { 6 | id: user.id, 7 | email: user.email, 8 | username: user.username, 9 | full_name: user.fullName(), 10 | role: user.role, 11 | } 12 | } 13 | } 14 | 15 | export default new UserTransformer() 16 | -------------------------------------------------------------------------------- /src/app/validations/auth.validations.js: -------------------------------------------------------------------------------- 1 | import { check } from "express-validator" 2 | 3 | export default { 4 | login: [ 5 | check("username"). 6 | isString().withMessage("Username is required."), 7 | 8 | check("password"). 9 | isString().withMessage("Password is required."), 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /src/app/validations/users.validations.js: -------------------------------------------------------------------------------- 1 | import { check } from "express-validator" 2 | import { UserRepository } from "../repositories" 3 | 4 | export default { 5 | create: [ 6 | check("email"). 7 | isString().withMessage("Email is required."). 8 | normalizeEmail().isEmail().withMessage("Invalid email format"). 9 | custom(async email => { 10 | try { 11 | await UserRepository.findByColumn("email", email) 12 | } catch (e) { 13 | return true 14 | } 15 | 16 | throw new Error("Email already exists.") 17 | }), 18 | 19 | check("username"). 20 | isString().withMessage("Username is required"). 21 | custom(async username => { 22 | try { 23 | await UserRepository.findByColumn("username", username) 24 | } catch (e) { 25 | return true 26 | } 27 | 28 | throw new Error("Username already exists.") 29 | }), 30 | 31 | check("password"). 32 | isString().withMessage("Password is required"). 33 | isLength({ min: 6 }).withMessage("Password should be greater than 6 characters long."), 34 | ], 35 | 36 | update: [ 37 | check("email"). 38 | isString().withMessage("Email is required."). 39 | normalizeEmail().isEmail().withMessage("Invalid email format"). 40 | custom(async (email, { req }) => { 41 | const userId = req.params.userId 42 | 43 | const user = await UserRepository.query().where("email", email).where("id", "<>", userId) 44 | 45 | if (user.length === 0) { 46 | return true 47 | } 48 | 49 | throw new Error("Email already exists.") 50 | }), 51 | 52 | check("username"). 53 | isString().withMessage("Username is required"). 54 | custom(async (username, { req }) => { 55 | const userId = req.params.userId 56 | 57 | const user = await UserRepository.query().where("username", username).where("id", "<>", userId) 58 | 59 | if (user.length === 0) { 60 | return true 61 | } 62 | 63 | throw new Error("Username already exists.") 64 | }), 65 | ], 66 | 67 | passwordChange: [ 68 | check("password"). 69 | isString().withMessage("Password is required"). 70 | isLength({ min: 6 }).withMessage("Password should be greater than 6 characters long."), 71 | 72 | check("confirm-password"). 73 | isString().withMessage("Password is required"). 74 | custom(async (password, { req }) => { 75 | if (password === req.body.password) { 76 | return true 77 | } 78 | 79 | throw new Error("Password confirmation does not match password") 80 | }), 81 | ], 82 | } 83 | -------------------------------------------------------------------------------- /src/bootstrap/app.js: -------------------------------------------------------------------------------- 1 | import { 2 | json, 3 | urlencoded, 4 | } from "body-parser" 5 | import compression from "compression" 6 | import cors from "cors" 7 | import express from "express" 8 | import helmet from "helmet" 9 | import Knex from "knex" 10 | import methodOverride from "method-override" 11 | import logger from "morgan" 12 | import { Model } from "objection" 13 | import passport from "passport" 14 | import { 15 | ExtractJwt, 16 | Strategy as JWTStrategy, 17 | } from "passport-jwt" 18 | import LocalStrategy from "passport-local" 19 | import { 20 | NotFoundException, 21 | UnAuthorizedException, 22 | ValidationException, 23 | } from "../app/exceptions" 24 | import { 25 | HTTP_INTERNAL_SERVER_ERROR, 26 | HTTP_NOT_FOUND, 27 | HTTP_UNAUTHORIZED, 28 | HTTP_UNPROCESSABLE_ENTITY, 29 | } from "../constants/HTTPCode" 30 | import { ModelNotFoundException } from "../libraries/Repository/exceptions" 31 | import { 32 | AppConfig, 33 | AuthConfig, 34 | DBConfig, 35 | } from "./../config" 36 | import routes from "./../routes" 37 | 38 | class App { 39 | constructor() { 40 | this.app = express() 41 | this.appDebug = AppConfig.debug === "true" 42 | 43 | this.setup() 44 | this.database() 45 | this.authentication() 46 | this.routers() 47 | } 48 | 49 | setup() { 50 | this.app.use(helmet()) 51 | this.app.use(compression()) 52 | this.app.use(json()) 53 | this.app.use(urlencoded({ extended: true })) 54 | this.app.use(logger("dev")) 55 | this.app.use(cors()) 56 | this.app.use(methodOverride("_method")) 57 | } 58 | 59 | database() { 60 | const knex = Knex(DBConfig) 61 | Model.knex(knex) 62 | } 63 | 64 | routers() { 65 | routes(this.app) 66 | 67 | this.app.use((req, res, next) => { 68 | throw new NotFoundException() 69 | }) 70 | 71 | this.app.use((error, req, res, next) => { 72 | if (error instanceof NotFoundException || error instanceof ModelNotFoundException) { 73 | res.status(error.status || HTTP_NOT_FOUND).json({ 74 | message: error.message, 75 | }) 76 | 77 | return 78 | } 79 | 80 | if (error instanceof ValidationException) { 81 | res.status(error.status || HTTP_UNPROCESSABLE_ENTITY).json({ 82 | errors: error.errors, 83 | message: error.message, 84 | }) 85 | 86 | return 87 | } 88 | 89 | if (error instanceof UnAuthorizedException) { 90 | res.status(error.status || HTTP_UNAUTHORIZED).json({ 91 | message: error.message, 92 | }) 93 | 94 | return 95 | } 96 | 97 | console.error(error) 98 | res.status(error.status || HTTP_INTERNAL_SERVER_ERROR).json({ 99 | message: this.appDebug ? error.message : "Server error.", 100 | errors: this.appDebug ? error : null, 101 | }) 102 | }) 103 | } 104 | 105 | authentication() { 106 | this.app.use(passport.initialize({})) 107 | 108 | const authModel = new AuthConfig.authModel() 109 | 110 | passport.use("local", new LocalStrategy({ 111 | usernameField: AuthConfig.request.usernameField, 112 | passwordField: AuthConfig.request.passwordField, 113 | }, authModel.authenticate)) 114 | 115 | passport.use("jwt", new JWTStrategy({ 116 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 117 | secretOrKey: AuthConfig.jwtSecret, 118 | }, authModel.authenticateJwt)) 119 | } 120 | } 121 | 122 | export default new App().app 123 | -------------------------------------------------------------------------------- /src/config/app.config.js: -------------------------------------------------------------------------------- 1 | import { env } from "./../helpers/core.helper" 2 | 3 | export default { 4 | name: env("APP_NAME", "NodeJs App"), 5 | port: env("APP_PORT", 3000), 6 | debug: env("APP_DEBUG", false), 7 | } 8 | -------------------------------------------------------------------------------- /src/config/auth.config.js: -------------------------------------------------------------------------------- 1 | import { UserModel } from "../app/models" 2 | import { env } from "../helpers/core.helper" 3 | 4 | export default { 5 | passwordField: "password", 6 | 7 | hashRounds: 12, 8 | 9 | tokenExpiration: 60 * 60, // in seconds 10 | 11 | jwtSecret: env("JWT_SECRET"), 12 | 13 | request: { 14 | usernameField: "username", 15 | passwordField: "password", 16 | }, 17 | 18 | authModel: UserModel, 19 | } 20 | -------------------------------------------------------------------------------- /src/config/database.config.js: -------------------------------------------------------------------------------- 1 | import { 2 | env, 3 | src_path, 4 | } from "../helpers/core.helper" 5 | import { MIGRATIONS_TABLE } from "./../constants/DBTables" 6 | 7 | export default { 8 | client: "postgresql", 9 | connection: { 10 | host: env("DB_HOST"), 11 | port: env("DB_PORT"), 12 | database: env("DB_NAME"), 13 | user: env("DB_USER"), 14 | password: env("DB_PASS"), 15 | }, 16 | pool: { 17 | min: 2, 18 | max: 10, 19 | }, 20 | migrations: { 21 | tableName: MIGRATIONS_TABLE, 22 | directory: src_path("database/migrations"), 23 | }, 24 | seeds: { 25 | directory: src_path("database/seeds"), 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | export { default as AppConfig } from "./app.config" 2 | export { default as DBConfig } from "./database.config" 3 | export { default as AuthConfig } from "./auth.config" 4 | -------------------------------------------------------------------------------- /src/constants/DBTables.js: -------------------------------------------------------------------------------- 1 | export const MIGRATIONS_TABLE = "sys_migrations" 2 | 3 | export const USERS_TABLE = "auth_users" 4 | -------------------------------------------------------------------------------- /src/constants/HTTPCode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Informational 3 | */ 4 | export const HTTP_CONTINUE = 100 5 | export const HTTP_SWITCHING_PROTOCOLS = 101 6 | export const HTTP_PROCESSING = 102 // RFC2518 7 | 8 | /** 9 | * Success 10 | */ 11 | export const HTTP_OK = 200 12 | export const HTTP_CREATED = 201 13 | export const HTTP_ACCEPTED = 202 14 | export const HTTP_NON_AUTHORITATIVE_INFORMATION = 203 15 | export const HTTP_NO_CONTENT = 204 16 | export const HTTP_RESET_CONTENT = 205 17 | export const HTTP_PARTIAL_CONTENT = 206 18 | export const HTTP_MULTI_STATUS = 207 // RFC4918 19 | export const HTTP_ALREADY_REPORTED = 208 // RFC5842 20 | export const HTTP_IM_USED = 226 // RFC3229 21 | 22 | /** 23 | * Redirection 24 | */ 25 | export const HTTP_MULTIPLE_CHOICES = 300 26 | export const HTTP_MOVED_PERMANENTLY = 301 27 | export const HTTP_FOUND = 302 28 | export const HTTP_SEE_OTHER = 303 29 | export const HTTP_NOT_MODIFIED = 304 30 | export const HTTP_USE_PROXY = 305 31 | export const HTTP_RESERVED = 306 32 | export const HTTP_TEMPORARY_REDIRECT = 307 33 | export const HTTP_PERMANENTLY_REDIRECT = 308 // RFC7238 34 | 35 | /** 36 | * Client Error 37 | */ 38 | export const HTTP_BAD_REQUEST = 400 39 | export const HTTP_UNAUTHORIZED = 401 40 | export const HTTP_PAYMENT_REQUIRED = 402 41 | export const HTTP_FORBIDDEN = 403 42 | /** 43 | * The requested resource could not be found 44 | * 45 | * Note: This is sometimes used to mask if there was an UNAUTHORIZED (401) or 46 | * FORBIDDEN (403) error, for security reasons 47 | */ 48 | export const HTTP_NOT_FOUND = 404 49 | export const HTTP_METHOD_NOT_ALLOWED = 405 50 | export const HTTP_NOT_ACCEPTABLE = 406 51 | export const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407 52 | export const HTTP_REQUEST_TIMEOUT = 408 53 | export const HTTP_CONFLICT = 409 54 | export const HTTP_GONE = 410 55 | export const HTTP_LENGTH_REQUIRED = 411 56 | export const HTTP_PRECONDITION_FAILED = 412 57 | export const HTTP_REQUEST_ENTITY_TOO_LARGE = 413 58 | export const HTTP_REQUEST_URI_TOO_LONG = 414 59 | export const HTTP_UNSUPPORTED_MEDIA_TYPE = 415 60 | export const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416 61 | export const HTTP_EXPECTATION_FAILED = 417 62 | export const HTTP_I_AM_A_TEAPOT = 418 // RFC2324 63 | export const HTTP_UNPROCESSABLE_ENTITY = 422 // RFC4918 64 | export const HTTP_LOCKED = 423 // RFC4918 65 | export const HTTP_FAILED_DEPENDENCY = 424 // RFC4918 66 | export const HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL = 425 // RFC2817 67 | export const HTTP_UPGRADE_REQUIRED = 426 // RFC2817 68 | export const HTTP_PRECONDITION_REQUIRED = 428 // RFC6585 69 | export const HTTP_TOO_MANY_REQUESTS = 429 // RFC6585 70 | export const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 // RFC6585 71 | 72 | /** 73 | * The server encountered an unexpected error 74 | */ 75 | export const HTTP_INTERNAL_SERVER_ERROR = 500 76 | export const HTTP_NOT_IMPLEMENTED = 501 77 | export const HTTP_BAD_GATEWAY = 502 78 | export const HTTP_SERVICE_UNAVAILABLE = 503 79 | export const HTTP_GATEWAY_TIMEOUT = 504 80 | export const HTTP_VERSION_NOT_SUPPORTED = 505 81 | export const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506 // RFC2295 82 | export const HTTP_INSUFFICIENT_STORAGE = 507 // RFC4918 83 | export const HTTP_LOOP_DETECTED = 508 // RFC5842 84 | export const HTTP_NOT_EXTENDED = 510 // RFC2774 85 | export const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511 86 | -------------------------------------------------------------------------------- /src/constants/Pagination.js: -------------------------------------------------------------------------------- 1 | export const PAGINATE_XS = 5 2 | export const PAGINATE_SM = 10 3 | export const PAGINATE_MD = 15 4 | export const PAGINATE_LG = 20 5 | export const PAGINATE_XL = 25 6 | -------------------------------------------------------------------------------- /src/constants/UserRoles.js: -------------------------------------------------------------------------------- 1 | export const SUPER_ADMIN = "superadmin" 2 | export const ADMIN = "admin" 3 | 4 | export const ALL = [SUPER_ADMIN, ADMIN] 5 | -------------------------------------------------------------------------------- /src/database/factories/UserFactory.js: -------------------------------------------------------------------------------- 1 | import { UserModel } from "../../app/models" 2 | import { ALL as ROLES } from "../../constants/UserRoles" 3 | import factory from "../../libraries/FakerFactories" 4 | 5 | factory.define("users", UserModel, faker => ({ 6 | email: faker.internet.email(), 7 | username: faker.internet.userName(), 8 | password: "secret", 9 | full_name: { 10 | first_name: faker.name.firstName(), 11 | last_name: faker.name.lastName(), 12 | }, 13 | role: faker.random.arrayElement(ROLES), 14 | })) 15 | -------------------------------------------------------------------------------- /src/database/migrations/20190707103825_create_users_table.js: -------------------------------------------------------------------------------- 1 | import { USERS_TABLE } from "./../../constants/DBTables" 2 | 3 | export const up = knex => knex.schema.createTable(USERS_TABLE, table => { 4 | table.increments() 5 | table.string("email").unique().notNullable() 6 | table.string("username").unique().notNullable() 7 | table.string("password").notNullable() 8 | table.jsonb("full_name").nullable() 9 | table.jsonb("metadata").nullable() 10 | table.string("profile_picture").nullable() 11 | table.string("role") 12 | 13 | table.integer("created_by").unsigned().nullable().index().references("id").inTable(USERS_TABLE) 14 | table.integer("updated_by").unsigned().nullable().index().references("id").inTable(USERS_TABLE) 15 | table.integer("deleted_by").unsigned().nullable().index().references("id").inTable(USERS_TABLE) 16 | 17 | table.timestamps(true, true) 18 | table.timestamp("deleted_at") 19 | }) 20 | 21 | export const down = knex => knex.schema.dropTable(USERS_TABLE) 22 | -------------------------------------------------------------------------------- /src/database/seeds/UserTableSeeder.js: -------------------------------------------------------------------------------- 1 | import { USERS_TABLE } from "../../constants/DBTables" 2 | import { SUPER_ADMIN } from "../../constants/UserRoles" 3 | import { random } from "../../helpers/core.helper" 4 | import factory from "../../libraries/FakerFactories" 5 | import "./../factories/UserFactory" 6 | 7 | export const seed = async knex => { 8 | const superAdmin = await knex(USERS_TABLE).where("email", "admin@admin.com") 9 | 10 | if (superAdmin.length === 0) { 11 | factory("users", 1).create({ 12 | email: "admin@admin.com", 13 | username: "admin", 14 | full_name: { 15 | first_name: "Administrator", 16 | last_name: "", 17 | }, 18 | role: SUPER_ADMIN, 19 | }) 20 | } 21 | 22 | await factory("users", random(15, 25)).create() 23 | } 24 | -------------------------------------------------------------------------------- /src/helpers/core.helper.js: -------------------------------------------------------------------------------- 1 | import { config as dotEnvConfig } from "dotenv" 2 | import path from "path" 3 | 4 | export const root_path = (directory = undefined) => path.resolve(__dirname, "../../..", directory || "") 5 | export const client_path = (directory) => path.resolve(root_path(), "client", directory || "") 6 | export const server_path = (directory) => path.resolve(root_path(), "server", directory || "") 7 | export const src_path = (directory) => path.resolve(server_path(), "src", directory || "") 8 | export const app_path = (directory) => path.resolve(server_path(), "src/app", directory || "") 9 | dotEnvConfig({ path: root_path(".env") }) 10 | 11 | export const env = (key, defaultValue) => { 12 | const value = process.env[key] || defaultValue 13 | if (typeof value === "undefined") { 14 | throw new Error(`Environment variable ${key} not set.`) 15 | } 16 | 17 | return value 18 | } 19 | 20 | export const normalizePort = (port) => { 21 | port = parseInt(port, 10) 22 | 23 | if (isNaN(port)) { 24 | throw new Error("Invalid port.") 25 | } 26 | 27 | if (port <= 0) { 28 | throw new Error("Invalid port.") 29 | } 30 | 31 | return port 32 | } 33 | 34 | export const onServerListening = (server) => { 35 | const addr = server.address() 36 | const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr.port}` 37 | console.info(`Server listening on ${bind}`) 38 | } 39 | 40 | export const onServerError = (error) => { 41 | if (error.syscall !== "listen") { 42 | throw error 43 | } 44 | 45 | const bind = typeof port === "string" ? "Pipe " + port : "Port " + port 46 | switch (error.code) { 47 | case "EACCES": 48 | console.error(`${bind} requires elevated privileges`) 49 | process.exit(1) 50 | break 51 | case "EADDRINUSE": 52 | console.error(`${bind} is already in use`) 53 | process.exit(1) 54 | break 55 | default: 56 | throw error 57 | } 58 | } 59 | 60 | export function * range(begin, end, interval = 1) { 61 | for (let i = begin; i < end; i += interval) { 62 | yield i 63 | } 64 | } 65 | 66 | export const random = (min, max) => Math.floor(Math.random() * (max - min + 1) + min) 67 | 68 | export const isArray = data => Array.isArray(data) 69 | export const isObject = data => typeof data === "object" && !isArray(data) 70 | export const isBycryptedHash = (string) => /^\$2[ayb]\$[0-9]{2}\$[A-Za-z0-9./]{53}$/.test(string) 71 | -------------------------------------------------------------------------------- /src/libraries/FakerFactories/FakerFactory.js: -------------------------------------------------------------------------------- 1 | import faker from "faker" 2 | import Knex from "knex" 3 | import { Model } from "objection" 4 | import { DBConfig } from "../../config" 5 | import { range } from "../../helpers/core.helper" 6 | 7 | export default class FakerFactory { 8 | /** 9 | * @param {BaseModel} model 10 | * @param {function} callback 11 | * @return {FakerFactory} 12 | */ 13 | constructor(model, callback) { 14 | this.initDatabase() 15 | 16 | this.model = model 17 | this.callback = callback 18 | this.number = 1 19 | } 20 | 21 | initDatabase() { 22 | const knex = Knex(DBConfig) 23 | Model.knex(knex) 24 | } 25 | 26 | /** 27 | * 28 | * @param attributes 29 | * @return {object|Array} 30 | */ 31 | async create(attributes = {}) { 32 | let data = this.make(attributes) 33 | 34 | return await this.model.query().insert(data) 35 | } 36 | 37 | /** 38 | * @param {object} attributes 39 | * @return {Array|object} 40 | */ 41 | make(attributes = {}) { 42 | if (this.number === 1) { 43 | return this.prepareData(attributes) 44 | } 45 | 46 | const data = [] 47 | for (let i of range(0, this.number)) { 48 | data.push(this.prepareData(attributes)) 49 | } 50 | 51 | return data 52 | } 53 | 54 | /** 55 | * @param {object} attributes 56 | * @return {object} 57 | */ 58 | prepareData(attributes) { 59 | return { ...this.callback(faker), ...attributes } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/libraries/FakerFactories/index.js: -------------------------------------------------------------------------------- 1 | import FakerFactory from "./FakerFactory" 2 | 3 | const registeredFaker = {} 4 | 5 | /** 6 | * @param {string} label 7 | * @param {Number} number 8 | * @returns {FakerFactory} 9 | */ 10 | const factory = (label, number = 1) => { 11 | const faker = registeredFaker[label] 12 | 13 | if (!(faker instanceof FakerFactory)) { 14 | throw new Error(`Faker '${label}' not defined.`) 15 | } 16 | 17 | faker.number = number 18 | 19 | return faker 20 | } 21 | 22 | /** 23 | * 24 | * @param {string} label 25 | * @param {BaseModel} model 26 | * @param {function} callback 27 | */ 28 | factory.define = (label, model, callback) => { 29 | registeredFaker[label] = new FakerFactory(model, callback) 30 | } 31 | 32 | export default factory 33 | -------------------------------------------------------------------------------- /src/libraries/Repository/RepositoryMixin.js: -------------------------------------------------------------------------------- 1 | import { ModelNotFoundException } from "./exceptions" 2 | import Pagination from "./helpers/Pagination" 3 | 4 | export default base => class extends base { 5 | async create(data) { 6 | const result = await this.model.query().insert(data) 7 | 8 | return this.parserResult(result) 9 | } 10 | 11 | async update(data, id) { 12 | await this.find(id) 13 | 14 | const result = await this.model.query().patchAndFetchById(id, data) 15 | 16 | return this.parserResult(result) 17 | } 18 | 19 | async delete(id) { 20 | await this.find(id) 21 | 22 | return await this.model.query().deleteById(id) 23 | } 24 | 25 | async find(id) { 26 | const result = await this.model.query().findById(id) 27 | 28 | if (!result) { 29 | throw new ModelNotFoundException(null, id) 30 | } 31 | 32 | return this.parserResult(result) 33 | } 34 | 35 | async findByColumn(column, value) { 36 | const result = await this.model.query().where(column, value) 37 | 38 | if (result.length === 0) { 39 | throw new ModelNotFoundException(null, value, column) 40 | } 41 | 42 | return this.parserResult(result[0]) 43 | } 44 | 45 | async all() { 46 | const results = await this.model.query() 47 | 48 | return this.parserResult(results) 49 | } 50 | 51 | async paginate(perPage = 10, page = 1) { 52 | const results = await this.model.query().page(page - 1, perPage) 53 | 54 | return this.parserResult(new Pagination(results, perPage, page)) 55 | } 56 | 57 | query() { 58 | return this.model.query() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/libraries/Repository/exceptions/ModelNotFoundException.js: -------------------------------------------------------------------------------- 1 | export default class ModelNotFoundException extends Error { 2 | constructor(modelName, id, idColumn = "id") { 3 | super() 4 | this.modelName = modelName 5 | this.id = id 6 | this.message = `No record found for ${this.modelName} with ${idColumn}: ${this.id}` 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/libraries/Repository/exceptions/index.js: -------------------------------------------------------------------------------- 1 | export { default as ModelNotFoundException } from "./ModelNotFoundException" 2 | -------------------------------------------------------------------------------- /src/libraries/Repository/helpers/Pagination.js: -------------------------------------------------------------------------------- 1 | export default class Pagination { 2 | constructor(data, perPage, page) { 3 | this.data = data 4 | this.perPage = perPage 5 | this.page = parseInt(page) 6 | 7 | this.setup() 8 | } 9 | 10 | setup() { 11 | const totalPages = Math.ceil(this.data.total / this.perPage) 12 | 13 | this.pagination = { 14 | total: this.data.total, 15 | count: this.data.results.length, 16 | per_page: this.perPage, 17 | current_page: this.page, 18 | total_pages: totalPages, 19 | } 20 | } 21 | 22 | get() { 23 | return { 24 | data: this.data.results, 25 | pagination: this.pagination, 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/libraries/Repository/index.js: -------------------------------------------------------------------------------- 1 | import { isArray } from "../../helpers/core.helper" 2 | import Pagination from "./helpers/Pagination" 3 | import RepositoryMixin from "./RepositoryMixin" 4 | import BaseTransformer from "./transformer" 5 | 6 | @RepositoryMixin 7 | export default class BaseRepository { 8 | constructor() { 9 | this._init() 10 | } 11 | 12 | _init() { 13 | if (this.model === undefined) { 14 | throw new TypeError("Repository should have 'model' method defined.") 15 | } 16 | 17 | this.model = this.model() 18 | this.transformer = null 19 | this.transformerSkipped = false 20 | } 21 | 22 | setTransformer(transformer) { 23 | this.transformer = transformer 24 | 25 | return this 26 | } 27 | 28 | skipTransformer(skip = true) { 29 | this.transformerSkipped = skip 30 | } 31 | 32 | parserResult(data) { 33 | if (this.transformerSkipped || !(this.transformer instanceof BaseTransformer)) { 34 | return data instanceof Pagination ? data.get() : data 35 | } 36 | 37 | if (data instanceof Pagination) { 38 | const paginatedResults = data.get() 39 | const results = paginatedResults.data.map(datum => this.transformer.transform(datum)) 40 | return { paginatedData: results, meta: { pagination: paginatedResults.pagination } } 41 | } 42 | 43 | return isArray(data) ? data.map(datum => this.transformer.transform(datum)) : this.transformer.transform(data) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/libraries/Repository/model/index.js: -------------------------------------------------------------------------------- 1 | import { Model } from "objection" 2 | 3 | export default class BaseModel extends Model { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/libraries/Repository/model/plugins/Authenticatable.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken" 2 | import { AuthConfig } from "../../../../config" 3 | 4 | export default base => class extends base { 5 | generateToken() { 6 | const today = new Date() 7 | const expirationDate = new Date(today) 8 | expirationDate.setTime(today.getTime() + AuthConfig.tokenExpiration * 1000) 9 | 10 | return jwt.sign({ 11 | id: this.id, 12 | email: this.email, 13 | exp: parseInt((expirationDate.getTime() / 1000).toString(), 10), 14 | }, AuthConfig.jwtSecret) 15 | } 16 | 17 | async authenticate(username, password, done) { 18 | const user = await AuthConfig.authModel.query().findOne({ "username": username }) 19 | 20 | if (!user || !user.matchPassword(password)) { 21 | return done(null, null, "Invalid login.") 22 | } 23 | 24 | return done(null, user, "Login success.") 25 | } 26 | 27 | async authenticateJwt(jwtPayload, done) { 28 | const user = await AuthConfig.authModel.query().findById(jwtPayload.id) 29 | 30 | if (!user) { 31 | return done("UnAuthenticated") 32 | } 33 | 34 | return done(null, user) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/libraries/Repository/model/plugins/Password.js: -------------------------------------------------------------------------------- 1 | import Bcrypt from "bcrypt" 2 | import { AuthConfig } from "../../../../config" 3 | import { isBycryptedHash } from "../../../../helpers/core.helper" 4 | 5 | export default base => class extends base { 6 | async $beforeInsert(context) { 7 | await super.$beforeInsert(context) 8 | 9 | const password = this[AuthConfig.passwordField] 10 | this[AuthConfig.passwordField] = await this.generateHash(password) 11 | } 12 | 13 | async $beforeUpdate(queryOptions, context) { 14 | await super.$beforeUpdate(queryOptions, context) 15 | 16 | const password = this[AuthConfig.passwordField] 17 | if (!password) { 18 | return 19 | } 20 | 21 | this[AuthConfig.passwordField] = await this.generateHash(password) 22 | } 23 | 24 | generateHash(password) { 25 | if (!password) { 26 | return password 27 | } 28 | 29 | if (isBycryptedHash(password)) { 30 | return password 31 | } 32 | 33 | return Bcrypt.hash(password, AuthConfig.hashRounds) 34 | } 35 | 36 | matchPassword(password) { 37 | return Bcrypt.compare(password, this[AuthConfig.passwordField]) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/libraries/Repository/transformer/index.js: -------------------------------------------------------------------------------- 1 | export default class BaseTransformer { 2 | constructor() { 3 | if (this.transform === undefined) { 4 | throw new TypeError("Transformer should have 'transform' method defined.") 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/libraries/controller/Response.js: -------------------------------------------------------------------------------- 1 | import { 2 | HTTP_NOT_FOUND, 3 | HTTP_OK, 4 | } from "../../constants/HTTPCode" 5 | import { isObject } from "../../helpers/core.helper" 6 | 7 | export default base => class extends base { 8 | constructor(props) { 9 | super(props) 10 | this.errors = null 11 | this.metadata = null 12 | } 13 | 14 | setErrors(errors) { 15 | this.errors = errors 16 | 17 | return this 18 | } 19 | 20 | setMetadata(metadata) { 21 | this.metadata = metadata 22 | 23 | return this 24 | } 25 | 26 | sendResponse(res, data = null, message = "", code = HTTP_OK) { 27 | if (data && isObject(data) && data.hasOwnProperty("meta")) { 28 | this.metadata = { ...this.metadata, ...data.meta } 29 | } 30 | 31 | res.status(code) 32 | res.json(this._prepareResponse(message, data)) 33 | } 34 | 35 | sendError(res, error = "Error", code = HTTP_NOT_FOUND) { 36 | res.status(code) 37 | res.json(this._prepareErrorResponse(error)) 38 | } 39 | 40 | _prepareResponse(message, data) { 41 | const response = { 42 | status: true, 43 | } 44 | 45 | if (message) { 46 | response["message"] = message 47 | } 48 | 49 | if (data) { 50 | response["data"] = this._extractData(data) 51 | } 52 | 53 | if (this.metadata) { 54 | response["metadata"] = this.metadata 55 | } 56 | 57 | return response 58 | } 59 | 60 | _prepareErrorResponse(errorMessage) { 61 | const response = { 62 | status: false, 63 | } 64 | 65 | if (errorMessage) { 66 | response["message"] = errorMessage 67 | } 68 | 69 | if (this.errors) { 70 | response["data"] = this.errors 71 | } 72 | 73 | if (this.metadata) { 74 | response["metadata"] = this.metadata 75 | } 76 | 77 | return response 78 | } 79 | 80 | _extractData(data) { 81 | if (isObject(data) && data.hasOwnProperty("paginatedData")) { 82 | return data.paginatedData 83 | } 84 | 85 | return data 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/libraries/controller/index.js: -------------------------------------------------------------------------------- 1 | import Response from "./Response" 2 | 3 | @Response 4 | export default class Controller { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/libraries/controller/router/index.js: -------------------------------------------------------------------------------- 1 | import { Router as ExpressRouter } from "express" 2 | import * as Controllers from "./../../../app/controllers" 3 | 4 | class Router { 5 | constructor() { 6 | this.router = ExpressRouter() 7 | } 8 | 9 | get(path, ...actions) { 10 | this.router.get(path, ...this._resolveController(actions)) 11 | } 12 | 13 | post(path, ...actions) { 14 | this.router.post(path, ...this._resolveController(actions)) 15 | } 16 | 17 | patch(path, ...actions) { 18 | this.router.patch(path, ...this._resolveController(actions)) 19 | } 20 | 21 | delete(path, ...actions) { 22 | this.router.delete(path, ...this._resolveController(actions)) 23 | } 24 | 25 | export() { 26 | return this.router 27 | } 28 | 29 | _resolveController(actions) { 30 | const lastIndex = actions.length - 1 31 | const action = actions[lastIndex] 32 | 33 | const [controllerName, methodName] = action.split("@") 34 | const controller = new Controllers[controllerName] 35 | 36 | actions[lastIndex] = typeof action === "string" ? controller[methodName].bind(controller) : action 37 | 38 | return actions 39 | } 40 | } 41 | 42 | export default new Router() 43 | -------------------------------------------------------------------------------- /src/routes/auth.routes.js: -------------------------------------------------------------------------------- 1 | import { FormValidations } from "../app/middlewares" 2 | import AuthValidations from "../app/validations/auth.validations" 3 | import Router from "../libraries/controller/router" 4 | 5 | Router.post("/login", [ 6 | AuthValidations.login, FormValidations, 7 | ], "AuthController@login") 8 | 9 | export default Router.export() 10 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import AuthRoutes from "./auth.routes" 2 | import UserRoutes from "./users.routes" 3 | 4 | export default app => { 5 | app.use("/", AuthRoutes) 6 | app.use("/users", UserRoutes) 7 | } 8 | -------------------------------------------------------------------------------- /src/routes/users.routes.js: -------------------------------------------------------------------------------- 1 | import { 2 | auth, 3 | FormValidations, 4 | } from "../app/middlewares" 5 | import UsersValidations from "./../app/validations/users.validations" 6 | import Router from "./../libraries/controller/router" 7 | 8 | Router.get("/", "UsersController@index") 9 | Router.get("/:userId", "UsersController@show") 10 | Router.post("/", [auth, UsersValidations.create, FormValidations], "UsersController@create") 11 | Router.patch("/:userId", [auth, UsersValidations.update, FormValidations], "UsersController@update") 12 | Router.post("/:userId/change-password", [auth, UsersValidations.passwordChange, FormValidations], "UsersController@changePassword") 13 | Router.delete("/:userId", auth, "UsersController@delete") 14 | 15 | export default Router.export() 16 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import { createServer } from "http" 2 | import app from "./bootstrap/app" 3 | import { AppConfig } from "./config" 4 | import { 5 | normalizePort, 6 | onServerError, 7 | onServerListening, 8 | } from "./helpers/core.helper" 9 | 10 | (async () => { 11 | const port = normalizePort(AppConfig.port) 12 | app.set("port", port) 13 | 14 | const server = createServer(app) 15 | server.listen(port) 16 | server.on("listening", onServerListening.bind(this, server)) 17 | server.on("error", onServerError) 18 | 19 | process.on("unhandledRejection", (reason, p) => { 20 | console.error("Unhandled Rejection at:", p, "reason:", reason) 21 | }) 22 | })() 23 | --------------------------------------------------------------------------------