├── .dockerignore ├── .env.example ├── .github └── workflows │ ├── auto-merge-dependabot.yml │ └── continuous-integration-workflow.yml ├── .gitignore ├── .prettierrc ├── Dockerfile ├── LICENSE ├── __tests__ └── Unit │ └── CommonTest.ts ├── docker-compose.yml ├── fix-module-alias.js ├── jest.config.js ├── nodemon.json ├── package-lock.json ├── package.json ├── readme.md ├── src ├── api │ ├── commands │ │ └── Common │ │ │ └── VersionCommand.ts │ ├── controllers │ │ ├── Auth │ │ │ ├── LoginController.ts │ │ │ └── RegisterController.ts │ │ ├── Chat │ │ │ └── ChatSocketController.ts │ │ └── Users │ │ │ └── UserController.ts │ ├── cron-jobs │ │ └── Common │ │ │ └── ExampleCronJob.ts │ ├── events │ │ └── Users │ │ │ └── UserEvent.ts │ ├── exceptions │ │ ├── Application │ │ │ └── ForbiddenException.ts │ │ ├── Auth │ │ │ └── InvalidCredentials.ts │ │ └── Users │ │ │ └── UserNotFoundException.ts │ ├── interfaces │ │ └── users │ │ │ └── LoggedUserInterface.ts │ ├── models │ │ └── Users │ │ │ ├── Role.ts │ │ │ └── User.ts │ ├── queue-jobs │ │ └── Users │ │ │ └── SendWelcomeMail.ts │ ├── repositories │ │ └── Users │ │ │ ├── RoleRepository.ts │ │ │ └── UserRepository.ts │ ├── requests │ │ ├── Auth │ │ │ ├── LoginRequest.ts │ │ │ └── RegisterRequest.ts │ │ └── Users │ │ │ ├── UserCreateRequest.ts │ │ │ └── UserUpdateRequest.ts │ ├── resolvers │ │ └── Users │ │ │ └── UserResolver.ts │ ├── services │ │ ├── Auth │ │ │ ├── LoginService.ts │ │ │ └── RegisterService.ts │ │ └── Users │ │ │ └── UserService.ts │ └── types │ │ └── Users │ │ └── Users.ts ├── config │ ├── app.ts │ ├── auth.ts │ ├── db.ts │ ├── filesystems.ts │ ├── hashing.ts │ └── mail.ts ├── database │ ├── factories │ │ └── UserFactory.ts │ ├── migrations │ │ ├── 1618771206804-CreateRolesTable.ts │ │ └── 1618771301779-CreateUsersTable.ts │ └── seeds │ │ ├── CreateRoles.ts │ │ └── CreateUsers.ts ├── decorators │ ├── EventDispatcher.ts │ ├── LoggedUser.ts │ └── SocketIoClient.ts ├── infrastructure │ ├── abstracts │ │ ├── ControllerBase.ts │ │ ├── EntityBase.ts │ │ ├── MailTemplateBase.ts │ │ ├── QueueJobBase.ts │ │ └── RepositoryBase.ts │ ├── middlewares │ │ ├── Application │ │ │ └── CustomErrorHandler.ts │ │ └── Auth │ │ │ ├── AuthCheck.ts │ │ │ └── HasRole.ts │ └── services │ │ ├── auth │ │ ├── AuthService.ts │ │ └── Providers │ │ │ └── JWTProvider.ts │ │ ├── hash │ │ ├── HashService.ts │ │ └── Providers │ │ │ └── BcryptProvider.ts │ │ ├── mail │ │ ├── Interfaces │ │ │ └── MailInterface.ts │ │ ├── MailGenerator.ts │ │ ├── MailService.ts │ │ ├── Providers │ │ │ └── SmtpProvider.ts │ │ └── Templates │ │ │ └── ForgotPasswordTemplate.ts │ │ └── storage │ │ ├── Providers │ │ └── LocalDisk.ts │ │ └── StorageService.ts ├── main.ts ├── public │ ├── assets │ │ └── .gitignore │ └── uploads │ │ └── .gitignore ├── utils │ ├── cli.ts │ ├── env.ts │ ├── fix-module-alias.ts │ ├── load-event-dispatcher.ts │ ├── load-helmet.ts │ └── to-bool.ts └── views │ └── chat │ └── index.html └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | /node_modules 3 | .eslintrc 4 | .eslintignore 5 | .editorconfig 6 | .huskyrc 7 | .lintstagedrc.json 8 | .prettierrc 9 | jest.config.js 10 | Dockerfile 11 | docker-compose.yml 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # App 2 | APP_NAME=express-typescript-starter 3 | APP_PORT=3000 4 | APP_ROUTE_PREFIX=/api 5 | JWT_SECRET=RANDOM_STRING_5729151279 6 | APP_URL=http://localhost:3000 7 | 8 | # DB 9 | TYPEORM_CONNECTION=mysql 10 | TYPEORM_HOST=127.0.0.1 11 | TYPEORM_PORT=3306 12 | TYPEORM_USERNAME=kutia 13 | TYPEORM_PASSWORD=6028201644 14 | TYPEORM_DATABASE=express-typescript 15 | TYPEORM_SYNCHRONIZE=false 16 | TYPEORM_LOGGING=TRUE 17 | TYPEORM_ENTITIES=src/api/models/**/*.ts,src/api/models/**/*.js 18 | TYPEORM_MIGRATIONS=src/database/migrations/*.ts,src/database/migrations/*.js 19 | TYPEORM_DRIVER_EXTRA='{"bigNumberStrings": false}' 20 | 21 | # MAIL 22 | MAIL_PROVIDER=smtp 23 | MAIL_HOST=smtp.gmail.com 24 | MAIL_PORT=465 25 | MAIL_AUTH_USER=example@gmail.com 26 | MAIL_AUTH_PASSWORD=password 27 | MAIL_FROM_NAME='My App' 28 | 29 | # Cron Jobs 30 | ENABLE_CRON_JOBS=false 31 | 32 | # Graph QL 33 | ENABLE_GRAPHQL=true 34 | 35 | # Path 36 | TYPEORM_ENTITIES_DIR=src/api/models 37 | TYPEORM_MIGRATIONS_DIR=src/database/migrations 38 | TYPEORM_SEEDING_FACTORIES=src/database/factories/**/*{.ts,.js} 39 | TYPEORM_SEEDING_SEEDS=src/database/seeds/**/*{.ts,.js} 40 | CONTROLLERS_DIR=/api/controllers/**/*Controller{.ts,.js} 41 | CRON_JOBS_DIR=/api/cron-jobs/**/*Job{.ts,.js} 42 | MIDDLEWARES_DIR=/infrastructure/middlewares/**/*{.ts,.js} 43 | SUBSCRIBERS_DIR=/api/subscribers/**/*Subscriber{.ts,.js} 44 | EVENTS_DIR=/api/events/**/*{.ts,.js} 45 | RESOLVERS_DIR=/api/resolvers/**/*Resolver{.ts,.js} -------------------------------------------------------------------------------- /.github/workflows/auto-merge-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto Merge Dependabot' 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | auto-merge: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 12 | with: 13 | target: minor 14 | github-token: ${{ secrets.TOKEN_GIT }} 15 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration-workflow.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | checks: 5 | name: Linters 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v1 9 | - uses: actions/setup-node@v1 10 | - run: npm ci --ignore-scripts 11 | - run: npm run code:check 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | - uses: actions/setup-node@v1 18 | - run: npm ci --ignore-scripts 19 | - run: npm run build 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env 4 | temp 5 | src/schema.gql 6 | .vscode 7 | .idea -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "printWidth": 150, 5 | "bracketSpacing": true, 6 | "arrowParens": "always", 7 | "insertPragma": false, 8 | "proseWrap": "preserve", 9 | "quoteProps": "as-needed", 10 | "requirePragma": false, 11 | "singleQuote": true, 12 | "trailingComma": "all", 13 | "useTabs": false 14 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Common build stage 2 | FROM node:14.14.0-alpine3.12 as common-build-stage 3 | 4 | COPY . ./app 5 | 6 | WORKDIR /app 7 | 8 | RUN npm install 9 | 10 | EXPOSE 3000 11 | 12 | # Dvelopment build stage 13 | FROM common-build-stage as development-build-stage 14 | 15 | ENV NODE_ENV development 16 | 17 | CMD ["npm", "run", "dev"] 18 | 19 | # Production build stage 20 | FROM common-build-stage as production-build-stage 21 | 22 | ENV NODE_ENV production 23 | 24 | CMD ["npm", "run", "start"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gentrit Abazi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__tests__/Unit/CommonTest.ts: -------------------------------------------------------------------------------- 1 | describe("CommonTest", function () { 2 | it("1 + 1 is equal 2", function () { 3 | const result = 1 + 1; 4 | 5 | expect(result).toBe(2); 6 | }); 7 | }); -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | server: 5 | build: 6 | context: ./ 7 | target: development-build-stage 8 | ports: 9 | - "${APP_PORT:-3000}:3000" 10 | volumes: 11 | - ./:/app 12 | - /app/node_modules 13 | restart: "unless-stopped" 14 | networks: 15 | - backend 16 | depends_on: 17 | - mysql 18 | - redis 19 | 20 | mysql: 21 | image: "mysql:8.0" 22 | environment: 23 | MYSQL_ROOT_PASSWORD: "${TYPEORM_PASSWORD}" 24 | MYSQL_DATABASE: "${TYPEORM_DATABASE}" 25 | MYSQL_USER: "${TYPEORM_USERNAME}" 26 | MYSQL_PASSWORD: "${TYPEORM_PASSWORD}" 27 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" 28 | ports: 29 | - "${FORWARD_DB_PORT:-3306}:3306" 30 | volumes: 31 | - "backendmysql:/var/lib/mysql" 32 | networks: 33 | - backend 34 | healthcheck: 35 | test: ["CMD", "mysqladmin", "ping"] 36 | 37 | redis: 38 | image: "redis:alpine" 39 | ports: 40 | - "${FORWARD_REDIS_PORT:-6379}:6379" 41 | volumes: 42 | - "backendredis:/data" 43 | networks: 44 | - backend 45 | healthcheck: 46 | test: ["CMD", "redis-cli", "ping"] 47 | 48 | mailhog: 49 | image: "mailhog/mailhog:latest" 50 | ports: 51 | - '${FORWARD_MAILHOG_PORT:-1025}:1025' 52 | - '${FORWARD_MAILHOG_DASHBOARD_PORT:-8025}:8025' 53 | networks: 54 | - backend 55 | 56 | networks: 57 | backend: 58 | driver: bridge 59 | 60 | volumes: 61 | backendmysql: 62 | driver: local 63 | backendredis: 64 | driver: local 65 | -------------------------------------------------------------------------------- /fix-module-alias.js: -------------------------------------------------------------------------------- 1 | const moduleAlias = require('module-alias'); 2 | 3 | let folder = ''; 4 | 5 | if (process.env.NODE_ENV == 'production') { 6 | folder = 'dist'; 7 | } else { 8 | folder = 'src'; 9 | } 10 | 11 | moduleAlias.addAliases({ 12 | '@base': __dirname + '/' + folder, 13 | '@api': __dirname + '/' + folder + '/api' 14 | }); -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | moduleNameMapper: { 5 | "@exmpl/(.*)": "/src/$1" 6 | }, 7 | }; -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "**/*.test.ts", 4 | "**/*.spec.ts", 5 | "node_modules" 6 | ], 7 | "watch": [ 8 | "src" 9 | ], 10 | "exec": "npm start", 11 | "ext": "ts" 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-typescript", 3 | "version": "1.0.0", 4 | "description": "Express typescript.", 5 | "dependencies": { 6 | "@types/glob": "^7.1.4", 7 | "@types/jsonwebtoken": "^8.5.5", 8 | "@types/mailgen": "^2.0.5", 9 | "@types/socket.io": "^2.1.13", 10 | "@types/swagger-ui-express": "^4.1.3", 11 | "@types/yargs": "^16.0.4", 12 | "bcrypt": "^5.0.1", 13 | "body-parser": "^1.19.0", 14 | "bullmq": "^1.46.4", 15 | "class-transformer": "^0.4.0", 16 | "class-validator": "^0.13.1", 17 | "class-validator-jsonschema": "^3.1.0", 18 | "cors": "^2.8.5", 19 | "cron-decorators": "^0.1.5", 20 | "dotenv": "^8.6.0", 21 | "event-dispatch": "^0.4.1", 22 | "express": "^4.17.1", 23 | "express-graphql": "^0.12.0", 24 | "glob": "^7.1.7", 25 | "graphql": "^15.5.3", 26 | "helmet": "^4.6.0", 27 | "jsonwebtoken": "^8.5.1", 28 | "mailgen": "^2.0.15", 29 | "module-alias": "^2.2.2", 30 | "multer": "^1.4.3", 31 | "mysql2": "^2.3.0", 32 | "nodemailer": "^6.6.3", 33 | "reflect-metadata": "^0.1.13", 34 | "routing-controllers": "^0.9.0", 35 | "routing-controllers-openapi": "^3.1.0", 36 | "socket-controllers": "0.0.5", 37 | "socket.io": "^3.0.0", 38 | "swagger-ui-express": "^4.1.6", 39 | "type-graphql": "^1.1.1", 40 | "typedi": "^0.10.0", 41 | "typeorm": "^0.2.37", 42 | "typeorm-seeding": "^1.6.1", 43 | "typeorm-simple-query-parser": "^1.0.16", 44 | "typeorm-typedi-extensions": "^0.4.1", 45 | "yargs": "^16.2.0" 46 | }, 47 | "devDependencies": { 48 | "@types/bcrypt": "^3.0.1", 49 | "@types/body-parser": "^1.19.1", 50 | "@types/cron": "^1.7.3", 51 | "@types/express": "^4.17.13", 52 | "@types/faker": "^5.5.8", 53 | "@types/jest": "^26.0.24", 54 | "@types/module-alias": "^2.0.1", 55 | "@types/multer": "^1.4.7", 56 | "@types/node": "^14.17.15", 57 | "@types/nodemailer": "^6.4.4", 58 | "@types/supertest": "^2.0.11", 59 | "jest": "^27.1.1", 60 | "nodemon": "^2.0.12", 61 | "prettier": "^2.4.0", 62 | "ts-jest": "^27.0.5", 63 | "ts-node": "^9.1.1", 64 | "typescript": "^4.4.3" 65 | }, 66 | "scripts": { 67 | "dev": "NODE_ENV=development ./node_modules/.bin/nodemon --exec './node_modules/.bin/ts-node' src/main.ts", 68 | "start": "npm run build && NODE_ENV=production node ./dist/main.js", 69 | "build": "tsc", 70 | "typeorm": "ts-node -r ./fix-module-alias.js ./node_modules/.bin/typeorm", 71 | "make:migration": "npm run typeorm -- migration:create -n", 72 | "migrate": "npm run typeorm -- migration:run", 73 | "seed:config": "ts-node -r ./fix-module-alias.js ./node_modules/typeorm-seeding/dist/cli.js config", 74 | "db:seed": "ts-node -r ./fix-module-alias.js ./node_modules/typeorm-seeding/dist/cli.js seed", 75 | "db:seed:production": "NODE_ENV=production ts-node -r ./fix-module-alias.js ./node_modules/typeorm-seeding/dist/cli.js seed", 76 | "code:format": "prettier --write \"src/**/*.{ts,css,js,html}\"", 77 | "code:check": "prettier --check \"src/**/*.{ts,css,js,html}\"", 78 | "code:format:specific-file": "prettier --write ", 79 | "test": "jest", 80 | "cli": "ts-node -r ./fix-module-alias.js src/utils/cli.ts" 81 | }, 82 | "repository": { 83 | "type": "git", 84 | "url": "https://github.com/kutia-software-company/express-typescript-starter" 85 | }, 86 | "keywords": [ 87 | "expressjs", 88 | "express-typescript" 89 | ], 90 | "author": "Gentrit Abazi", 91 | "license": "MIT", 92 | "bugs": { 93 | "url": "https://github.com/kutia-software-company/express-typescript-starter/issues" 94 | }, 95 | "homepage": "https://github.com/kutia-software-company/express-typescript-starter#readme" 96 | } 97 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ### Introduction 2 | 3 | Project is a faster way to building a Node.js RESTful API in TypeScript. 4 | 5 | Start use now and just focus on your business and not spending hours in project configuration. 6 | 7 | ### Features 8 | 9 | - **Beautiful Code** thanks to the awesome annotations of the libraries from [pleerock](https://github.com/pleerock). 10 | - **Dependency Injection** done with the nice framework from [TypeDI](https://github.com/pleerock/typedi). 11 | - **Simplified Database Query** with the ORM [TypeORM](https://github.com/typeorm/typeorm). 12 | - **Clear Structure** with different layers such as controllers, services, repositories, models, middlewares... 13 | - **Easy Exception Handling** thanks to [routing-controllers](https://github.com/pleerock/routing-controllers). 14 | - **Smart Validation** thanks to [class-validator](https://github.com/pleerock/class-validator) with some nice annotations. 15 | - **Custom Validators** to validate your request even better and stricter ([custom-validation-classes](https://github.com/pleerock/class-validator#custom-validation-classes)). 16 | - **Basic Security Features** thanks to [Helmet](https://helmetjs.github.io/). 17 | - **Authentication and Authorization** thanks to [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken). 18 | - **CLI Commands** thanks to [yargs](https://github.com/yargs/yargs). 19 | - **Easy event dispatching** thanks to [event-dispatch](https://github.com/pleerock/event-dispatch). 20 | - **Fast Database Building** with simple migration from [TypeORM](https://github.com/typeorm/typeorm). 21 | - **Easy Data Seeding** with our own factories. 22 | - **Auth System** thanks to [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken). 23 | - **Docker** thanks to [docker](https://github.com/docker). 24 | - **Class-based to handle websocket events** thanks to [socket-controllers](https://github.com/typestack/socket-controllers). 25 | - **Class-based to handle Cron Jobs** thanks to [cron-decorators](https://github.com/mrbandler/cron-decorators). 26 | - **API Documentation** thanks to [swagger](http://swagger.io/) and [routing-controllers-openapi](https://github.com/epiphone/routing-controllers-openapi). 27 | - **GraphQL** thanks to [TypeGraphQL](https://19majkel94.github.io/type-graphql/) we have a some cool decorators to simplify the usage of GraphQL. 28 | - **Queue Jobs** thanks to [BullMQ](https://github.com/taskforcesh/bullmq). 29 | - **Query Parser** thanks to [Typeorm Query Parser](https://github.com/gentritabazi01/typeorm-simple-query-parser). 30 | 31 | ### Documentation 32 | 33 | https://kutia-software-company.github.io/express-typescript-starter 34 | 35 | ### License 36 | 37 | [MIT](/LICENSE) 38 | -------------------------------------------------------------------------------- /src/api/commands/Common/VersionCommand.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from 'yargs'; 2 | 3 | export class VersionCommand implements yargs.CommandModule { 4 | public command = 'version'; 5 | public describe = 'Prints Application version.'; 6 | 7 | public async handler() { 8 | console.log(process.env.npm_package_version); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/api/controllers/Auth/LoginController.ts: -------------------------------------------------------------------------------- 1 | import { JsonController, Body, Post } from 'routing-controllers'; 2 | import { Service } from 'typedi'; 3 | import { LoginRequest } from '@api/requests/Auth/LoginRequest'; 4 | import { LoginService } from '@api/services/Auth/LoginService'; 5 | import { ControllerBase } from '@base/infrastructure/abstracts/ControllerBase'; 6 | import { OpenAPI } from 'routing-controllers-openapi'; 7 | 8 | @Service() 9 | @OpenAPI({ 10 | tags: ['Auth'], 11 | }) 12 | @JsonController('/login') 13 | export class LoginController extends ControllerBase { 14 | public constructor(private loginService: LoginService) { 15 | super(); 16 | } 17 | 18 | @Post() 19 | public async login(@Body() user: LoginRequest) { 20 | return await this.loginService.login(user); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/api/controllers/Auth/RegisterController.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { JsonController, Body, Post } from 'routing-controllers'; 3 | import { RegisterRequest } from '@api/requests/Auth/RegisterRequest'; 4 | import { RegisterService } from '@api/services/Auth/RegisterService'; 5 | import { ControllerBase } from '@base/infrastructure/abstracts/ControllerBase'; 6 | import { OpenAPI } from 'routing-controllers-openapi'; 7 | 8 | @Service() 9 | @OpenAPI({ 10 | tags: ['Auth'], 11 | }) 12 | @JsonController('/register') 13 | export class RegisterController extends ControllerBase { 14 | public constructor(private registerService: RegisterService) { 15 | super(); 16 | } 17 | 18 | @Post() 19 | public async register(@Body() user: RegisterRequest) { 20 | return await this.registerService.register(user); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/api/controllers/Chat/ChatSocketController.ts: -------------------------------------------------------------------------------- 1 | import { OnConnect, SocketController, ConnectedSocket, SocketIO, OnDisconnect, MessageBody, OnMessage } from 'socket-controllers'; 2 | import { Service } from 'typedi'; 3 | 4 | @Service() 5 | @SocketController() 6 | export class ChatSocketController { 7 | @OnConnect() 8 | connection(@ConnectedSocket() socket: any) { 9 | console.log('Client connected.'); 10 | } 11 | 12 | @OnDisconnect() 13 | disconnect(@ConnectedSocket() socket: any) { 14 | console.log('Client disconnected.'); 15 | } 16 | 17 | @OnMessage('save-message') 18 | save(@SocketIO() socket: any, @MessageBody() message: any) { 19 | console.log('Received message: ', message); 20 | console.log('Setting id to the message and sending it back to the client.'); 21 | message.id = 1; 22 | 23 | socket.emit('message-saved', message); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/api/controllers/Users/UserController.ts: -------------------------------------------------------------------------------- 1 | import { Param, Get, JsonController, Post, Body, Put, Delete, HttpCode, UseBefore, QueryParams } from 'routing-controllers'; 2 | import { UserService } from '@api/services/Users/UserService'; 3 | import { Service } from 'typedi'; 4 | import { UserCreateRequest } from '@api/requests/Users/UserCreateRequest'; 5 | import { AuthCheck } from '@base/infrastructure/middlewares/Auth/AuthCheck'; 6 | import { ControllerBase } from '@base/infrastructure/abstracts/ControllerBase'; 7 | import { UserUpdateRequest } from '@api/requests/Users/UserUpdateRequest'; 8 | import { OpenAPI } from 'routing-controllers-openapi'; 9 | import { RequestQueryParser } from 'typeorm-simple-query-parser'; 10 | import { LoggedUser } from '@base/decorators/LoggedUser'; 11 | import { LoggedUserInterface } from '@api/interfaces/users/LoggedUserInterface'; 12 | 13 | @Service() 14 | @OpenAPI({ 15 | security: [{ bearerAuth: [] }], 16 | }) 17 | @JsonController('/users') 18 | @UseBefore(AuthCheck) 19 | export class UserController extends ControllerBase { 20 | public constructor(private userService: UserService) { 21 | super(); 22 | } 23 | 24 | @Get() 25 | public async getAll(@QueryParams() parseResourceOptions: RequestQueryParser) { 26 | const resourceOptions = parseResourceOptions.getAll(); 27 | 28 | return await this.userService.getAll(resourceOptions); 29 | } 30 | 31 | @Get('/:id([0-9]+)') 32 | public async getOne(@Param('id') id: number, @QueryParams() parseResourceOptions: RequestQueryParser) { 33 | const resourceOptions = parseResourceOptions.getAll(); 34 | 35 | return await this.userService.findOneById(id, resourceOptions); 36 | } 37 | 38 | @Get('/me') 39 | public async getMe(@QueryParams() parseResourceOptions: RequestQueryParser, @LoggedUser() loggedUser: LoggedUserInterface) { 40 | const resourceOptions = parseResourceOptions.getAll(); 41 | 42 | return await this.userService.findOneById(loggedUser.userId, resourceOptions); 43 | } 44 | 45 | @Post() 46 | @HttpCode(201) 47 | public async create(@Body() user: UserCreateRequest) { 48 | return await this.userService.create(user); 49 | } 50 | 51 | @Put('/:id') 52 | public async update(@Param('id') id: number, @Body() user: UserUpdateRequest) { 53 | return await this.userService.updateOneById(id, user); 54 | } 55 | 56 | @Delete('/:id') 57 | @HttpCode(204) 58 | public async delete(@Param('id') id: number) { 59 | return await this.userService.deleteOneById(id); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/api/cron-jobs/Common/ExampleCronJob.ts: -------------------------------------------------------------------------------- 1 | import { CronController as CronJobClass, Cron } from 'cron-decorators'; 2 | import { Service as Injectable } from 'typedi'; 3 | 4 | @Injectable() 5 | @CronJobClass() 6 | export class ExampleCronJob { 7 | @Cron('Log every second', '* * * * * *') 8 | public async handle(): Promise { 9 | console.log('I am cron Job and I just ran!'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/api/events/Users/UserEvent.ts: -------------------------------------------------------------------------------- 1 | import { EventSubscriber, On } from 'event-dispatch'; 2 | import { SendWelcomeMail } from '@api/queue-jobs/Users/SendWelcomeMail'; 3 | 4 | @EventSubscriber() 5 | export class UserEvent { 6 | @On('onUserRegister') 7 | public onUserRegister(user: any) { 8 | new SendWelcomeMail(user).setOptions({ delay: 5000 }).dispatch(); 9 | } 10 | 11 | @On('onUserCreate') 12 | public onUserCreate(user: any) { 13 | console.log('User ' + user.email + ' created!'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/api/exceptions/Application/ForbiddenException.ts: -------------------------------------------------------------------------------- 1 | import { ForbiddenError } from 'routing-controllers'; 2 | 3 | export class ForbiddenException extends ForbiddenError { 4 | constructor() { 5 | super('Forbidden!'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/api/exceptions/Auth/InvalidCredentials.ts: -------------------------------------------------------------------------------- 1 | import { UnauthorizedError } from 'routing-controllers'; 2 | 3 | export class InvalidCredentials extends UnauthorizedError { 4 | constructor() { 5 | super('Invalid credentials!'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/api/exceptions/Users/UserNotFoundException.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundError } from 'routing-controllers'; 2 | 3 | export class UserNotFoundException extends NotFoundError { 4 | constructor() { 5 | super('User not found!'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/api/interfaces/users/LoggedUserInterface.ts: -------------------------------------------------------------------------------- 1 | export interface LoggedUserInterface { 2 | userId: number; 3 | email: string; 4 | role_id: number; 5 | role: string; 6 | iat: number; 7 | exp: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/api/models/Users/Role.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { EntityBase } from '@base/infrastructure/abstracts/EntityBase'; 3 | 4 | @Entity({ name: 'roles' }) 5 | export class Role extends EntityBase { 6 | @PrimaryGeneratedColumn('increment') 7 | id: number; 8 | 9 | @Column() 10 | name: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/api/models/Users/User.ts: -------------------------------------------------------------------------------- 1 | import { BeforeInsert, BeforeUpdate, Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { EntityBase } from '@base/infrastructure/abstracts/EntityBase'; 3 | import { Exclude, Expose } from 'class-transformer'; 4 | import { Role } from './Role'; 5 | import { HashService } from '@base/infrastructure/services/hash/HashService'; 6 | 7 | @Entity({ name: 'users' }) 8 | export class User extends EntityBase { 9 | @PrimaryGeneratedColumn('increment') 10 | id: number; 11 | 12 | @Column() 13 | first_name: string; 14 | 15 | @Column() 16 | last_name: string; 17 | 18 | @Column() 19 | email: string; 20 | 21 | @Column() 22 | @Exclude() 23 | password: string; 24 | 25 | @Column() 26 | role_id: number; 27 | 28 | @OneToOne(() => Role) 29 | @JoinColumn({ name: 'role_id' }) 30 | role: Role; 31 | 32 | @Expose({ name: 'full_name' }) 33 | get fullName() { 34 | return this.first_name + ' ' + this.last_name; 35 | } 36 | 37 | @BeforeInsert() 38 | @BeforeUpdate() 39 | async setPassword() { 40 | if (this.password) this.password = await new HashService().make(this.password); 41 | } 42 | 43 | @BeforeInsert() 44 | async setDefaultRole() { 45 | const roleId = this.role_id ? this.role_id : 2; 46 | 47 | this.role_id = roleId; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/api/queue-jobs/Users/SendWelcomeMail.ts: -------------------------------------------------------------------------------- 1 | import { QueueJobBase } from '@base/infrastructure/abstracts/QueueJobBase'; 2 | import { Job } from 'bullmq'; 3 | 4 | export class SendWelcomeMail extends QueueJobBase { 5 | /** 6 | * Create a new job instance. 7 | */ 8 | public constructor(data: any) { 9 | super(data); 10 | } 11 | 12 | /** 13 | * Execute the job. 14 | */ 15 | public async handle(job: Job) { 16 | const user = job.data; 17 | 18 | console.log('Recieved job', job.name); 19 | console.log(user); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/api/repositories/Users/RoleRepository.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '@api/models/Users/Role'; 2 | import { EntityRepository } from 'typeorm'; 3 | import { RepositoryBase } from '@base/infrastructure/abstracts/RepositoryBase'; 4 | 5 | @EntityRepository(Role) 6 | export class RoleRepository extends RepositoryBase { 7 | public async createRole(data: object) { 8 | let entity = new Role(); 9 | 10 | Object.assign(entity, data); 11 | 12 | return await this.save(entity); 13 | } 14 | 15 | public async updateRole(role: Role, data: object) { 16 | Object.assign(role, data); 17 | 18 | return await role.save(data); 19 | } 20 | 21 | public async createRoles(data: any[]) { 22 | const roles: Role[] = []; 23 | 24 | data.forEach((element) => { 25 | const role = new Role(); 26 | role.name = element.name; 27 | roles.push(role); 28 | }); 29 | 30 | await this.save(roles); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/api/repositories/Users/UserRepository.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@api/models/Users/User'; 2 | import { EntityRepository } from 'typeorm'; 3 | import { RepositoryBase } from '@base/infrastructure/abstracts/RepositoryBase'; 4 | 5 | @EntityRepository(User) 6 | export class UserRepository extends RepositoryBase { 7 | public async createUser(data: object) { 8 | let entity = new User(); 9 | 10 | Object.assign(entity, data); 11 | 12 | return await this.save(entity); 13 | } 14 | 15 | public async updateUser(user: User, data: object) { 16 | Object.assign(user, data); 17 | 18 | return await user.save(data); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/api/requests/Auth/LoginRequest.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class LoginRequest { 4 | @IsNotEmpty() 5 | @IsString() 6 | email: string; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | password: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/api/requests/Auth/RegisterRequest.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsEmail, IsString, MinLength, MaxLength } from 'class-validator'; 2 | 3 | export class RegisterRequest { 4 | @MaxLength(20) 5 | @MinLength(2) 6 | @IsString() 7 | @IsNotEmpty() 8 | first_name: string; 9 | 10 | @MaxLength(20) 11 | @MinLength(2) 12 | @IsString() 13 | @IsNotEmpty() 14 | last_name: string; 15 | 16 | @IsEmail() 17 | @IsString() 18 | @IsNotEmpty() 19 | email: string; 20 | 21 | @MaxLength(20) 22 | @MinLength(6) 23 | @IsString() 24 | @IsNotEmpty() 25 | password: string; 26 | } 27 | -------------------------------------------------------------------------------- /src/api/requests/Users/UserCreateRequest.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsEmail, IsString, MinLength, MaxLength } from 'class-validator'; 2 | 3 | export class UserCreateRequest { 4 | @MaxLength(20) 5 | @MinLength(2) 6 | @IsString() 7 | @IsNotEmpty() 8 | first_name: string; 9 | 10 | @MaxLength(20) 11 | @MinLength(2) 12 | @IsString() 13 | @IsNotEmpty() 14 | last_name: string; 15 | 16 | @IsEmail() 17 | @IsString() 18 | @IsNotEmpty() 19 | email: string; 20 | 21 | @MaxLength(20) 22 | @MinLength(6) 23 | @IsString() 24 | @IsNotEmpty() 25 | password: string; 26 | } 27 | -------------------------------------------------------------------------------- /src/api/requests/Users/UserUpdateRequest.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString, MinLength, MaxLength, IsOptional } from 'class-validator'; 2 | 3 | export class UserUpdateRequest { 4 | @MaxLength(20) 5 | @MinLength(2) 6 | @IsString() 7 | @IsOptional() 8 | first_name: string; 9 | 10 | @MaxLength(20) 11 | @MinLength(2) 12 | @IsString() 13 | @IsOptional() 14 | last_name: string; 15 | 16 | @IsEmail() 17 | @IsString() 18 | @IsOptional() 19 | email: string; 20 | 21 | @MaxLength(20) 22 | @MinLength(6) 23 | @IsString() 24 | @IsOptional() 25 | password: string; 26 | } 27 | -------------------------------------------------------------------------------- /src/api/resolvers/Users/UserResolver.ts: -------------------------------------------------------------------------------- 1 | import { Query, Resolver } from 'type-graphql'; 2 | import { Service } from 'typedi'; 3 | import { UserService } from '@api/services/Users/UserService'; 4 | import { Users } from '@api/types/Users/Users'; 5 | 6 | @Service() 7 | @Resolver((of) => Users) 8 | export class UserResolver { 9 | constructor(private userService: UserService) {} 10 | 11 | @Query((returns) => [Users]) 12 | public async users(): Promise { 13 | return await this.userService.getAll().then((result) => { 14 | return result.rows; 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/api/services/Auth/LoginService.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { UserRepository } from '@api/repositories/Users/UserRepository'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | import { InvalidCredentials } from '@api/exceptions/Auth/InvalidCredentials'; 5 | import { AuthService } from '@base/infrastructure/services/auth/AuthService'; 6 | import { LoginRequest } from '@base/api/requests/Auth/LoginRequest'; 7 | import { HashService } from '@base/infrastructure/services/hash/HashService'; 8 | 9 | @Service() 10 | export class LoginService { 11 | constructor(@InjectRepository() private userRepository: UserRepository, private authService: AuthService, private hashService: HashService) { 12 | // 13 | } 14 | 15 | public async login(data: LoginRequest) { 16 | let user = await this.userRepository.findOne({ 17 | where: { email: data.email }, 18 | relations: ['role'], 19 | }); 20 | 21 | if (!user) { 22 | throw new InvalidCredentials(); 23 | } 24 | 25 | if (!(await this.hashService.compare(data.password, user.password))) { 26 | throw new InvalidCredentials(); 27 | } 28 | 29 | return this.authService.sign( 30 | { 31 | userId: user.id, 32 | email: user.email, 33 | role_id: user.role_id, 34 | role: user.role.name, 35 | }, 36 | { user: { id: user.id, email: user.email, role: user.role.name } }, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/api/services/Auth/RegisterService.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { UserRepository } from '@api/repositories/Users/UserRepository'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | import { EventDispatcher, EventDispatcherInterface } from '@base/decorators/EventDispatcher'; 5 | import { AuthService } from '@base/infrastructure/services/auth/AuthService'; 6 | 7 | @Service() 8 | export class RegisterService { 9 | constructor( 10 | @InjectRepository() private userRepository: UserRepository, 11 | @EventDispatcher() private eventDispatcher: EventDispatcherInterface, 12 | private authService: AuthService, 13 | ) { 14 | // 15 | } 16 | 17 | public async register(data: object) { 18 | let user = await this.userRepository.createUser(data); 19 | 20 | user = await this.userRepository.findOne({ 21 | where: { id: user.id }, 22 | relations: ['role'], 23 | }); 24 | 25 | this.eventDispatcher.dispatch('onUserRegister', user); 26 | 27 | return this.authService.sign( 28 | { 29 | userId: user.id, 30 | email: user.email, 31 | role_id: user.role_id, 32 | role: user.role.name, 33 | }, 34 | { user: { id: user.id, email: user.email, role: user.role.name } }, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/api/services/Users/UserService.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { UserRepository } from '@api/repositories/Users/UserRepository'; 3 | import { UserNotFoundException } from '@api/exceptions/Users/UserNotFoundException'; 4 | import { EventDispatcher, EventDispatcherInterface } from '@base/decorators/EventDispatcher'; 5 | import { InjectRepository } from 'typeorm-typedi-extensions'; 6 | 7 | @Service() 8 | export class UserService { 9 | constructor(@InjectRepository() private userRepository: UserRepository, @EventDispatcher() private eventDispatcher: EventDispatcherInterface) { 10 | // 11 | } 12 | 13 | public async getAll(resourceOptions?: object) { 14 | return await this.userRepository.getManyAndCount(resourceOptions); 15 | } 16 | 17 | public async findOneById(id: number, resourceOptions?: object) { 18 | return await this.getRequestedUserOrFail(id, resourceOptions); 19 | } 20 | 21 | public async create(data: object) { 22 | let user = await this.userRepository.createUser(data); 23 | 24 | this.eventDispatcher.dispatch('onUserCreate', user); 25 | 26 | return user; 27 | } 28 | 29 | public async updateOneById(id: number, data: object) { 30 | const user = await this.getRequestedUserOrFail(id); 31 | 32 | return await this.userRepository.updateUser(user, data); 33 | } 34 | 35 | public async deleteOneById(id: number) { 36 | return await this.userRepository.delete(id); 37 | } 38 | 39 | private async getRequestedUserOrFail(id: number, resourceOptions?: object) { 40 | let user = await this.userRepository.getOneById(id, resourceOptions); 41 | 42 | if (!user) { 43 | throw new UserNotFoundException(); 44 | } 45 | 46 | return user; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/api/types/Users/Users.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from 'type-graphql'; 2 | 3 | @ObjectType({ 4 | description: 'User object.', 5 | }) 6 | export class Users { 7 | @Field((type) => ID) 8 | public id: string; 9 | 10 | @Field({ 11 | description: 'The first name of the user.', 12 | }) 13 | public first_name: string; 14 | 15 | @Field({ 16 | description: 'The last name of the user.', 17 | }) 18 | public last_name: string; 19 | 20 | @Field({ 21 | description: 'The email of the user.', 22 | }) 23 | public email: string; 24 | } 25 | -------------------------------------------------------------------------------- /src/config/app.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@base/utils/env'; 2 | import { toBool } from '@base/utils/to-bool'; 3 | 4 | function getAppPath() { 5 | let currentDir = __dirname; 6 | currentDir = currentDir.replace('/config', ''); 7 | 8 | return currentDir; 9 | } 10 | 11 | export const appConfig = { 12 | node: env('NODE_ENV') || 'development', 13 | isProduction: env('NODE_ENV') === 'production', 14 | isStaging: env('NODE_ENV') === 'staging', 15 | isDevelopment: env('NODE_ENV') === 'development', 16 | name: env('APP_NAME'), 17 | port: Number(env('APP_PORT')), 18 | routePrefix: env('APP_ROUTE_PREFIX'), 19 | url: env('APP_URL'), 20 | appPath: getAppPath(), 21 | 22 | cronJobsEnabled: toBool(env('ENABLE_CRON_JOBS')), 23 | graphqlEnabled: toBool(env('ENABLE_GRAPHQL')), 24 | 25 | entitiesDir: env('TYPEORM_ENTITIES_DIR'), 26 | controllersDir: env('CONTROLLERS_DIR'), 27 | cronJobsDir: env('CRON_JOBS_DIR'), 28 | middlewaresDir: env('MIDDLEWARES_DIR'), 29 | eventsDir: env('EVENTS_DIR'), 30 | subscribersDir: env('SUBSCRIBERS_DIR'), 31 | resolversDir: env('RESOLVERS_DIR'), 32 | }; 33 | -------------------------------------------------------------------------------- /src/config/auth.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@base/utils/env'; 2 | 3 | export const authConfig = { 4 | defaultProvider: env('AUTH_DEFAULT_PROVIDER', 'jwt'), 5 | 6 | providers: { 7 | jwt: { 8 | secret: env('JWT_SECRET'), 9 | expiresIn: '24h', 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/config/db.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@base/utils/env'; 2 | 3 | export const dbConfig = { 4 | dbConnection: env('TYPEORM_CONNECTION'), 5 | dbHost: env('TYPEORM_HOST'), 6 | dbPort: env('TYPEORM_PORT'), 7 | dbDatabase: env('TYPEORM_DATABASE'), 8 | dbUsername: env('TYPEORM_USERNAME'), 9 | dbPassword: env('TYPEORM_PASSWORD'), 10 | dbEntities: env('TYPEORM_ENTITIES'), 11 | allowLogging: env('TYPEORM_LOGGING'), 12 | }; 13 | -------------------------------------------------------------------------------- /src/config/filesystems.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@base/utils/env'; 2 | import { appConfig } from './app'; 3 | 4 | export const fileSystemsConfig = { 5 | defaultDisk: env('FILESYSTEM_DEFAULT_DISK', 'local'), 6 | 7 | disks: { 8 | local: { 9 | root: appConfig.appPath + '/public/uploads', 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/config/hashing.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@base/utils/env'; 2 | 3 | export const hashingConfig = { 4 | defaultDriver: env('HASHING_DEFAULT_DRIVER', 'bcrypt'), 5 | 6 | disks: { 7 | bcrypt: { 8 | defaultRounds: 10, 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/config/mail.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@base/utils/env'; 2 | 3 | export const mailConfig = { 4 | provider: env('MAIL_PROVIDER'), 5 | host: env('MAIL_HOST'), 6 | port: Number(env('MAIL_PORT')), 7 | authUser: env('MAIL_AUTH_USER'), 8 | authPassword: env('MAIL_AUTH_PASSWORD'), 9 | fromName: env('MAIL_FROM_NAME'), 10 | }; 11 | -------------------------------------------------------------------------------- /src/database/factories/UserFactory.ts: -------------------------------------------------------------------------------- 1 | import Faker from 'faker'; 2 | import { define } from 'typeorm-seeding'; 3 | import { User } from '@api/models/Users/User'; 4 | 5 | define(User, (faker: typeof Faker) => { 6 | const firstName = faker.name.firstName(); 7 | const lastName = faker.name.lastName(); 8 | const email = faker.internet.email(firstName, lastName).toLowerCase(); 9 | 10 | const user = new User(); 11 | user.first_name = firstName; 12 | user.last_name = lastName; 13 | user.email = email; 14 | user.password = 'password'; 15 | 16 | return user; 17 | }); 18 | -------------------------------------------------------------------------------- /src/database/migrations/1618771206804-CreateRolesTable.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, Table } from 'typeorm'; 2 | 3 | export class CreateRolesTable1618771206804 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | const table = new Table({ 6 | name: 'roles', 7 | columns: [ 8 | { 9 | name: 'id', 10 | type: 'bigint', 11 | isPrimary: true, 12 | isGenerated: true, 13 | generationStrategy: 'increment', 14 | }, 15 | { name: 'name', type: 'varchar', length: '50', isUnique: true }, 16 | ], 17 | }); 18 | 19 | await queryRunner.createTable(table); 20 | } 21 | 22 | public async down(queryRunner: QueryRunner): Promise { 23 | await queryRunner.dropTable('roles'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/database/migrations/1618771301779-CreateUsersTable.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; 2 | 3 | export class CreateUsersTable1618771301779 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | const table = new Table({ 6 | name: 'users', 7 | columns: [ 8 | { 9 | name: 'id', 10 | type: 'bigint', 11 | isPrimary: true, 12 | isGenerated: true, 13 | generationStrategy: 'increment', 14 | }, 15 | { name: 'first_name', type: 'varchar', length: '191' }, 16 | { name: 'last_name', type: 'varchar', length: '191' }, 17 | { name: 'email', type: 'varchar', length: '191' }, 18 | { name: 'password', type: 'varchar', length: '191' }, 19 | { name: 'role_id', type: 'bigint' }, 20 | ], 21 | }); 22 | 23 | await queryRunner.createTable(table); 24 | 25 | await queryRunner.createForeignKey( 26 | 'users', 27 | new TableForeignKey({ 28 | columnNames: ['role_id'], 29 | referencedTableName: 'roles', 30 | referencedColumnNames: ['id'], 31 | onUpdate: 'CASCADE', 32 | onDelete: 'CASCADE', 33 | }), 34 | ); 35 | } 36 | 37 | public async down(queryRunner: QueryRunner): Promise { 38 | await queryRunner.dropTable('users'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/database/seeds/CreateRoles.ts: -------------------------------------------------------------------------------- 1 | import { Factory, Seeder } from 'typeorm-seeding'; 2 | import { Connection } from 'typeorm/connection/Connection'; 3 | import { RoleRepository } from '@base/api/repositories/Users/RoleRepository'; 4 | 5 | export default class CreateRoles implements Seeder { 6 | public async run(factory: Factory, connection: Connection): Promise { 7 | const roles = [{ name: 'Admin' }, { name: 'Client' }]; 8 | 9 | for (const [key, value] of Object.entries(roles)) { 10 | const role = await connection.getCustomRepository(RoleRepository).findOne({ where: { name: value.name } }); 11 | 12 | if (role) { 13 | continue; 14 | } 15 | 16 | await connection.getCustomRepository(RoleRepository).createRole({ name: value.name }); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/database/seeds/CreateUsers.ts: -------------------------------------------------------------------------------- 1 | import { Factory, Seeder } from 'typeorm-seeding'; 2 | import { Connection } from 'typeorm/connection/Connection'; 3 | import { User } from '@api/models/Users/User'; 4 | 5 | export default class CreateUsers implements Seeder { 6 | public async run(factory: Factory, connection: Connection): Promise { 7 | await factory(User)().createMany(10); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/decorators/EventDispatcher.ts: -------------------------------------------------------------------------------- 1 | import { EventDispatcher as EventDispatcherClass } from 'event-dispatch'; 2 | import { Container } from 'typedi'; 3 | 4 | export function EventDispatcher(): any { 5 | return (object: any, propertyName: string, index?: number): any => { 6 | const eventDispatcher = new EventDispatcherClass(); 7 | Container.registerHandler({ 8 | object, 9 | propertyName, 10 | index, 11 | value: () => eventDispatcher, 12 | }); 13 | }; 14 | } 15 | 16 | export { EventDispatcher as EventDispatcherInterface } from 'event-dispatch'; 17 | -------------------------------------------------------------------------------- /src/decorators/LoggedUser.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from 'routing-controllers'; 2 | 3 | export function LoggedUser() { 4 | return createParamDecorator({ 5 | value: (action) => { 6 | return action.request.loggedUser; 7 | }, 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/decorators/SocketIoClient.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from 'routing-controllers'; 2 | 3 | export function SocketIoClient(options?: { required?: boolean }) { 4 | return createParamDecorator({ 5 | required: options && options.required ? true : false, 6 | value: (action) => { 7 | if (action.request.app) { 8 | return action.request.io; 9 | } 10 | 11 | return undefined; 12 | }, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/infrastructure/abstracts/ControllerBase.ts: -------------------------------------------------------------------------------- 1 | export abstract class ControllerBase { 2 | // 3 | } 4 | -------------------------------------------------------------------------------- /src/infrastructure/abstracts/EntityBase.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity } from 'typeorm'; 2 | 3 | export abstract class EntityBase extends BaseEntity { 4 | // 5 | } 6 | -------------------------------------------------------------------------------- /src/infrastructure/abstracts/MailTemplateBase.ts: -------------------------------------------------------------------------------- 1 | import Mailgen from 'mailgen'; 2 | import { MailGenerator } from '../services/mail/MailGenerator'; 3 | 4 | export abstract class MailTemplateBase { 5 | abstract getTemplate(): Mailgen.Content; 6 | 7 | public getHtmlContent() { 8 | return new MailGenerator().generateHtmlContent(this.getTemplate()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/infrastructure/abstracts/QueueJobBase.ts: -------------------------------------------------------------------------------- 1 | import { Queue, Worker, Processor, QueueScheduler, QueueEvents, JobsOptions } from 'bullmq'; 2 | 3 | export abstract class QueueJobBase { 4 | private queue: Queue; 5 | private queueScheduler: QueueScheduler; 6 | private queueEvents: QueueEvents; 7 | private readonly jobName: string = (this).constructor.name; 8 | private jobOptions: JobsOptions; 9 | private data: any; 10 | 11 | abstract handle(job: any): Promise> | any; 12 | 13 | public constructor(data: any) { 14 | this.data = data; 15 | } 16 | 17 | public process() { 18 | this.queue = new Queue(this.jobName); 19 | this.queueScheduler = new QueueScheduler(this.jobName); 20 | this.queueEvents = new QueueEvents(this.jobName); 21 | 22 | this.queue.add(this.jobName, this.data, this.jobOptions); 23 | const worker = new Worker(this.jobName, this.handle); 24 | 25 | worker.on('completed', this.onCompleted); 26 | worker.on('failed', this.onFailed); 27 | } 28 | 29 | public setOptions(jobOptions: JobsOptions): this { 30 | this.jobOptions = jobOptions; 31 | 32 | return this; 33 | } 34 | 35 | public dispatch() { 36 | this.process(); 37 | } 38 | 39 | public onCompleted(job: any): any { 40 | // 41 | } 42 | 43 | public onFailed(job: any): any { 44 | // 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/infrastructure/abstracts/RepositoryBase.ts: -------------------------------------------------------------------------------- 1 | import { MainRepository } from 'typeorm-simple-query-parser'; 2 | 3 | export abstract class RepositoryBase extends MainRepository {} 4 | -------------------------------------------------------------------------------- /src/infrastructure/middlewares/Application/CustomErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import { ExpressErrorMiddlewareInterface, Middleware, HttpError } from 'routing-controllers'; 2 | import * as express from 'express'; 3 | import { Service } from 'typedi'; 4 | 5 | @Service() 6 | @Middleware({ type: 'after' }) 7 | export class CustomErrorHandler implements ExpressErrorMiddlewareInterface { 8 | public error(error: any, _req: express.Request, res: express.Response, _next: express.NextFunction) { 9 | const responseObject = {} as any; 10 | responseObject.success = false; 11 | 12 | // Status code 13 | if (error instanceof HttpError && error.httpCode) { 14 | responseObject.status = error.httpCode; 15 | res.status(error.httpCode); 16 | } else { 17 | responseObject.status = 500; 18 | res.status(500); 19 | } 20 | 21 | // Message 22 | responseObject.message = error.message; 23 | 24 | // Class validator handle errors 25 | if (responseObject.status == 400) { 26 | let validatorErrors = {} as any; 27 | if (typeof error === 'object' && error.hasOwnProperty('errors')) { 28 | error.errors.forEach((element: any) => { 29 | if (element.property && element.constraints) { 30 | validatorErrors[element.property] = element.constraints; 31 | } 32 | }); 33 | } 34 | responseObject.errors = validatorErrors; 35 | } 36 | 37 | // Append stack 38 | if (error.stack && process.env.NODE_ENV === 'development' && responseObject.status == 500) { 39 | responseObject.stack = error.stack; 40 | } 41 | 42 | // Final response 43 | res.json(responseObject); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/infrastructure/middlewares/Auth/AuthCheck.ts: -------------------------------------------------------------------------------- 1 | import { ExpressMiddlewareInterface } from 'routing-controllers'; 2 | import { Service } from 'typedi'; 3 | import * as jwt from 'jsonwebtoken'; 4 | import { authConfig } from '@base/config/auth'; 5 | import { Response } from 'express'; 6 | 7 | @Service() 8 | export class AuthCheck implements ExpressMiddlewareInterface { 9 | use(request: any, response: Response, next?: (err?: any) => any): any { 10 | const authHeader = request.headers.authorization; 11 | if (!authHeader) { 12 | return response.status(401).send({ status: 403, message: 'Unauthorized!' }); 13 | } 14 | 15 | const token = authHeader.split(' ')[1]; 16 | 17 | jwt.verify(token, authConfig.providers.jwt.secret, (err: any, user: any) => { 18 | if (err) { 19 | return response.status(403).send({ status: 403, message: 'Forbidden!' }); 20 | } 21 | 22 | request.loggedUser = user; 23 | next(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/infrastructure/middlewares/Auth/HasRole.ts: -------------------------------------------------------------------------------- 1 | export function HasRole(role: string | string[]): any { 2 | return function (request: any, response: any, next: any) { 3 | const loggedUser = request.loggedUser; 4 | let haveAccess = true; 5 | 6 | if (!loggedUser) { 7 | return response.status(403).send({ status: 401, message: 'Unauthorized!' }); 8 | } 9 | 10 | if (typeof role == 'string') { 11 | if (loggedUser.role != role) { 12 | haveAccess = false; 13 | } 14 | } else { 15 | if (!role.includes(loggedUser.role)) { 16 | haveAccess = false; 17 | } 18 | } 19 | 20 | if (!haveAccess) { 21 | return response.status(403).send({ 22 | status: 403, 23 | message: 'User does not have the right permissions!', 24 | }); 25 | } 26 | 27 | return next(); 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/infrastructure/services/auth/AuthService.ts: -------------------------------------------------------------------------------- 1 | import { authConfig } from '@base/config/auth'; 2 | import { Service } from 'typedi'; 3 | import { JWTProvider } from './Providers/JWTProvider'; 4 | 5 | @Service() 6 | export class AuthService { 7 | private provider: any; 8 | 9 | public constructor() { 10 | this.setProvider(authConfig.defaultProvider); 11 | } 12 | 13 | public setProvider(provider: string): this { 14 | switch (provider) { 15 | case 'jwt': 16 | this.provider = new JWTProvider(); 17 | 18 | default: 19 | break; 20 | } 21 | 22 | return this; 23 | } 24 | 25 | public sign(payload: object, dataReturn: object): object { 26 | return this.provider.sign(payload, dataReturn); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/infrastructure/services/auth/Providers/JWTProvider.ts: -------------------------------------------------------------------------------- 1 | import { authConfig } from '@base/config/auth'; 2 | import * as jwt from 'jsonwebtoken'; 3 | 4 | export class JWTProvider { 5 | public sign(payload: object, dataReturn: object): object { 6 | return { 7 | ...dataReturn, 8 | access_token: jwt.sign(payload, authConfig.providers.jwt.secret, { 9 | expiresIn: authConfig.providers.jwt.expiresIn, 10 | }), 11 | expires_in: authConfig.providers.jwt.expiresIn, 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/infrastructure/services/hash/HashService.ts: -------------------------------------------------------------------------------- 1 | import { hashingConfig } from '@base/config/hashing'; 2 | import { Service } from 'typedi'; 3 | import { BcryptProvider } from './Providers/BcryptProvider'; 4 | 5 | @Service() 6 | export class HashService { 7 | private provider: any; 8 | 9 | public constructor() { 10 | this.setDriver(hashingConfig.defaultDriver); 11 | } 12 | 13 | public setDriver(provider: string) { 14 | switch (provider) { 15 | case 'bcrypt': 16 | this.provider = new BcryptProvider(); 17 | break; 18 | 19 | default: 20 | break; 21 | } 22 | 23 | return this; 24 | } 25 | 26 | public async make(data: any, saltOrRounds: string | number = 10) { 27 | return await this.provider.make(data, saltOrRounds); 28 | } 29 | 30 | public async compare(data: any, encrypted: string) { 31 | return await this.provider.compare(data, encrypted); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/infrastructure/services/hash/Providers/BcryptProvider.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | import { hashingConfig } from '@base/config/hashing'; 3 | 4 | export class BcryptProvider { 5 | private bcrypt = bcrypt; 6 | private defaultRounds = hashingConfig.disks.bcrypt.defaultRounds; 7 | 8 | public async make(data: any, saltOrRounds: string | number = this.defaultRounds) { 9 | return await this.bcrypt.hash(data, saltOrRounds); 10 | } 11 | 12 | public async compare(data: any, encrypted: string) { 13 | return await this.bcrypt.compare(data, encrypted); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/infrastructure/services/mail/Interfaces/MailInterface.ts: -------------------------------------------------------------------------------- 1 | export interface MailInterface { 2 | from(value: string): this; 3 | 4 | to(value: string): this; 5 | 6 | subject(value: string): this; 7 | 8 | text(value: string): this; 9 | 10 | html(value: string): this; 11 | 12 | send(): Promise; 13 | } 14 | -------------------------------------------------------------------------------- /src/infrastructure/services/mail/MailGenerator.ts: -------------------------------------------------------------------------------- 1 | import Mailgen from 'mailgen'; 2 | import { Service } from 'typedi'; 3 | import { appConfig } from '@base/config/app'; 4 | 5 | @Service() 6 | export class MailGenerator { 7 | private readonly mailGenerator: Mailgen; 8 | 9 | constructor() { 10 | this.mailGenerator = new Mailgen({ 11 | theme: 'default', 12 | product: { 13 | // Appears in header & footer of e-mails 14 | name: appConfig.name, 15 | link: appConfig.url, 16 | }, 17 | }); 18 | } 19 | 20 | generatePlaintext(params: Mailgen.Content): string { 21 | return this.mailGenerator.generatePlaintext(params); 22 | } 23 | 24 | generateHtmlContent(params: Mailgen.Content): string { 25 | return this.mailGenerator.generate(params); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/infrastructure/services/mail/MailService.ts: -------------------------------------------------------------------------------- 1 | import { mailConfig } from '@base/config/mail'; 2 | import { Service } from 'typedi'; 3 | import { SmtpProvider } from './Providers/SmtpProvider'; 4 | import { MailInterface } from './Interfaces/MailInterface'; 5 | 6 | @Service() 7 | export class MailService implements MailInterface { 8 | private provider: any; 9 | 10 | public constructor() { 11 | this.setProvider(mailConfig.provider); 12 | } 13 | 14 | public setProvider(provider: string) { 15 | switch (provider) { 16 | case 'smtp': 17 | this.provider = new SmtpProvider(); 18 | break; 19 | 20 | default: 21 | break; 22 | } 23 | 24 | return this; 25 | } 26 | 27 | public from(value: string): this { 28 | return this.provider.from(value); 29 | } 30 | 31 | public to(value: string): this { 32 | return this.provider.to(value); 33 | } 34 | 35 | public subject(value: string): this { 36 | return this.provider.subject(value); 37 | } 38 | 39 | public text(value: string): this { 40 | return this.provider.text(value); 41 | } 42 | 43 | public html(value: string): this { 44 | return this.provider.html(value); 45 | } 46 | 47 | public async send(): Promise { 48 | return await this.provider.send(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/infrastructure/services/mail/Providers/SmtpProvider.ts: -------------------------------------------------------------------------------- 1 | import * as nodeMailer from 'nodemailer'; 2 | import { mailConfig } from '@base/config/mail'; 3 | 4 | export class SmtpProvider { 5 | private transporter: nodeMailer.Transporter; 6 | private fromValue: string = mailConfig.fromName + ' ' + mailConfig.authUser; 7 | private toValue: string; 8 | private subjectValue: string; 9 | private textValue: string; 10 | private htmlValue: string; 11 | 12 | public constructor() { 13 | this.transporter = nodeMailer.createTransport({ 14 | host: mailConfig.host, 15 | port: mailConfig.port, 16 | auth: { 17 | user: mailConfig.authUser, 18 | pass: mailConfig.authPassword, 19 | }, 20 | }); 21 | } 22 | 23 | public from(value: string) { 24 | this.fromValue = value; 25 | 26 | return this; 27 | } 28 | 29 | public to(value: string) { 30 | this.toValue = value; 31 | 32 | return this; 33 | } 34 | 35 | public subject(value: string) { 36 | this.subjectValue = value; 37 | 38 | return this; 39 | } 40 | 41 | public text(value: string) { 42 | this.textValue = value; 43 | 44 | return this; 45 | } 46 | 47 | public html(value: string) { 48 | this.htmlValue = value; 49 | 50 | return this; 51 | } 52 | 53 | public async send() { 54 | const mailOptions = { 55 | from: this.fromValue, 56 | to: this.toValue, 57 | subject: this.subjectValue, 58 | text: this.textValue, 59 | html: this.htmlValue, 60 | }; 61 | 62 | return await this.transporter.sendMail(mailOptions); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/infrastructure/services/mail/Templates/ForgotPasswordTemplate.ts: -------------------------------------------------------------------------------- 1 | import Mailgen from 'mailgen'; 2 | import { Service } from 'typedi'; 3 | import { appConfig } from '@base/config/app'; 4 | import { MailGenerator } from '../MailGenerator'; 5 | import { MailTemplateBase } from '@base/infrastructure/abstracts/MailTemplateBase'; 6 | 7 | @Service() 8 | export class ForgotPasswordTemplate extends MailTemplateBase { 9 | private username: string; 10 | private token: string; 11 | 12 | constructor(username: string, token: string) { 13 | super(); 14 | this.username = username; 15 | this.token = token; 16 | } 17 | 18 | public getTemplate(): Mailgen.Content { 19 | return { 20 | body: { 21 | name: this.username, 22 | intro: 'You have received this email because a password reset request for your account was received.', 23 | action: { 24 | instructions: 'Click the button below to reset your password:', 25 | button: { 26 | color: '#DC4D2F', 27 | text: 'Reset your password', 28 | link: `${appConfig.url}/reset-password?token=${this.token}`, 29 | }, 30 | }, 31 | outro: 'If you did not request a password reset, no further action is required on your part.', 32 | }, 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/infrastructure/services/storage/Providers/LocalDisk.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import * as fs from 'fs'; 3 | import { fileSystemsConfig } from '@base/config/filesystems'; 4 | 5 | export class LocalDisk { 6 | private root = fileSystemsConfig.disks.local.root; 7 | 8 | public async put(filePath: string, content: string | Buffer, encoding?: string): Promise { 9 | filePath = this.root + '/' + filePath; 10 | 11 | return new Promise((resolve, reject) => { 12 | if (!filePath || !filePath.trim()) return reject(new Error('The path is required!')); 13 | if (!content) return reject(new Error('The content is required!')); 14 | 15 | const dir = path.dirname(filePath); 16 | if (!fs.existsSync(dir)) this.createDirectory(dir); 17 | 18 | if (dir === filePath.trim()) return reject(new Error('The path is invalid!')); 19 | 20 | fs.writeFile(filePath, content, { encoding } as fs.BaseEncodingOptions, (error) => { 21 | if (error) return reject(error); 22 | resolve(); 23 | }); 24 | }); 25 | } 26 | 27 | public createDirectory(dir: string): void { 28 | const splitPath = dir.split('/'); 29 | if (splitPath.length > 20) throw new Error('The path is invalid!'); 30 | 31 | splitPath.reduce((path, subPath) => { 32 | let currentPath; 33 | if (subPath !== '.') { 34 | currentPath = path + '/' + subPath; 35 | if (!fs.existsSync(currentPath)) fs.mkdirSync(currentPath); 36 | } else currentPath = subPath; 37 | 38 | return currentPath; 39 | }, ''); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/infrastructure/services/storage/StorageService.ts: -------------------------------------------------------------------------------- 1 | import { fileSystemsConfig } from '@base/config/filesystems'; 2 | import { Service } from 'typedi'; 3 | import { LocalDisk } from './Providers/LocalDisk'; 4 | 5 | @Service() 6 | export class StorageService { 7 | private disk: any; 8 | 9 | public constructor() { 10 | this.setDisk(fileSystemsConfig.defaultDisk); 11 | } 12 | 13 | public setDisk(disk: string) { 14 | switch (disk) { 15 | case 'local': 16 | this.disk = new LocalDisk(); 17 | break; 18 | 19 | default: 20 | break; 21 | } 22 | 23 | return this; 24 | } 25 | 26 | public async put(filePath: string, content: string | Buffer, encoding?: string): Promise { 27 | return await this.disk.put(filePath, content, encoding); 28 | } 29 | 30 | public createDirectory(dir: string): void { 31 | return this.disk.createDirectory(dir); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { fixModuleAlias } from './utils/fix-module-alias'; 3 | fixModuleAlias(__dirname); 4 | import { appConfig } from '@base/config/app'; 5 | import { loadEventDispatcher } from '@base/utils/load-event-dispatcher'; 6 | import { useContainer as routingControllersUseContainer, useExpressServer, getMetadataArgsStorage } from 'routing-controllers'; 7 | import { loadHelmet } from '@base/utils/load-helmet'; 8 | import { Container } from 'typedi'; 9 | import { createConnection, useContainer as typeormOrmUseContainer } from 'typeorm'; 10 | import { Container as containerTypeorm } from 'typeorm-typedi-extensions'; 11 | import { useSocketServer, useContainer as socketUseContainer } from 'socket-controllers'; 12 | import { registerController as registerCronJobs, useContainer as cronUseContainer } from 'cron-decorators'; 13 | import * as path from 'path'; 14 | import express from 'express'; 15 | import { validationMetadatasToSchemas } from 'class-validator-jsonschema'; 16 | import { routingControllersToSpec } from 'routing-controllers-openapi'; 17 | import * as swaggerUiExpress from 'swagger-ui-express'; 18 | import { buildSchema } from 'type-graphql'; 19 | import bodyParser from 'body-parser'; 20 | 21 | export class App { 22 | private app: express.Application = express(); 23 | private port: Number = appConfig.port; 24 | 25 | public constructor() { 26 | this.bootstrap(); 27 | } 28 | 29 | public async bootstrap() { 30 | this.useContainers(); 31 | await this.typeOrmCreateConnection(); 32 | this.registerEvents(); 33 | this.registerCronJobs(); 34 | this.serveStaticFiles(); 35 | this.setupMiddlewares(); 36 | this.registerSocketControllers(); 37 | this.registerRoutingControllers(); 38 | this.registerDefaultHomePage(); 39 | this.setupSwagger(); 40 | await this.setupGraphQL(); 41 | // this.register404Page() 42 | } 43 | 44 | private useContainers() { 45 | routingControllersUseContainer(Container); 46 | typeormOrmUseContainer(containerTypeorm); 47 | socketUseContainer(Container); 48 | cronUseContainer(Container); 49 | } 50 | 51 | private async typeOrmCreateConnection() { 52 | try { 53 | await createConnection(); 54 | } catch (error) { 55 | console.log('Caught! Cannot connect to database: ', error); 56 | } 57 | } 58 | 59 | private registerEvents() { 60 | return loadEventDispatcher(); 61 | } 62 | 63 | private registerCronJobs() { 64 | if (!appConfig.cronJobsEnabled) { 65 | return false; 66 | } 67 | 68 | registerCronJobs([__dirname + appConfig.cronJobsDir]); 69 | } 70 | 71 | private serveStaticFiles() { 72 | this.app.use('/public', express.static(path.join(__dirname, 'public'), { maxAge: 31557600000 })); 73 | } 74 | 75 | private setupMiddlewares() { 76 | this.app.use(bodyParser.urlencoded({ extended: true })); 77 | this.app.use(bodyParser.json()); 78 | loadHelmet(this.app); 79 | } 80 | 81 | private registerSocketControllers() { 82 | const server = require('http').Server(this.app); 83 | const io = require('socket.io')(server); 84 | 85 | this.app.use(function (req: any, res: any, next) { 86 | req.io = io; 87 | next(); 88 | }); 89 | 90 | server.listen(this.port, () => console.log(`🚀 Server started at http://localhost:${this.port}\n🚨️ Environment: ${process.env.NODE_ENV}`)); 91 | 92 | useSocketServer(io, { 93 | controllers: [__dirname + appConfig.controllersDir], 94 | }); 95 | } 96 | 97 | private registerRoutingControllers() { 98 | useExpressServer(this.app, { 99 | validation: { stopAtFirstError: true }, 100 | cors: true, 101 | classTransformer: true, 102 | defaultErrorHandler: false, 103 | routePrefix: appConfig.routePrefix, 104 | controllers: [__dirname + appConfig.controllersDir], 105 | middlewares: [__dirname + appConfig.middlewaresDir], 106 | }); 107 | } 108 | 109 | private registerDefaultHomePage() { 110 | this.app.get('/', (req, res) => { 111 | res.json({ 112 | title: appConfig.name, 113 | mode: appConfig.node, 114 | date: new Date(), 115 | }); 116 | }); 117 | } 118 | 119 | private register404Page() { 120 | this.app.get('*', function (req, res) { 121 | res.status(404).send({ status: 404, message: 'Page Not Found!' }); 122 | }); 123 | } 124 | 125 | private setupSwagger() { 126 | // Parse class-validator classes into JSON Schema 127 | const schemas = validationMetadatasToSchemas({ 128 | refPointerPrefix: '#/components/schemas/', 129 | }); 130 | 131 | // Parse routing-controllers classes into OpenAPI spec: 132 | const storage = getMetadataArgsStorage(); 133 | const spec = routingControllersToSpec( 134 | storage, 135 | { routePrefix: appConfig.routePrefix }, 136 | { 137 | components: { 138 | schemas, 139 | securitySchemes: { 140 | bearerAuth: { 141 | type: 'http', 142 | scheme: 'bearer', 143 | bearerFormat: 'JWT', 144 | }, 145 | }, 146 | }, 147 | info: { 148 | description: 'Welcome to the club!', 149 | title: 'API Documentation', 150 | version: '1.0.0', 151 | contact: { 152 | name: 'Kutia', 153 | url: 'https://kutia.net', 154 | email: 'support@kutia.net', 155 | }, 156 | }, 157 | }, 158 | ); 159 | 160 | // Use Swagger 161 | this.app.use('/docs', swaggerUiExpress.serve, swaggerUiExpress.setup(spec)); 162 | } 163 | 164 | private async setupGraphQL() { 165 | if (!appConfig.graphqlEnabled) { 166 | return false; 167 | } 168 | 169 | const graphqlHTTP = require('express-graphql').graphqlHTTP; 170 | 171 | const schema = await buildSchema({ 172 | resolvers: [__dirname + appConfig.resolversDir], 173 | emitSchemaFile: path.resolve(__dirname, 'schema.gql'), 174 | container: Container, 175 | }); 176 | 177 | this.app.use('/graphql', (request: express.Request, response: express.Response) => { 178 | graphqlHTTP({ 179 | schema, 180 | graphiql: true, 181 | })(request, response); 182 | }); 183 | } 184 | } 185 | 186 | new App(); 187 | -------------------------------------------------------------------------------- /src/public/assets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /src/public/uploads/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /src/utils/cli.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import yargs from 'yargs'; 3 | import { VersionCommand } from '@base/api/commands/Common/VersionCommand'; 4 | 5 | yargs 6 | .usage('Usage: cli [options]') 7 | .command(new VersionCommand()) 8 | .demandCommand(1, 'Please provide a valid command.') 9 | .strict() 10 | .help('help') 11 | .alias('help', 'h').argv; 12 | -------------------------------------------------------------------------------- /src/utils/env.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | dotenv.config(); // { path: `.env.${process.env.NODE_ENV}` } 3 | 4 | export function env(key: string, defaultValue: null | string = null): string { 5 | return process.env[key] ?? (defaultValue as string); 6 | } 7 | 8 | export function envOrFail(key: string): string { 9 | if (typeof process.env[key] === 'undefined') { 10 | throw new Error(`Environment variable ${key} is not set.`); 11 | } 12 | 13 | return process.env[key] as string; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/fix-module-alias.ts: -------------------------------------------------------------------------------- 1 | import ModuleAlias from 'module-alias'; 2 | 3 | export function fixModuleAlias(dirName: string) { 4 | ModuleAlias.addAliases({ 5 | '@base': dirName, 6 | '@api': dirName + '/api', 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/load-event-dispatcher.ts: -------------------------------------------------------------------------------- 1 | import glob from 'glob'; 2 | import { appConfig } from '@base/config/app'; 3 | 4 | /** 5 | * This loads all the created subscribers into the project, so we do not have to import them manually. 6 | */ 7 | export function loadEventDispatcher() { 8 | const patterns = appConfig.appPath + appConfig.eventsDir; 9 | 10 | glob(patterns, (err: any, files: string[]) => { 11 | for (const file of files) { 12 | require(file); 13 | } 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/load-helmet.ts: -------------------------------------------------------------------------------- 1 | import Helmet from 'helmet'; 2 | import express from 'express'; 3 | 4 | export function loadHelmet(app: express.Application) { 5 | return app.use( 6 | Helmet({ 7 | /** 8 | * Default helmet policy + own customizations - graphiql support 9 | * https://helmetjs.github.io/ 10 | */ 11 | contentSecurityPolicy: { 12 | directives: { 13 | defaultSrc: [ 14 | "'self'", 15 | /** @by-us - adds graphiql support over helmet's default CSP */ 16 | "'unsafe-inline'", 17 | ], 18 | baseUri: ["'self'"], 19 | blockAllMixedContent: [], 20 | fontSrc: ["'self'", 'https:', 'data:'], 21 | frameAncestors: ["'self'"], 22 | imgSrc: ["'self'", 'data:'], 23 | objectSrc: ["'none'"], 24 | scriptSrc: [ 25 | "'self'", 26 | /** @by-us - adds graphiql support over helmet's default CSP */ 27 | "'unsafe-inline'", 28 | /** @by-us - adds graphiql support over helmet's default CSP */ 29 | "'unsafe-eval'", 30 | ], 31 | upgradeInsecureRequests: [], 32 | }, 33 | }, 34 | }), 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/to-bool.ts: -------------------------------------------------------------------------------- 1 | export function toBool(value: string): boolean { 2 | return value === 'true'; 3 | } 4 | -------------------------------------------------------------------------------- /src/views/chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Socket.IO Chat 5 | 6 | 62 | 63 | 64 | 65 |
    66 | 67 |
    68 | 69 | 70 | 71 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "paths": { 12 | "*": [ 13 | "node_modules/*" 14 | ], 15 | "@base/*": [ 16 | "src/*" 17 | ], 18 | "@api/*": [ 19 | "src/api/*" 20 | ] 21 | }, 22 | "emitDecoratorMetadata": true, 23 | "experimentalDecorators": true 24 | }, 25 | "include": [ 26 | "src/**/*" 27 | ] 28 | } --------------------------------------------------------------------------------