├── .eslintignore ├── .env.example ├── .vscode └── extensions.json ├── src ├── modules │ ├── common │ │ ├── persistence │ │ │ └── prisma-client.ts │ │ └── types │ │ │ └── Option.type.ts │ ├── user │ │ ├── entities │ │ │ └── user.entity.ts │ │ ├── dto │ │ │ └── user.dto.ts │ │ ├── service │ │ │ ├── IUserRepository.ts │ │ │ └── user.service.ts │ │ ├── application │ │ │ ├── router.ts │ │ │ ├── controller.ts │ │ │ └── middleware.ts │ │ └── persistence │ │ │ └── user.repository.ts │ └── auth │ │ ├── application │ │ ├── router.ts │ │ ├── controller.ts │ │ └── middleware.ts │ │ ├── service │ │ ├── IAuthRepository.ts │ │ └── auth.service.ts │ │ └── persistence │ │ └── auth.repository.ts ├── @type │ └── express.d.ts ├── IoC │ ├── icradle.interface.ts │ ├── container.ts │ └── providers │ │ ├── auth.provider.ts │ │ └── user.provider.ts ├── config │ └── index.ts └── index.ts ├── .prettierrc.js ├── .gitignore ├── tsconfig.json ├── .eslintrc.js ├── prisma └── schema.prisma ├── Dockerfile ├── .github └── FUNDING.yml ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | # build artefacts 2 | dist/* 3 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://username:password@localhost:5432/dbname?schema=public" -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Prisma.prisma", 4 | ] 5 | } -------------------------------------------------------------------------------- /src/modules/common/persistence/prisma-client.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | export default new PrismaClient(); 4 | -------------------------------------------------------------------------------- /src/modules/common/types/Option.type.ts: -------------------------------------------------------------------------------- 1 | type None = null | undefined; 2 | 3 | type Option = T | None; 4 | 5 | export default Option; 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "es5", 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2 7 | }; 8 | -------------------------------------------------------------------------------- /src/modules/user/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | export default interface UserEntity { 2 | id: number; 3 | firstName: string; 4 | lastName: string; 5 | email: string; 6 | password: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/@type/express.d.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedUserDTO } from '../modules/user/dto/user.dto'; 2 | 3 | declare module 'express' { 4 | export interface Request { 5 | requester?: AuthenticatedUserDTO; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/IoC/icradle.interface.ts: -------------------------------------------------------------------------------- 1 | import { IAuthProvider } from './providers/auth.provider'; 2 | import { IUserProvider } from './providers/user.provider'; 3 | 4 | export default interface ICradle extends IAuthProvider, IUserProvider {} 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node build artifacts 2 | node_modules 3 | npm-debug.log 4 | dist 5 | build 6 | .idea 7 | yarn-error.log 8 | .env 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local 13 | prisma/migrations 14 | -------------------------------------------------------------------------------- /src/modules/auth/application/router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import ICradle from '../../../IoC/icradle.interface'; 3 | 4 | export default (cradle: ICradle) => { 5 | const router = Router(); 6 | 7 | router.post('/login', cradle.authController.login); 8 | 9 | return router; 10 | }; 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true 10 | }, 11 | "include": ["src/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | ecmaVersion: 2019, 5 | sourceType: 'module', 6 | }, 7 | extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], 8 | rules: { 9 | '@typescript-eslint/no-explicit-any': 1, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | // Admin credentials 2 | export const ADMIN_USERNAME = process.env.ADMIN_USERNAME || 'admin'; 3 | export const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin'; 4 | 5 | // JWT config 6 | export const AUTHENTICATED_USER_TOKEN_TTL = 86400; // (in second) 1 day = 24 x 60 x 60 7 | export const JWT_SECRET_KEY = 'secret'; 8 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model User { 11 | id Int @id @default(autoincrement()) 12 | lastName String 13 | firstName String 14 | email String @unique 15 | password String 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/user/dto/user.dto.ts: -------------------------------------------------------------------------------- 1 | import UserEntity from '../entities/user.entity'; 2 | 3 | export interface CreateUserDTO { 4 | email: string; 5 | firstName: string; 6 | lastName: string; 7 | password: string; 8 | } 9 | 10 | export type PublicUserDTO = Omit; 11 | 12 | export interface AuthenticatedUserDTO extends PublicUserDTO { 13 | token: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/user/service/IUserRepository.ts: -------------------------------------------------------------------------------- 1 | import Option from '../../common/types/Option.type'; 2 | import { CreateUserDTO, PublicUserDTO } from '../dto/user.dto'; 3 | 4 | export default interface IUserRepository { 5 | createUser(data: CreateUserDTO): Promise; 6 | getUserByEmail(email: string): Promise>; 7 | getUserById(id: number): Promise>; 8 | } 9 | -------------------------------------------------------------------------------- /src/IoC/container.ts: -------------------------------------------------------------------------------- 1 | import { createContainer, InjectionMode } from 'awilix'; 2 | 3 | import ICradle from './icradle.interface'; 4 | import authProvider from './providers/auth.provider'; 5 | import userProvider from './providers/user.provider'; 6 | 7 | const container = createContainer({ 8 | injectionMode: InjectionMode.CLASSIC, 9 | }); 10 | 11 | authProvider(container); 12 | userProvider(container); 13 | 14 | export default container; 15 | -------------------------------------------------------------------------------- /src/modules/user/application/router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import ICradle from '../../../IoC/icradle.interface'; 3 | 4 | export default (cradle: ICradle) => { 5 | const router = Router(); 6 | 7 | router.post('/signup', cradle.userMiddleware.validateCreateAccountBody, cradle.userController.createAccount); 8 | 9 | router.get('/profile', cradle.authMiddleware.authenticate, cradle.userController.getMyProfile); 10 | 11 | return router; 12 | }; 13 | -------------------------------------------------------------------------------- /src/modules/auth/service/IAuthRepository.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedUserDTO, PublicUserDTO } from '../../user/dto/user.dto'; 2 | import Option from '../../common/types/Option.type'; 3 | 4 | export default interface IAuthRepository { 5 | getAuthenticatedUserByToken(token: string): Promise>; 6 | 7 | generateToken(user: PublicUserDTO): Promise; 8 | 9 | authenticateUser(username: string, password: string): Promise>; 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.13.1-alpine As build-stage 2 | 3 | # Create app directory 4 | WORKDIR /app 5 | 6 | COPY package*.json ./ 7 | 8 | # Install app dependencies 9 | RUN npm install 10 | 11 | COPY . . 12 | 13 | RUN npm run build 14 | 15 | FROM node:16.13.1-alpine as production-stage 16 | 17 | # Copy necessary files 18 | COPY --from=build-stage /app/node_modules ./node_modules 19 | COPY --from=build-stage /app/package*.json ./ 20 | COPY --from=build-stage /app/dist ./dist 21 | 22 | EXPOSE 8080 23 | 24 | CMD ["npm", "run", "start"] 25 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import cors from 'cors'; 4 | import authRouter from './modules/auth/application/router'; 5 | import userRouter from './modules/user/application/router'; 6 | import container from './IoC/container'; 7 | 8 | const API_PREFIX = '/api'; 9 | const app = express(); 10 | 11 | app.use(cors()); 12 | app.use(bodyParser.json()); 13 | app.use(API_PREFIX, authRouter(container.cradle)); 14 | app.use(API_PREFIX, userRouter(container.cradle)); 15 | 16 | const port = process.env.PORT || 3000; 17 | 18 | app.listen(port, () => { 19 | console.log(`Application started at port ${port}`); 20 | }); 21 | -------------------------------------------------------------------------------- /src/modules/user/service/user.service.ts: -------------------------------------------------------------------------------- 1 | import Option from '../../common/types/Option.type'; 2 | import { CreateUserDTO, PublicUserDTO } from '../dto/user.dto'; 3 | import IUserRepository from './IUserRepository'; 4 | 5 | export default class UserService { 6 | constructor(private userRepository: IUserRepository) {} 7 | 8 | createUser(data: CreateUserDTO): Promise { 9 | return this.userRepository.createUser(data); 10 | } 11 | 12 | getUserByEmail(email: string): Promise> { 13 | return this.userRepository.getUserByEmail(email); 14 | } 15 | 16 | getUserById(id: number): Promise> { 17 | return this.userRepository.getUserById(id); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: tlcong 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /src/modules/auth/service/auth.service.ts: -------------------------------------------------------------------------------- 1 | import Option from '../../common/types/Option.type'; 2 | import { AuthenticatedUserDTO } from '../../user/dto/user.dto'; 3 | import IAuthRepository from './IAuthRepository'; 4 | 5 | export default class AuthService { 6 | constructor(private authRepository: IAuthRepository) {} 7 | 8 | async authenticateUser(username: string, password: string): Promise> { 9 | const authenticatedUser = await this.authRepository.authenticateUser(username, password); 10 | 11 | if (!authenticatedUser) return; 12 | 13 | const token = await this.authRepository.generateToken(authenticatedUser); 14 | 15 | return { 16 | ...authenticatedUser, 17 | token, 18 | }; 19 | } 20 | 21 | async authenticateUserByToken(token: string): Promise> { 22 | return this.authRepository.getAuthenticatedUserByToken(token); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/IoC/providers/auth.provider.ts: -------------------------------------------------------------------------------- 1 | import { asClass, AwilixContainer } from 'awilix'; 2 | import AuthController from '../../modules/auth/application/controller'; 3 | import AuthMiddleware from '../../modules/auth/application/middleware'; 4 | import AuthRepository from '../../modules/auth/persistence/auth.repository'; 5 | import AuthService from '../../modules/auth/service/auth.service'; 6 | 7 | import ICradle from '../icradle.interface'; 8 | 9 | export interface IAuthProvider { 10 | authRepository: AuthRepository; 11 | authService: AuthService; 12 | authController: AuthController; 13 | authMiddleware: AuthMiddleware; 14 | } 15 | 16 | const authProvider = (container: AwilixContainer): void => { 17 | container.register({ 18 | authRepository: asClass(AuthRepository), 19 | authService: asClass(AuthService), 20 | authController: asClass(AuthController), 21 | authMiddleware: asClass(AuthMiddleware), 22 | }); 23 | }; 24 | 25 | export default authProvider; 26 | -------------------------------------------------------------------------------- /src/IoC/providers/user.provider.ts: -------------------------------------------------------------------------------- 1 | import { asClass, AwilixContainer } from 'awilix'; 2 | import UserController from '../../modules/user/application/controller'; 3 | import UserMiddleware from '../../modules/user/application/middleware'; 4 | import UserRepository from '../../modules/user/persistence/user.repository'; 5 | import UserService from '../../modules/user/service/user.service'; 6 | 7 | import ICradle from '../icradle.interface'; 8 | 9 | export interface IUserProvider { 10 | userRepository: UserRepository; 11 | userService: UserService; 12 | userController: UserController; 13 | userMiddleware: UserMiddleware; 14 | } 15 | 16 | const userProvider = (container: AwilixContainer): void => { 17 | container.register({ 18 | userRepository: asClass(UserRepository), 19 | userService: asClass(UserService), 20 | userController: asClass(UserController), 21 | userMiddleware: asClass(UserMiddleware), 22 | }); 23 | }; 24 | 25 | export default userProvider; 26 | -------------------------------------------------------------------------------- /src/modules/auth/application/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import AuthService from '../service/auth.service'; 3 | 4 | export default class AuthController { 5 | constructor(private authService: AuthService) {} 6 | 7 | login = async (req: Request, res: Response): Promise => { 8 | const { email, password } = req.body; 9 | 10 | if (!email || !password) { 11 | return res.status(400).json({ 12 | error: { 13 | code: 400, 14 | message: 'Bad Request', 15 | details: 'Both email and password are required to login', 16 | }, 17 | }); 18 | } 19 | 20 | try { 21 | const authenticatedUser = await this.authService.authenticateUser(email, password); 22 | 23 | if (!authenticatedUser) { 24 | return res.status(400).json({ 25 | error: { 26 | code: 400, 27 | message: 'Bad Request', 28 | details: 'email or password is incorrect', 29 | }, 30 | }); 31 | } 32 | 33 | res.status(200).json(authenticatedUser); 34 | } catch (err) { 35 | console.log('Unable to login user:', err); 36 | 37 | return res.status(500).json({ 38 | error: { 39 | code: 500, 40 | message: 'Internal Server Error', 41 | details: 'Unable to login user', 42 | }, 43 | }); 44 | } 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/modules/auth/application/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import AuthService from '../service/auth.service'; 3 | 4 | export default class AuthMiddleware { 5 | constructor(private authService: AuthService) {} 6 | 7 | authenticate = async (req: Request, res: Response, next: NextFunction): Promise => { 8 | const authorizationHeader = req.get('Authorization'); 9 | 10 | if (!authorizationHeader) { 11 | return res.status(401).json({ 12 | error: { 13 | code: 401, 14 | message: 'Unauthorized', 15 | details: 'This operation requires login', 16 | }, 17 | }); 18 | } 19 | 20 | const jwt = authorizationHeader.replace('Bearer ', ''); 21 | 22 | try { 23 | const authenticatedUser = await this.authService.authenticateUserByToken(jwt); 24 | 25 | if (!authenticatedUser) { 26 | return res.status(401).json({ 27 | error: { 28 | code: 401, 29 | message: 'Unauthorized', 30 | details: 'This operation requires login', 31 | }, 32 | }); 33 | } 34 | 35 | req.requester = authenticatedUser; 36 | 37 | next(); 38 | } catch (err) { 39 | return res.status(500).json({ 40 | error: { 41 | code: 500, 42 | message: 'Internal Server Error', 43 | details: 'Unable to authenticate user', 44 | }, 45 | }); 46 | } 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-postgresql-prisma-solid-boilerplate", 3 | "version": "0.0.1", 4 | "description": "Boilerplate for typescript, postgresql and prisma. This boilerplate follows clean architecture with SOLID principles", 5 | "main": "index.js", 6 | "author": "tuancnttbk93@gmail.com", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "ts-node-dev --respawn --transpile-only --exit-child src/index.ts", 10 | "lint": "eslint . --ext .ts", 11 | "lint:auto-fix": "eslint . --ext .ts --quiet --fix", 12 | "build:ts": "tsc", 13 | "build:clean": "rimraf ./dist", 14 | "build": "npm run build:clean && npm run build:ts", 15 | "start": "node dist/index.js" 16 | }, 17 | "engines": { 18 | "node": "18.18.0" 19 | }, 20 | "dependencies": { 21 | "@prisma/client": "^5.4.1", 22 | "awilix": "6.0.0", 23 | "bcryptjs": "2.4.3", 24 | "body-parser": "^1.20.2", 25 | "cors": "2.8.5", 26 | "express": "^4.18.2", 27 | "jsonwebtoken": "^9.0.2" 28 | }, 29 | "devDependencies": { 30 | "@types/bcryptjs": "2.4.2", 31 | "@types/cors": "2.8.12", 32 | "@types/express": "4.17.13", 33 | "@types/jsonwebtoken": "8.5.6", 34 | "@typescript-eslint/eslint-plugin": "5.5.0", 35 | "@typescript-eslint/parser": "5.5.0", 36 | "eslint": "8.3.0", 37 | "eslint-config-prettier": "8.3.0", 38 | "eslint-plugin-prettier": "4.0.0", 39 | "prettier": "2.5.0", 40 | "prisma": "^5.4.1", 41 | "ts-node-dev": "^2.0.0", 42 | "typescript": "^5.2.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/user/persistence/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { hashSync } from 'bcryptjs'; 2 | import prismaClient from '../../common/persistence/prisma-client'; 3 | import Option from '../../common/types/Option.type'; 4 | import { CreateUserDTO, PublicUserDTO } from '../dto/user.dto'; 5 | import IUserRepository from '../service/IUserRepository'; 6 | 7 | export default class UserRepository implements IUserRepository { 8 | async getUserById(id: number): Promise> { 9 | const foundUser = await prismaClient.user.findUnique({ where: { id } }); 10 | 11 | if (!foundUser) return; 12 | 13 | const { firstName, lastName, email } = foundUser; 14 | 15 | return { 16 | id, 17 | firstName, 18 | lastName, 19 | email, 20 | }; 21 | } 22 | 23 | async getUserByEmail(email: string): Promise> { 24 | const foundUser = await prismaClient.user.findUnique({ where: { email } }); 25 | 26 | if (!foundUser) return; 27 | 28 | const { firstName, lastName, id } = foundUser; 29 | 30 | return { 31 | id, 32 | firstName, 33 | lastName, 34 | email, 35 | }; 36 | } 37 | 38 | async createUser({ firstName, lastName, email, password }: CreateUserDTO): Promise { 39 | const createdUser = await prismaClient.user.create({ 40 | data: { 41 | firstName, 42 | lastName, 43 | email, 44 | password: hashSync(password, 8), 45 | }, 46 | }); 47 | 48 | return { 49 | id: createdUser.id, 50 | firstName, 51 | lastName, 52 | email, 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/modules/user/application/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { AuthenticatedUserDTO } from '../dto/user.dto'; 3 | import UserService from '../service/user.service'; 4 | 5 | export default class UserController { 6 | constructor(private userService: UserService) {} 7 | 8 | getMyProfile = async (req: Request, res: Response): Promise => { 9 | try { 10 | const authenticatedUser = req.requester as AuthenticatedUserDTO; 11 | const user = await this.userService.getUserById(authenticatedUser.id); 12 | 13 | if (!user) { 14 | return res.status(404).json({ 15 | error: { 16 | code: 404, 17 | message: 'Not Found', 18 | details: 'User not found', 19 | }, 20 | }); 21 | } 22 | 23 | res.status(200).json(user); 24 | } catch (err) { 25 | console.log('Unable to get profile:', err); 26 | 27 | return res.status(500).json({ 28 | error: { 29 | code: 500, 30 | message: 'Server Internal Error', 31 | details: 'Unable to get profile', 32 | }, 33 | }); 34 | } 35 | }; 36 | 37 | createAccount = async (req: Request, res: Response): Promise => { 38 | try { 39 | const createdUser = await this.userService.createUser(req.body); 40 | 41 | res.status(201).json(createdUser); 42 | } catch (err) { 43 | console.log('Unable to create account:', err); 44 | 45 | return res.status(500).json({ 46 | error: { 47 | code: 500, 48 | message: 'Server Internal Error', 49 | details: 'Unable to create account', 50 | }, 51 | }); 52 | } 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/modules/user/application/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import UserService from '../service/user.service'; 3 | 4 | export default class UserMiddleware { 5 | constructor(private userService: UserService) {} 6 | 7 | validateCreateAccountBody = async (req: Request, res: Response, next: NextFunction): Promise => { 8 | const { firstName, lastName, email, password } = req.body; 9 | 10 | if (!firstName) { 11 | return res.status(400).json({ 12 | error: { 13 | code: 400, 14 | message: 'Bad Request', 15 | details: 'firstName is required', 16 | }, 17 | }); 18 | } 19 | 20 | if (!lastName) { 21 | return res.status(400).json({ 22 | error: { 23 | code: 400, 24 | message: 'Bad Request', 25 | details: 'lastName is required', 26 | }, 27 | }); 28 | } 29 | 30 | if (!email) { 31 | return res.status(400).json({ 32 | error: { 33 | code: 400, 34 | message: 'Bad Request', 35 | details: 'email is required', 36 | }, 37 | }); 38 | } 39 | 40 | if (!password) { 41 | return res.status(400).json({ 42 | error: { 43 | code: 400, 44 | message: 'Bad Request', 45 | details: 'password is required', 46 | }, 47 | }); 48 | } 49 | 50 | next(); 51 | }; 52 | 53 | requireEmailDoesNotExist = async (req: Request, res: Response, next: NextFunction): Promise => { 54 | const { email } = req.body; 55 | const foundUser = await this.userService.getUserByEmail(email); 56 | 57 | if (foundUser) { 58 | return res.status(409).json({ 59 | error: { 60 | code: 409, 61 | message: 'Bad Request', 62 | details: 'The email is already taken', 63 | }, 64 | }); 65 | } 66 | 67 | next(); 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boilerplate for express, postgresql, prisma and typescript 2 | 3 | This boilerplate follows clean architecture with SOLID principles. This sample define a simple user schema and handle two endpoints that allows users to create new accounts and login by them. There is also a middleware to validate JWT tokens that are provided by clients with `Bearer` authorization header. 4 | 5 | ## Technologies 6 | 7 | - Environment: Nodejs 8 | - Language: Typescript 9 | - Framework: Expressjs 10 | - ORM: Prisma 11 | - Database: Postgresql 12 | - Authentication: Basic, JWT 13 | - Inversion Of Control (D in SOLID): Awillix 14 | - Password hash library: bcryptjs 15 | 16 | ## Prequisite 17 | 18 | - Create the `.env` file by copying the `.env.example` file 19 | - Database: 20 | 21 | - The easy way to have a Postgresql server is using docker or you can use Postgreql cloud 22 | - Once you have the database connection URL, replace the sample one in the `.env` file by yours 23 | 24 | - Run Postgres database at port 5432 25 | 26 | ```bash 27 | docker run -e POSTGRES_PASSWORD=secrect -e POSTGRES_USER=postgres -p 5432:5432 postgres:15-alpine 28 | ``` 29 | 30 | - Create `.env` file from `.env.template` and update the `DATABASE_URL`: 31 | 32 | ```bash 33 | cp .env.template .env 34 | ``` 35 | 36 | - Edit file `.env` 37 | 38 | ``` 39 | DATABASE_URL="postgresql://postgres:secrect@localhost:5432/prisma-posgres-boilerplate?schema=public" 40 | ``` 41 | 42 | - Install dependencies 43 | 44 | ```bash 45 | npm ci 46 | ``` 47 | 48 | - Initialize database schema 49 | 50 | ```bash 51 | npx prisma migrate dev --name "init" --preview-feature 52 | ``` 53 | 54 | ## Docker build 55 | 56 | ```bash 57 | docker build -t app-name . 58 | ``` 59 | 60 | ## NPM scripts 61 | 62 | 1. Run dev 63 | 64 | ```bash 65 | npm run dev 66 | ``` 67 | 68 | 2. Linting 69 | 70 | ```bash 71 | npm run lint 72 | ``` 73 | 74 | 3. Build 75 | 76 | ```bash 77 | npm run build 78 | ``` 79 | 80 | 4. Run production 81 | 82 | ```bash 83 | npm run start 84 | ``` 85 | -------------------------------------------------------------------------------- /src/modules/auth/persistence/auth.repository.ts: -------------------------------------------------------------------------------- 1 | import { compare } from 'bcryptjs'; 2 | import { 3 | JsonWebTokenError, 4 | NotBeforeError, 5 | sign as signJWT, 6 | TokenExpiredError, 7 | verify as verifyJWT, 8 | } from 'jsonwebtoken'; 9 | import Option from '../../common/types/Option.type'; 10 | import { AUTHENTICATED_USER_TOKEN_TTL, JWT_SECRET_KEY } from '../../../config'; 11 | import prismaClient from '../../common/persistence/prisma-client'; 12 | import IAuthRepository from '../service/IAuthRepository'; 13 | import { AuthenticatedUserDTO, PublicUserDTO } from '../../user/dto/user.dto'; 14 | 15 | export default class AuthRepository implements IAuthRepository { 16 | async authenticateUser(email: string, password: string): Promise> { 17 | const foundUser = await prismaClient.user.findUnique({ where: { email } }); 18 | 19 | if (!foundUser) return; 20 | 21 | const isMatchedPassword = await compare(password, foundUser.password); 22 | 23 | if (!isMatchedPassword) return; 24 | 25 | return { 26 | id: foundUser.id, 27 | firstName: foundUser.firstName, 28 | lastName: foundUser.lastName, 29 | email: foundUser.email, 30 | }; 31 | } 32 | 33 | async getAuthenticatedUserByToken(token: string): Promise> { 34 | try { 35 | const decodedData = verifyJWT(token, JWT_SECRET_KEY); 36 | const { id, firstName, lastName, email } = decodedData as PublicUserDTO; 37 | 38 | if (!id || !email) { 39 | return; 40 | } 41 | 42 | return { 43 | id, 44 | email, 45 | token, 46 | firstName, 47 | lastName, 48 | }; 49 | } catch (err) { 50 | if (err instanceof TokenExpiredError || err instanceof JsonWebTokenError || err instanceof NotBeforeError) { 51 | return; 52 | } 53 | 54 | throw err; 55 | } 56 | } 57 | 58 | async generateToken(user: PublicUserDTO): Promise { 59 | return signJWT(user, JWT_SECRET_KEY, { expiresIn: AUTHENTICATED_USER_TOKEN_TTL }); 60 | } 61 | } 62 | --------------------------------------------------------------------------------