├── .env.example
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── config
└── paths.js
├── package-lock.json
├── package.json
├── pm2
└── prod.json
├── src
├── config
│ └── secrets.ts
├── connection
│ ├── connection-factory.ts
│ ├── connection.ts
│ ├── mongodb.ts
│ └── mysql.ts
├── global.ts
├── helpers
│ ├── backup-db.ts
│ ├── emails.ts
│ ├── logs.ts
│ ├── schedule-job.ts
│ ├── times.ts
│ └── uuid.ts
├── middleware
│ ├── check-auth.ts
│ ├── file-upload.ts
│ └── validate-input.ts
├── models
│ ├── http-error.model.ts
│ ├── index.ts
│ ├── product.model.ts
│ └── user.model.ts
├── modules
│ ├── auth
│ │ ├── auth.controller.ts
│ │ ├── auth.route.ts
│ │ └── auth.service.ts
│ └── production
│ │ ├── order
│ │ ├── order.controller.ts
│ │ ├── order.route.ts
│ │ └── order.service.ts
│ │ └── product
│ │ ├── product.controller.ts
│ │ ├── product.route.ts
│ │ └── product.service.ts
├── server.ts
└── types
│ └── types.d.ts
├── tsconfig.json
└── webpack.config.js
/.env.example:
--------------------------------------------------------------------------------
1 | PORT=8080
2 | NODE_ENV=development
3 |
4 | DB_AUTHENTICATE=false
5 | DB_HOST=127.0.0.1
6 | DB_PORT=27017
7 | DB_NAME=ExTyDB
8 | DB_USERNAME=
9 | DB_PASSWORD=
10 |
11 | JWT_KEY=secret_key_not_share
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | dist/
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true,
6 | jest: true,
7 | },
8 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
9 | parser: '@typescript-eslint/parser',
10 | parserOptions: {
11 | ecmaFeatures: {
12 | jsx: true,
13 | },
14 | ecmaVersion: 13,
15 | sourceType: 'module',
16 | },
17 | plugins: ['@typescript-eslint'],
18 | rules: {
19 | 'array-bracket-spacing': ['warn'],
20 | 'object-curly-spacing': ['warn', 'always'],
21 | // the variable is not reassigned. Change let to const
22 | 'prefer-const': 'off',
23 | 'no-console': 'off',
24 | 'no-useless-escape': 'off',
25 | // I dont know. I use any :)
26 | '@typescript-eslint/no-explicit-any': ['off', { ignoreRestArgs: true }],
27 | '@typescript-eslint/no-empty-function': 'off',
28 | '@typescript-eslint/no-var-requires': 'off',
29 | '@typescript-eslint/no-inferrable-types': 'off',
30 | },
31 | }
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 |
107 | .husky
108 |
109 | build
110 |
111 | # Backup database folder
112 | backups
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | build/
2 | node_modules/
3 | package-lock.json
4 | yarn.lock
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 4,
4 | "useTabs": false,
5 | "semi": false,
6 | "singleQuote": true,
7 | "trailingComma": "all"
8 | }
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Phuong Anh Nguyen
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # express-typescript-webpack
2 |
3 | Express server boilerplate with typescript and webpack. The project helps developers quickly create new backend project in the future.
4 |
5 | ## Install package
6 |
7 | - Node
8 | - Npm
9 | - MongoDb
10 | - Pm2
11 |
12 | ## Set up evironment variables
13 |
14 | ```sh
15 | cp .env.example .env
16 | ```
17 |
18 | Edit .env file:
19 |
20 | - PORT=8080 (port of server)
21 | - NODE_ENV=development (node enviroment)
22 | - DB_AUTHENTICATE=false (is database authenticated?)
23 | - DB_HOST=127.0.0.1 (database host)
24 | - DB_PORT=27017 (database port)
25 | - DB_NAME=ExTyDB (database name)
26 | - DB_USERNAME= (database username)
27 | - DB_PASSWORD= (database password)
28 | - JWT_KEY=secret_key_not_share (secret key for authenticating user)
29 | - EMAIL_ACCOUNT= (your google account to send the mail - send email using [nodemailer](https://nodemailer.com/about/))
30 | - EMAIL_PASSWORD= (password of account)
31 |
32 | ## Set up dev environment
33 |
34 | ```sh
35 | npm install
36 | ```
37 |
38 | ```sh
39 | npm run postinstall
40 | ```
41 |
42 | ```sh
43 | npm run dev
44 | ```
45 |
46 | ## Set up production environment
47 |
48 | ### Build project and test production mode
49 |
50 | ```sh
51 | npm run build
52 | ```
53 |
54 | ### Run production mode with pm2
55 |
56 | ```sh
57 | npm run start-pm2
58 | ```
59 |
60 | The copyright belongs to [Adrien Nguyen](https://adriennguyen.github.io/portfolio/)
61 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('path')
4 | const fs = require('fs')
5 |
6 | const appDirectory = fs.realpathSync(process.cwd())
7 |
8 | const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath)
9 |
10 | module.exports = {
11 | appBuild: resolveApp('build'),
12 | appServerTs: resolveApp('src/server.ts'),
13 | apPackageJson: resolveApp('package.json'),
14 | appNodemodule: resolveApp('node_modules'),
15 | appLog: resolveApp('logs'),
16 | appBackup: resolveApp('backups'),
17 | }
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "express-typescript-webpack",
3 | "version": "1.0.0",
4 | "description": "Express server boilerplate with typescript and webpack. The project helps developers quickly create new backend project in the future.",
5 | "main": "src/server.ts",
6 | "scripts": {
7 | "postinstall": "rm -rf .husky && husky install && npx husky add .husky/pre-commit \"npx lint-staged\"",
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "dev": "concurrently \"npm run watch-ts\" \"npm run watch-node\"",
10 | "watch-node": "node --trace-warnings dist/server.js",
11 | "watch-ts": "tsc --watch",
12 | "build": "webpack && node build/server.js",
13 | "start-pm2": "pm2 start ./pm2/prod.json",
14 | "lint": "eslint .",
15 | "format": "prettier --write \"**/*.+(js|ts|json|md)\""
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/AdrienNguyen/express-typescript-webpack.git"
20 | },
21 | "keywords": [],
22 | "author": {
23 | "name": "Adrien Nguyen",
24 | "email": "phuong.na163228@gmail.com"
25 | },
26 | "license": "ISC",
27 | "bugs": {
28 | "url": "https://github.com/AdrienNguyen/express-typescript-webpack/issues"
29 | },
30 | "homepage": "https://github.com/AdrienNguyen/express-typescript-webpack#readme",
31 | "lint-staged": {
32 | "*.+(js|ts)": "eslint --max-warnings 0 --fix",
33 | "*.+(js|ts|json|md)": "prettier --write"
34 | },
35 | "dependencies": {
36 | "bcryptjs": "^2.4.3",
37 | "body-parser": "^1.19.1",
38 | "child_process": "^1.0.2",
39 | "crypto": "^1.0.1",
40 | "dotenv": "^10.0.0",
41 | "express": "^4.17.2",
42 | "express-validator": "^6.14.0",
43 | "jsonwebtoken": "^8.5.1",
44 | "mongoose": "^6.1.4",
45 | "mongoose-paginate-v2": "^1.4.2",
46 | "node-schedule": "^2.1.0",
47 | "nodemailer": "^6.7.2",
48 | "winston": "^3.3.3"
49 | },
50 | "devDependencies": {
51 | "@types/express": "^4.17.13",
52 | "@types/node": "^17.0.2",
53 | "@typescript-eslint/eslint-plugin": "^5.8.0",
54 | "babel-loader": "^8.2.3",
55 | "concurrently": "^6.5.1",
56 | "eslint": "^8.5.0",
57 | "husky": "^7.0.4",
58 | "lint-staged": "^12.1.3",
59 | "nodemon": "^2.0.15",
60 | "pre-commit": "^1.2.2",
61 | "prettier": "^2.5.1",
62 | "terser-webpack-plugin": "^5.3.0",
63 | "ts-loader": "^9.2.6",
64 | "ts-node": "^10.4.0",
65 | "typescript": "^4.5.4",
66 | "webpack": "^5.65.0",
67 | "webpack-cli": "^4.9.1"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/pm2/prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "apps": [
3 | {
4 | "name": "express-typescript-webpack",
5 | "cwd": "./",
6 | "kill_timeout": 3000,
7 | "restart_delay": 3000,
8 | "script": "node",
9 | "args": "build/main.js"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/src/config/secrets.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv'
2 | import fs from 'fs'
3 |
4 | if (fs.existsSync('.env')) {
5 | dotenv.config({
6 | path: '.env',
7 | })
8 | } else {
9 | dotenv.config({
10 | path: '.env.example',
11 | })
12 | }
13 |
14 | export const {
15 | NODE_ENV,
16 | PORT,
17 | DB_AUTHENTICATE,
18 | DB_HOST,
19 | DB_PORT,
20 | DB_NAME,
21 | DB_USERNAME,
22 | DB_PASSWORD,
23 | JWT_KEY,
24 | EMAIL_ACCOUNT,
25 | EMAIL_PASSWORD,
26 | } = process.env
27 |
--------------------------------------------------------------------------------
/src/connection/connection-factory.ts:
--------------------------------------------------------------------------------
1 | import MongoDb from './mongodb'
2 | import MySQL from './mysql'
3 |
4 | export default class ConnectionFactory {
5 | constructor() {}
6 |
7 | public static getConnection(connectionType: DBType): Connection {
8 | switch (connectionType) {
9 | case DBType.MongoDb:
10 | return new MongoDb()
11 | case DBType.MySQL:
12 | return new MySQL()
13 | default:
14 | throw new Error(
15 | 'Get database connection failed. Please try again!',
16 | )
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/connection/connection.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | interface Connection {
3 | connect: () => void
4 | }
5 |
--------------------------------------------------------------------------------
/src/connection/mongodb.ts:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 | import {
3 | DB_AUTHENTICATE,
4 | DB_HOST,
5 | DB_PORT,
6 | DB_NAME,
7 | DB_USERNAME,
8 | DB_PASSWORD,
9 | } from '../config/secrets'
10 |
11 | export default class MongoDb implements Connection {
12 | uri: string
13 | connectOptions: object
14 |
15 | constructor() {
16 | this.uri = `mongodb://${DB_HOST}:${DB_PORT || '27017'}/${DB_NAME}`
17 | this.connectOptions =
18 | DB_AUTHENTICATE === 'true'
19 | ? {
20 | useNewUrlParser: true,
21 | useUnifiedTopology: true,
22 | user: DB_USERNAME,
23 | pass: DB_PASSWORD,
24 | }
25 | : {
26 | useNewUrlParser: true,
27 | useUnifiedTopology: true,
28 | }
29 | }
30 |
31 | async connect() {
32 | mongoose
33 | .connect(this.uri, this.connectOptions)
34 | .then(() => {
35 | console.log('Connect Database MongoDb successfully')
36 | })
37 | .catch((error) => {
38 | console.log(error.message)
39 | })
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/connection/mysql.ts:
--------------------------------------------------------------------------------
1 | export default class MySQL implements Connection {
2 | async connect() {
3 | console.log('Connect Database MySQL here.')
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/global.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AdrienNguyen/express-typescript-webpack/7052b0d6c9eb2f0c09bafcd99550d9ccf156fce6/src/global.ts
--------------------------------------------------------------------------------
/src/helpers/backup-db.ts:
--------------------------------------------------------------------------------
1 | // using node-schedule to set up job backup database every week
2 |
3 | import {
4 | DB_HOST,
5 | DB_PORT,
6 | DB_NAME,
7 | DB_USERNAME,
8 | DB_PASSWORD,
9 | DB_AUTHENTICATE,
10 | } from '../config/secrets'
11 | import { getLocalDateTimeString } from './times'
12 | import { appBackup } from '../../config/paths'
13 | import fs from 'fs'
14 | import { exec } from 'child_process'
15 |
16 | export const backupDB = (): void => {
17 | try {
18 | const version: string = getLocalDateTimeString()
19 |
20 | const createBackupFolder = (): string => {
21 | const path: string = appBackup + '/' + version
22 | if (!fs.existsSync(path)) {
23 | fs.mkdirSync(path, {
24 | recursive: true,
25 | })
26 | }
27 | return path
28 | }
29 |
30 | const backupFolder: string = createBackupFolder()
31 | const backupDescription: string = `Backup database ${DB_NAME} at ${version}`
32 |
33 | // 1. create version.txt in folderback up
34 | fs.appendFile(
35 | backupFolder + `/README.txt`,
36 | backupDescription,
37 | (err) => {
38 | if (err) throw err
39 | },
40 | )
41 | // 2. backup database
42 | const commandBackup =
43 | DB_AUTHENTICATE === 'true'
44 | ? `mongodump --host="${DB_HOST}" --port="${DB_PORT}" --db="${DB_NAME}" --username="${DB_USERNAME}" --password="${DB_PASSWORD}" --out="${backupFolder}"`
45 | : `mongodump --host="${DB_HOST}" --port="${DB_PORT}" --db="${DB_NAME}" --out="${backupFolder}"`
46 | exec(commandBackup, (err) => {
47 | if (err) throw err
48 | })
49 | } catch (error) {
50 | console.log(error.message)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/helpers/emails.ts:
--------------------------------------------------------------------------------
1 | import nodemailer from 'nodemailer'
2 | import { EMAIL_ACCOUNT, EMAIL_PASSWORD } from '../config/secrets'
3 |
4 | export const sendEmailForCreatedAccount = async (email: string) => {
5 | try {
6 | console.log(email)
7 | const transporter = nodemailer.createTransport({
8 | service: 'Gmail',
9 | auth: {
10 | user: EMAIL_ACCOUNT,
11 | pass: EMAIL_PASSWORD,
12 | },
13 | })
14 |
15 | const mainOptions = {
16 | from: EMAIL_ACCOUNT,
17 | to: email,
18 | subject: 'Register account successfully!',
19 | text:
20 | 'You have registered account successfully in out system with email!: ' +
21 | email,
22 | html: `
23 |
24 |
64 |
65 |
66 |
67 |
68 |
69 |
WEBSITE-DEMO
70 |
71 |
77 |
82 |
83 |
84 | `,
85 | }
86 |
87 | return await transporter.sendMail(mainOptions)
88 | } catch (error) {
89 | console.log(error.message)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/helpers/logs.ts:
--------------------------------------------------------------------------------
1 | import { format, createLogger, transports, Logger } from 'winston'
2 |
3 | const { splat, printf, combine, label, timestamp } = format
4 |
5 | import { appLog } from '../../config/paths'
6 |
7 | const Log = async (title: string): Promise => {
8 | let options = {
9 | format: combine(
10 | splat(),
11 | timestamp({
12 | format: 'YYYY-MM-DD HH:mm:ss',
13 | }),
14 | label({
15 | label: title,
16 | }),
17 | printf((log) => {
18 | return `${log.timestamp} | ${log.label} | ${log.level} | ${log.message}`
19 | }),
20 | ),
21 | transports: [
22 | new transports.File({
23 | filename: appLog + `/info.log`,
24 | level: 'info',
25 | }),
26 | new transports.File({
27 | filename: appLog + `/error.log`,
28 | level: 'error',
29 | }),
30 | ],
31 | }
32 |
33 | return createLogger(options)
34 | }
35 |
36 | export const LogInfo = async (
37 | email: string,
38 | content: string,
39 | ): Promise => {
40 | try {
41 | const Logger = await Log(content)
42 | Logger.info(email)
43 | } catch (error) {
44 | console.trace(error.message)
45 | }
46 | }
47 |
48 | export const LogError = async (
49 | email: string,
50 | content: string,
51 | ): Promise => {
52 | try {
53 | const Logger = await Log(content)
54 | Logger.error(email)
55 | } catch (error) {
56 | console.trace(error.message)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/helpers/schedule-job.ts:
--------------------------------------------------------------------------------
1 | import schedule from 'node-schedule'
2 | import { uuid } from './uuid'
3 |
4 | export default class ScheduleJob {
5 | id: string
6 | ruleTime: object
7 | action: any
8 |
9 | constructor(ruleTime: object, action: any) {
10 | this.id = uuid()
11 | this.ruleTime = ruleTime
12 | this.action = action
13 | }
14 |
15 | start = (): void => {
16 | schedule.scheduleJob(this.id, this.ruleTime, this.action)
17 | }
18 |
19 | cancel = (): void => {
20 | schedule.scheduledJobs[this.id].cancel()
21 | }
22 |
23 | reschedule = (newRule: object): void => {
24 | schedule.scheduledJobs[this.id].reschedule(newRule)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/helpers/times.ts:
--------------------------------------------------------------------------------
1 | export const getLocalDateTimeString = (): string => {
2 | const dateTime = new Date()
3 | const year = dateTime.getFullYear()
4 | let month: string | number = dateTime.getMonth() + 1
5 | month = month < 9 ? '0' + month : month
6 | let date: string | number = dateTime.getDate()
7 | date = date < 9 ? '0' + date : date
8 | const hour = dateTime.getHours()
9 | const minute = dateTime.getMinutes()
10 | const second = dateTime.getSeconds()
11 |
12 | return `${year}-${month}-${date}_${hour}h${minute}m${second}s`
13 | }
14 |
--------------------------------------------------------------------------------
/src/helpers/uuid.ts:
--------------------------------------------------------------------------------
1 | const { randomBytes } = require('crypto')
2 |
3 | let SIZE = 4096,
4 | IDX = 0,
5 | BUFFER,
6 | HEX = []
7 |
8 | // HEX = [00 01 02 .... ff]
9 | for (let i = 0; i < 256; i++) {
10 | HEX[i] = (i + 256).toString(16).substring(1)
11 | }
12 |
13 | export const uuid = (): string => {
14 | if (!BUFFER || IDX + 16 > SIZE) {
15 | // SIZE bytes
16 | BUFFER = randomBytes(SIZE)
17 | IDX = 0
18 | }
19 |
20 | let i = 0,
21 | tmp,
22 | out = ''
23 | for (i = 0; i < 16; i++) {
24 | tmp = BUFFER[i + IDX]
25 |
26 | if (i == 6) {
27 | out += HEX[(tmp & 15) | 64]
28 | } else if (i == 8) {
29 | out += HEX[(tmp & 63) | 128]
30 | } else {
31 | out += HEX[tmp]
32 | }
33 |
34 | if (i === 3 || i === 5 || i === 7 || i == 9) {
35 | out += '-'
36 | }
37 | }
38 | IDX++
39 | return out
40 | }
41 |
--------------------------------------------------------------------------------
/src/middleware/check-auth.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 |
3 | import { HttpError } from '../models'
4 | import jwt from 'jsonwebtoken'
5 | import { JWT_KEY } from '../config/secrets'
6 |
7 | const checkAuth = (req, res, next) => {
8 | if (req.method === 'OPTIONS') {
9 | next()
10 | }
11 |
12 | try {
13 | const token = req.headers.authorization.split(' ')[1]
14 | if (!token) {
15 | throw new HttpError('Authentication failed!', 401)
16 | }
17 |
18 | const decodedToken = jwt.verify(token, JWT_KEY)
19 |
20 | req.user = decodedToken
21 |
22 | next()
23 | } catch (error) {
24 | return next(new HttpError('Authentication failed!', 401))
25 | }
26 | }
27 |
28 | export default checkAuth
29 |
--------------------------------------------------------------------------------
/src/middleware/file-upload.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AdrienNguyen/express-typescript-webpack/7052b0d6c9eb2f0c09bafcd99550d9ccf156fce6/src/middleware/file-upload.ts
--------------------------------------------------------------------------------
/src/middleware/validate-input.ts:
--------------------------------------------------------------------------------
1 | import { validationResult } from 'express-validator'
2 | import { LogError } from '../helpers/logs'
3 | import { HttpError } from '../models'
4 |
5 | const validateInput = (message) => {
6 | return async (req, res, next) => {
7 | const error = validationResult(req)
8 | if (!error.isEmpty()) {
9 | res.message = message + '_FAILED'
10 |
11 | req.user && (await LogError(req.user.email, message))
12 |
13 | return next(
14 | new HttpError(
15 | 'Invalid inputs passed, please check your data',
16 | 422,
17 | ),
18 | )
19 | }
20 | next()
21 | }
22 | }
23 |
24 | export default validateInput
25 |
--------------------------------------------------------------------------------
/src/models/http-error.model.ts:
--------------------------------------------------------------------------------
1 | class HttpError extends Error {
2 | code: number
3 |
4 | constructor(message, errorCode) {
5 | super(message)
6 | this.code = errorCode
7 | }
8 | }
9 |
10 | export default HttpError
11 |
--------------------------------------------------------------------------------
/src/models/index.ts:
--------------------------------------------------------------------------------
1 | import User from './user.model'
2 | import HttpError from './http-error.model'
3 | import Product from './product.model'
4 |
5 | export { User, HttpError, Product }
6 |
--------------------------------------------------------------------------------
/src/models/product.model.ts:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 | import mongoosePaginate from 'mongoose-paginate-v2'
3 | const Schema = mongoose.Schema
4 |
5 | const productSchema = new Schema(
6 | {
7 | name: {
8 | type: String,
9 | required: true,
10 | },
11 | price: {
12 | type: Number,
13 | required: true,
14 | },
15 | description: {
16 | type: String,
17 | },
18 | image: {
19 | type: String,
20 | },
21 | },
22 | {
23 | timestamps: true,
24 | },
25 | )
26 |
27 | productSchema.plugin(mongoosePaginate)
28 |
29 | const Product = mongoose.model('Product', productSchema) as any
30 | export default Product
31 |
--------------------------------------------------------------------------------
/src/models/user.model.ts:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 | const Schema = mongoose.Schema
3 |
4 | const userSchema = new Schema(
5 | {
6 | name: {
7 | type: String,
8 | required: true,
9 | },
10 | email: {
11 | type: String,
12 | required: true,
13 | },
14 | password: {
15 | type: String,
16 | required: true,
17 | },
18 | image: {
19 | type: String,
20 | },
21 | },
22 | {
23 | timestamps: true,
24 | },
25 | )
26 |
27 | const User = mongoose.model('User', userSchema)
28 | export default User
29 |
--------------------------------------------------------------------------------
/src/modules/auth/auth.controller.ts:
--------------------------------------------------------------------------------
1 | import { LogInfo, LogError } from '../../helpers/logs'
2 | import authService from './auth.service'
3 |
4 | const register = async (req, res) => {
5 | try {
6 | const user = await authService.register(req.body)
7 |
8 | req.body.email && LogInfo(req.body.email, 'REGISTER_SUCCESSFULLY')
9 |
10 | res.status(201).json({
11 | success: true,
12 | message: 'REGISTER_SUCCESSFULLY',
13 | content: user,
14 | })
15 | } catch (error) {
16 | req.body.email && LogError(req.body.email, 'REGISTER_FAILED')
17 |
18 | res.status(error.code || 400).json({
19 | success: false,
20 | message: 'REGISTER_FAILED',
21 | content: error.message,
22 | })
23 | }
24 | }
25 |
26 | const login = async (req, res) => {
27 | try {
28 | const user = await authService.login(req.body)
29 |
30 | req.body.email && LogInfo(req.body.email, 'LOGIN_SUCCESSFULLY')
31 |
32 | res.status(200).json({
33 | success: true,
34 | message: 'LOGIN_SUCCESSFULLY',
35 | content: user,
36 | })
37 | } catch (error) {
38 | req.body.email && LogError(req.body.email, 'REGISTER_FAILED')
39 |
40 | res.status(error.code || 400).json({
41 | success: false,
42 | message: 'LOGIN_FAILED',
43 | content: error.message,
44 | })
45 | }
46 | }
47 |
48 | export default {
49 | register,
50 | login,
51 | }
52 |
--------------------------------------------------------------------------------
/src/modules/auth/auth.route.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import { check } from 'express-validator'
3 |
4 | import validateInput from '../../middleware/validate-input'
5 | import authController from './auth.controller'
6 |
7 | const router = express.Router()
8 |
9 | router.post(
10 | '/register',
11 | [
12 | check('name').not().isEmpty(),
13 | check('email').isEmail(),
14 | check('password').isLength({ min: 6 }),
15 | ],
16 | validateInput('REGISTER_FAILED'),
17 | authController.register,
18 | )
19 |
20 | router.post('/login', authController.login)
21 |
22 | export default router
23 |
--------------------------------------------------------------------------------
/src/modules/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | import { User, HttpError } from '../../models'
2 | import bcrypt from 'bcryptjs'
3 | import jwt from 'jsonwebtoken'
4 | import { JWT_KEY } from '../../config/secrets'
5 | import { sendEmailForCreatedAccount } from '../../helpers/emails'
6 |
7 | export const register = async (data) => {
8 | const { name, email, password } = data
9 |
10 | const existingUser = await User.findOne({
11 | email: email,
12 | })
13 |
14 | if (existingUser) {
15 | throw new HttpError('User exists already, please login instead', 422)
16 | }
17 |
18 | const hashedPassword = await bcrypt.hash(password, 12)
19 |
20 | const newUser = await User.create({
21 | email: email,
22 | password: hashedPassword,
23 | name: name,
24 | })
25 |
26 | const token = jwt.sign(
27 | {
28 | userId: newUser.id,
29 | email: newUser.email,
30 | },
31 | JWT_KEY,
32 | {
33 | expiresIn: '1h',
34 | },
35 | )
36 |
37 | return {
38 | user: {
39 | userId: newUser.id,
40 | email: newUser.email,
41 | token: token,
42 | },
43 | }
44 | }
45 |
46 | const login = async (data) => {
47 | const { email, password } = data
48 |
49 | const existingUser = await User.findOne({
50 | email: email,
51 | })
52 |
53 | if (!existingUser) {
54 | throw new HttpError('Invalid credentials, could not log you in.', 403)
55 | }
56 |
57 | const isValidPassword = await bcrypt.compare(
58 | password,
59 | existingUser.password,
60 | )
61 |
62 | if (!isValidPassword) {
63 | throw new HttpError('Invalid credentials, could not log you in.', 403)
64 | }
65 |
66 | const token = jwt.sign(
67 | {
68 | userId: existingUser.id,
69 | email: existingUser.email,
70 | },
71 | JWT_KEY,
72 | {
73 | expiresIn: '1h',
74 | },
75 | )
76 |
77 | await sendEmailForCreatedAccount(email)
78 |
79 | return {
80 | user: {
81 | userId: existingUser.id,
82 | email: existingUser.email,
83 | token: token,
84 | },
85 | }
86 | }
87 |
88 | export default {
89 | register,
90 | login,
91 | }
92 |
--------------------------------------------------------------------------------
/src/modules/production/order/order.controller.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AdrienNguyen/express-typescript-webpack/7052b0d6c9eb2f0c09bafcd99550d9ccf156fce6/src/modules/production/order/order.controller.ts
--------------------------------------------------------------------------------
/src/modules/production/order/order.route.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AdrienNguyen/express-typescript-webpack/7052b0d6c9eb2f0c09bafcd99550d9ccf156fce6/src/modules/production/order/order.route.ts
--------------------------------------------------------------------------------
/src/modules/production/order/order.service.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AdrienNguyen/express-typescript-webpack/7052b0d6c9eb2f0c09bafcd99550d9ccf156fce6/src/modules/production/order/order.service.ts
--------------------------------------------------------------------------------
/src/modules/production/product/product.controller.ts:
--------------------------------------------------------------------------------
1 | import { LogInfo, LogError } from '../../../helpers/logs'
2 | import productService from './product.service'
3 |
4 | const createProduct = async (req, res) => {
5 | try {
6 | const product = await productService.createProduct(req.body)
7 |
8 | req.user && (await LogInfo(req.user.email, 'CREATE_PRODUCT'))
9 |
10 | res.status(201).json({
11 | success: true,
12 | message: 'CREATE_PRODUCT_SUCCESSFULLY',
13 | content: product,
14 | })
15 | } catch (error) {
16 | req.user && (await LogError(req.user.email, 'CREATE_PRODUCT'))
17 |
18 | res.status(error.code || 400).json({
19 | success: false,
20 | message: 'CREATE_PRODUCT_FAILED',
21 | content: error.message,
22 | })
23 | }
24 | }
25 |
26 | const getProducts = async (req, res) => {
27 | try {
28 | const products = await productService.getProducts(req.query)
29 |
30 | res.status(201).json({
31 | success: true,
32 | message: 'GET_PRODUCTS_SUCCESSFULLY',
33 | content: products,
34 | })
35 | } catch (error) {
36 | res.status(error.code || 400).json({
37 | success: false,
38 | message: 'GET_PRODUCTS_FAILED',
39 | content: error.message,
40 | })
41 | }
42 | }
43 |
44 | export default {
45 | createProduct,
46 | getProducts,
47 | }
48 |
--------------------------------------------------------------------------------
/src/modules/production/product/product.route.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import { check } from 'express-validator'
3 | import validateInput from '../../../middleware/validate-input'
4 |
5 | import checkAuth from '../../../middleware/check-auth'
6 | import productController from './product.controller'
7 |
8 | const router = express.Router()
9 |
10 | router.get('/', productController.getProducts)
11 |
12 | router.use(checkAuth)
13 |
14 | router.post(
15 | '/',
16 | [check('name').isLength({ min: 6, max: 100 }), check('price').isNumeric()],
17 | validateInput('CREATE_PRODUCT'),
18 | productController.createProduct,
19 | )
20 |
21 | export default router
22 |
--------------------------------------------------------------------------------
/src/modules/production/product/product.service.ts:
--------------------------------------------------------------------------------
1 | import { Product } from '../../../models'
2 |
3 | const createProduct = async (data) => {
4 | const { name, price, description } = data
5 |
6 | const newProduct = await Product.create({
7 | name,
8 | price,
9 | description,
10 | })
11 |
12 | return { product: newProduct.toObject({ getters: true }) }
13 | }
14 |
15 | interface IQueryProducts {
16 | name?: RegExp
17 | }
18 |
19 | const getProducts = async (data) => {
20 | let { name, page, limit } = data
21 |
22 | page = parseInt(page)
23 | limit = parseInt(limit)
24 |
25 | const options: IQueryProducts = {}
26 | if (name) {
27 | options.name = new RegExp(name, 'i')
28 | }
29 |
30 | let products: Array