├── .gitignore ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── app.ts ├── authentication │ ├── authentication.controller.ts │ ├── authentication.service.ts │ ├── logIn.dto.ts │ └── tests │ │ ├── authentication.controller.test.ts │ │ └── authentication.service.test.ts ├── exceptions │ ├── AuthenticationTokenMissingException.ts │ ├── HttpException.ts │ ├── NotAuthorizedException.ts │ ├── PostNotFoundException.ts │ ├── UserNotFoundException.ts │ ├── UserWithThatEmailAlreadyExistsException.ts │ ├── WrongAuthenticationTokenException.ts │ └── WrongCredentialsException.ts ├── interfaces │ ├── controller.interface.ts │ ├── dataStoredInToken.ts │ ├── requestWithUser.interface.ts │ └── tokenData.interface.ts ├── middleware │ ├── auth.middleware.ts │ ├── error.middleware.ts │ ├── logger.middleware.ts │ └── validation.middleware.ts ├── post │ ├── post.controller.ts │ ├── post.dto.ts │ ├── post.interface.ts │ └── post.model.ts ├── report │ └── report.controller.ts ├── server.ts ├── user │ ├── address.dto.ts │ ├── user.controller.ts │ ├── user.dto.ts │ ├── user.interface.ts │ └── user.model.ts └── utils │ └── validateEnv.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | 4 | .env 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
4 | 5 | ## Description 6 | 7 | This repository is a part of the [Express Typescript tutorial](https://wanago.io/courses/typescript-express-tutorial/). 8 | 9 | Each part of the course has its own branch, called for example [_part-1_](https://github.com/mwanago/express-typescript/tree/part-1). 10 | 11 | The the [_master_](https://github.com/mwanago/express-typescript) branch represents the version with **MongoDB**. 12 | 13 | The [_postgres_](https://github.com/mwanago/express-typescript/tree/postgres) branch contains the version with **PostgreSQL**. 14 | 15 | ## Installation 16 | 17 | ```bash 18 | npm install 19 | ``` 20 | 21 | ## Running 22 | 23 | ```bash 24 | npm run dev 25 | ``` 26 | 27 | ## Testing 28 | 29 | ```bash 30 | npm run test 31 | ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-typescript", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/server.ts", 6 | "dependencies": { 7 | "@types/bcrypt": "^3.0.0", 8 | "bcrypt": "^3.0.8", 9 | "body-parser": "^1.19.0", 10 | "class-transformer": "^0.2.3", 11 | "class-validator": "^0.11.0", 12 | "cookie-parser": "^1.4.4", 13 | "dotenv": "^8.2.0", 14 | "envalid": "^6.0.1", 15 | "express": "^4.17.1", 16 | "jsonwebtoken": "^8.5.1", 17 | "mongoose": "^5.8.11" 18 | }, 19 | "devDependencies": { 20 | "@types/cookie-parser": "^1.4.2", 21 | "@types/express": "^4.17.2", 22 | "@types/jest": "^25.1.2", 23 | "@types/jsonwebtoken": "^8.3.7", 24 | "@types/mongoose": "^5.7.13", 25 | "@types/node": "^13.7.0", 26 | "@types/supertest": "^2.0.8", 27 | "husky": "^4.2.1", 28 | "jest": "^25.1.0", 29 | "node-gyp": "^6.1.0", 30 | "nodemon": "^2.0.2", 31 | "supertest": "^4.0.2", 32 | "ts-jest": "^25.2.0", 33 | "ts-node": "^8.6.2", 34 | "tslint": "^6.0.0", 35 | "tslint-config-airbnb": "^5.11.2", 36 | "typescript": "^3.7.5" 37 | }, 38 | "scripts": { 39 | "dev": "ts-node ./src/server.ts", 40 | "lint": "tslint -p tsconfig.json -c tslint.json", 41 | "test": "jest" 42 | }, 43 | "author": "Marcin Wanago", 44 | "license": "MIT", 45 | "husky": { 46 | "hooks": { 47 | "pre-commit": "npm run lint" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import * as cookieParser from 'cookie-parser'; 3 | import * as express from 'express'; 4 | import * as mongoose from 'mongoose'; 5 | import Controller from './interfaces/controller.interface'; 6 | import errorMiddleware from './middleware/error.middleware'; 7 | 8 | class App { 9 | public app: express.Application; 10 | 11 | constructor(controllers: Controller[]) { 12 | this.app = express(); 13 | 14 | this.connectToTheDatabase(); 15 | this.initializeMiddlewares(); 16 | this.initializeControllers(controllers); 17 | this.initializeErrorHandling(); 18 | } 19 | 20 | public listen() { 21 | this.app.listen(process.env.PORT, () => { 22 | console.log(`App listening on the port ${process.env.PORT}`); 23 | }); 24 | } 25 | 26 | public getServer() { 27 | return this.app; 28 | } 29 | 30 | private initializeMiddlewares() { 31 | this.app.use(bodyParser.json()); 32 | this.app.use(cookieParser()); 33 | } 34 | 35 | private initializeErrorHandling() { 36 | this.app.use(errorMiddleware); 37 | } 38 | 39 | private initializeControllers(controllers: Controller[]) { 40 | controllers.forEach((controller) => { 41 | this.app.use('/', controller.router); 42 | }); 43 | } 44 | 45 | private connectToTheDatabase() { 46 | const { 47 | MONGO_USER, 48 | MONGO_PASSWORD, 49 | MONGO_PATH, 50 | } = process.env; 51 | mongoose.connect(`mongodb://${MONGO_USER}:${MONGO_PASSWORD}${MONGO_PATH}`); 52 | } 53 | } 54 | 55 | export default App; 56 | -------------------------------------------------------------------------------- /src/authentication/authentication.controller.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcrypt'; 2 | import { Request, Response, NextFunction, Router } from 'express'; 3 | import * as jwt from 'jsonwebtoken'; 4 | import WrongCredentialsException from '../exceptions/WrongCredentialsException'; 5 | import Controller from '../interfaces/controller.interface'; 6 | import DataStoredInToken from '../interfaces/dataStoredInToken'; 7 | import TokenData from '../interfaces/tokenData.interface'; 8 | import validationMiddleware from '../middleware/validation.middleware'; 9 | import CreateUserDto from '../user/user.dto'; 10 | import User from '../user/user.interface'; 11 | import userModel from './../user/user.model'; 12 | import AuthenticationService from './authentication.service'; 13 | import LogInDto from './logIn.dto'; 14 | 15 | class AuthenticationController implements Controller { 16 | public path = '/auth'; 17 | public router = Router(); 18 | public authenticationService = new AuthenticationService(); 19 | private user = userModel; 20 | 21 | constructor() { 22 | this.initializeRoutes(); 23 | } 24 | 25 | private initializeRoutes() { 26 | this.router.post(`${this.path}/register`, validationMiddleware(CreateUserDto), this.registration); 27 | this.router.post(`${this.path}/login`, validationMiddleware(LogInDto), this.loggingIn); 28 | this.router.post(`${this.path}/logout`, this.loggingOut); 29 | } 30 | 31 | private registration = async (request: Request, response: Response, next: NextFunction) => { 32 | const userData: CreateUserDto = request.body; 33 | try { 34 | const { 35 | cookie, 36 | user, 37 | } = await this.authenticationService.register(userData); 38 | response.setHeader('Set-Cookie', [cookie]); 39 | response.send(user); 40 | } catch (error) { 41 | next(error); 42 | } 43 | } 44 | 45 | private loggingIn = async (request: Request, response: Response, next: NextFunction) => { 46 | const logInData: LogInDto = request.body; 47 | const user = await this.user.findOne({ email: logInData.email }); 48 | if (user) { 49 | const isPasswordMatching = await bcrypt.compare( 50 | logInData.password, 51 | user.get('password', null, { getters: false }), 52 | ); 53 | if (isPasswordMatching) { 54 | const tokenData = this.createToken(user); 55 | response.setHeader('Set-Cookie', [this.createCookie(tokenData)]); 56 | response.send(user); 57 | } else { 58 | next(new WrongCredentialsException()); 59 | } 60 | } else { 61 | next(new WrongCredentialsException()); 62 | } 63 | } 64 | 65 | private loggingOut = (request: Request, response: Response) => { 66 | response.setHeader('Set-Cookie', ['Authorization=;Max-age=0']); 67 | response.send(200); 68 | } 69 | 70 | private createCookie(tokenData: TokenData) { 71 | return `Authorization=${tokenData.token}; HttpOnly; Max-Age=${tokenData.expiresIn}`; 72 | } 73 | 74 | private createToken(user: User): TokenData { 75 | const expiresIn = 60 * 60; // an hour 76 | const secret = process.env.JWT_SECRET; 77 | const dataStoredInToken: DataStoredInToken = { 78 | _id: user._id, 79 | }; 80 | return { 81 | expiresIn, 82 | token: jwt.sign(dataStoredInToken, secret, { expiresIn }), 83 | }; 84 | } 85 | 86 | } 87 | 88 | export default AuthenticationController; 89 | -------------------------------------------------------------------------------- /src/authentication/authentication.service.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcrypt'; 2 | import * as jwt from 'jsonwebtoken'; 3 | import UserWithThatEmailAlreadyExistsException from '../exceptions/UserWithThatEmailAlreadyExistsException'; 4 | import DataStoredInToken from '../interfaces/dataStoredInToken'; 5 | import TokenData from '../interfaces/tokenData.interface'; 6 | import CreateUserDto from '../user/user.dto'; 7 | import User from '../user/user.interface'; 8 | import userModel from './../user/user.model'; 9 | 10 | class AuthenticationService { 11 | public user = userModel; 12 | 13 | public async register(userData: CreateUserDto) { 14 | if ( 15 | await this.user.findOne({ email: userData.email }) 16 | ) { 17 | throw new UserWithThatEmailAlreadyExistsException(userData.email); 18 | } 19 | const hashedPassword = await bcrypt.hash(userData.password, 10); 20 | const user = await this.user.create({ 21 | ...userData, 22 | password: hashedPassword, 23 | }); 24 | const tokenData = this.createToken(user); 25 | const cookie = this.createCookie(tokenData); 26 | return { 27 | cookie, 28 | user, 29 | }; 30 | } 31 | public createCookie(tokenData: TokenData) { 32 | return `Authorization=${tokenData.token}; HttpOnly; Max-Age=${tokenData.expiresIn}`; 33 | } 34 | public createToken(user: User): TokenData { 35 | const expiresIn = 60 * 60; // an hour 36 | const secret = process.env.JWT_SECRET; 37 | const dataStoredInToken: DataStoredInToken = { 38 | _id: user._id, 39 | }; 40 | return { 41 | expiresIn, 42 | token: jwt.sign(dataStoredInToken, secret, { expiresIn }), 43 | }; 44 | } 45 | } 46 | 47 | export default AuthenticationService; 48 | -------------------------------------------------------------------------------- /src/authentication/logIn.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | 3 | class LogInDto { 4 | @IsString() 5 | public email: string; 6 | 7 | @IsString() 8 | public password: string; 9 | } 10 | 11 | export default LogInDto; 12 | -------------------------------------------------------------------------------- /src/authentication/tests/authentication.controller.test.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | import * as request from 'supertest'; 3 | import App from '../../app'; 4 | import CreateUserDto from '../../user/user.dto'; 5 | import AuthenticationController from '../authentication.controller'; 6 | 7 | describe('The AuthenticationController', () => { 8 | describe('POST /auth/register', () => { 9 | describe('if the email is not taken', () => { 10 | it('response should have the Set-Cookie header with the Authorization token', () => { 11 | const userData: CreateUserDto = { 12 | name: 'John Smith', 13 | email: 'john@smith.com', 14 | password: 'strongPassword123', 15 | }; 16 | process.env.JWT_SECRET = 'jwt_secret'; 17 | const authenticationController = new AuthenticationController(); 18 | authenticationController.authenticationService.user.findOne = jest.fn().mockReturnValue(Promise.resolve(undefined)); 19 | authenticationController.authenticationService.user.create = jest.fn().mockReturnValue({ 20 | ...userData, 21 | _id: 0, 22 | }); 23 | (mongoose as any).connect = jest.fn(); 24 | const app = new App([ 25 | authenticationController, 26 | ]); 27 | return request(app.getServer()) 28 | .post(`${authenticationController.path}/register`) 29 | .send(userData) 30 | .expect('Set-Cookie', /^Authorization=.+/); 31 | }); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/authentication/tests/authentication.service.test.ts: -------------------------------------------------------------------------------- 1 | import UserWithThatEmailAlreadyExistsException from '../../exceptions/UserWithThatEmailAlreadyExistsException'; 2 | import TokenData from '../../interfaces/tokenData.interface'; 3 | import CreateUserDto from '../../user/user.dto'; 4 | import AuthenticationService from '../authentication.service'; 5 | 6 | describe('The AuthenticationService', () => { 7 | describe('when creating a cookie', () => { 8 | it('should return a string', () => { 9 | const tokenData: TokenData = { 10 | token: '', 11 | expiresIn: 1, 12 | }; 13 | const authenticationService = new AuthenticationService(); 14 | expect(typeof authenticationService.createCookie(tokenData)) 15 | .toEqual('string'); 16 | }); 17 | }); 18 | describe('when registering a user', () => { 19 | describe('if the email is already taken', () => { 20 | it('should throw an error', async () => { 21 | const userData: CreateUserDto = { 22 | name: 'John Smith', 23 | email: 'john@smith.com', 24 | password: 'strongPassword123', 25 | }; 26 | const authenticationService = new AuthenticationService(); 27 | authenticationService.user.findOne = jest.fn().mockReturnValue(Promise.resolve(userData)); 28 | await expect(authenticationService.register(userData)) 29 | .rejects.toMatchObject(new UserWithThatEmailAlreadyExistsException(userData.email)); 30 | }); 31 | }); 32 | describe('if the email is not taken', () => { 33 | it('should not throw an error', async () => { 34 | const userData: CreateUserDto = { 35 | name: 'John Smith', 36 | email: 'john@smith.com', 37 | password: 'strongPassword123', 38 | }; 39 | process.env.JWT_SECRET = 'jwt_secret'; 40 | const authenticationService = new AuthenticationService(); 41 | authenticationService.user.findOne = jest.fn().mockReturnValue(Promise.resolve(undefined)); 42 | authenticationService.user.create = jest.fn().mockReturnValue({ 43 | ...userData, 44 | _id: 0, 45 | }); 46 | await expect(authenticationService.register(userData)) 47 | .resolves.toBeDefined(); 48 | }); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/exceptions/AuthenticationTokenMissingException.ts: -------------------------------------------------------------------------------- 1 | import HttpException from './HttpException'; 2 | 3 | class AuthenticationTokenMissingException extends HttpException { 4 | constructor() { 5 | super(401, 'Authentication token missing'); 6 | } 7 | } 8 | 9 | export default AuthenticationTokenMissingException; 10 | -------------------------------------------------------------------------------- /src/exceptions/HttpException.ts: -------------------------------------------------------------------------------- 1 | class HttpException extends Error { 2 | public status: number; 3 | public message: string; 4 | constructor(status: number, message: string) { 5 | super(message); 6 | this.status = status; 7 | this.message = message; 8 | } 9 | } 10 | 11 | export default HttpException; 12 | -------------------------------------------------------------------------------- /src/exceptions/NotAuthorizedException.ts: -------------------------------------------------------------------------------- 1 | import HttpException from './HttpException'; 2 | 3 | class NotAuthorizedException extends HttpException { 4 | constructor() { 5 | super(403, "You're not authorized"); 6 | } 7 | } 8 | 9 | export default NotAuthorizedException; 10 | -------------------------------------------------------------------------------- /src/exceptions/PostNotFoundException.ts: -------------------------------------------------------------------------------- 1 | import HttpException from './HttpException'; 2 | 3 | class PostNotFoundException extends HttpException { 4 | constructor(id: string) { 5 | super(404, `Post with id ${id} not found`); 6 | } 7 | } 8 | 9 | export default PostNotFoundException; 10 | -------------------------------------------------------------------------------- /src/exceptions/UserNotFoundException.ts: -------------------------------------------------------------------------------- 1 | import HttpException from './HttpException'; 2 | 3 | class UserNotFoundException extends HttpException { 4 | constructor(id: string) { 5 | super(404, `User with id ${id} not found`); 6 | } 7 | } 8 | 9 | export default UserNotFoundException; 10 | -------------------------------------------------------------------------------- /src/exceptions/UserWithThatEmailAlreadyExistsException.ts: -------------------------------------------------------------------------------- 1 | import HttpException from './HttpException'; 2 | 3 | class UserWithThatEmailAlreadyExistsException extends HttpException { 4 | constructor(email: string) { 5 | super(400, `User with email ${email} already exists`); 6 | } 7 | } 8 | 9 | export default UserWithThatEmailAlreadyExistsException; 10 | -------------------------------------------------------------------------------- /src/exceptions/WrongAuthenticationTokenException.ts: -------------------------------------------------------------------------------- 1 | import HttpException from './HttpException'; 2 | 3 | class WrongAuthenticationTokenException extends HttpException { 4 | constructor() { 5 | super(401, 'Wrong authentication token'); 6 | } 7 | } 8 | 9 | export default WrongAuthenticationTokenException; 10 | -------------------------------------------------------------------------------- /src/exceptions/WrongCredentialsException.ts: -------------------------------------------------------------------------------- 1 | import HttpException from './HttpException'; 2 | 3 | class WrongCredentialsException extends HttpException { 4 | constructor() { 5 | super(401, 'Wrong credentials provided'); 6 | } 7 | } 8 | 9 | export default WrongCredentialsException; 10 | -------------------------------------------------------------------------------- /src/interfaces/controller.interface.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | interface Controller { 4 | path: string; 5 | router: Router; 6 | } 7 | 8 | export default Controller; 9 | -------------------------------------------------------------------------------- /src/interfaces/dataStoredInToken.ts: -------------------------------------------------------------------------------- 1 | interface DataStoredInToken { 2 | _id: string; 3 | } 4 | 5 | export default DataStoredInToken; 6 | -------------------------------------------------------------------------------- /src/interfaces/requestWithUser.interface.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import User from 'users/user.interface'; 3 | 4 | interface RequestWithUser extends Request { 5 | user: User; 6 | } 7 | 8 | export default RequestWithUser; 9 | -------------------------------------------------------------------------------- /src/interfaces/tokenData.interface.ts: -------------------------------------------------------------------------------- 1 | interface TokenData { 2 | token: string; 3 | expiresIn: number; 4 | } 5 | 6 | export default TokenData; 7 | -------------------------------------------------------------------------------- /src/middleware/auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Response } from 'express'; 2 | import * as jwt from 'jsonwebtoken'; 3 | import AuthenticationTokenMissingException from '../exceptions/AuthenticationTokenMissingException'; 4 | import WrongAuthenticationTokenException from '../exceptions/WrongAuthenticationTokenException'; 5 | import DataStoredInToken from '../interfaces/dataStoredInToken'; 6 | import RequestWithUser from '../interfaces/requestWithUser.interface'; 7 | import userModel from '../user/user.model'; 8 | 9 | async function authMiddleware(request: RequestWithUser, response: Response, next: NextFunction) { 10 | const cookies = request.cookies; 11 | if (cookies && cookies.Authorization) { 12 | const secret = process.env.JWT_SECRET; 13 | try { 14 | const verificationResponse = jwt.verify(cookies.Authorization, secret) as DataStoredInToken; 15 | const id = verificationResponse._id; 16 | const user = await userModel.findById(id); 17 | if (user) { 18 | request.user = user; 19 | next(); 20 | } else { 21 | next(new WrongAuthenticationTokenException()); 22 | } 23 | } catch (error) { 24 | next(new WrongAuthenticationTokenException()); 25 | } 26 | } else { 27 | next(new AuthenticationTokenMissingException()); 28 | } 29 | } 30 | 31 | export default authMiddleware; 32 | -------------------------------------------------------------------------------- /src/middleware/error.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import HttpException from '../exceptions/HttpException'; 3 | 4 | function errorMiddleware(error: HttpException, request: Request, response: Response, next: NextFunction) { 5 | const status = error.status || 500; 6 | const message = error.message || 'Something went wrong'; 7 | response 8 | .status(status) 9 | .send({ 10 | message, 11 | status, 12 | }); 13 | } 14 | 15 | export default errorMiddleware; 16 | -------------------------------------------------------------------------------- /src/middleware/logger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request } from 'express'; 2 | 3 | function loggerMiddleware(request: Request, response: Response, next: NextFunction) { 4 | console.log(`${request.method} ${request.path}`); 5 | next(); 6 | } 7 | 8 | export default loggerMiddleware; 9 | -------------------------------------------------------------------------------- /src/middleware/validation.middleware.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from 'class-transformer'; 2 | import { validate, ValidationError } from 'class-validator'; 3 | import { RequestHandler } from 'express'; 4 | import HttpException from '../exceptions/HttpException'; 5 | 6 | function validationMiddleware