├── .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 |

2 | Express Logo 3 |

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(type: any, skipMissingProperties = false): RequestHandler { 7 | return (req, res, next) => { 8 | validate(plainToClass(type, req.body), { skipMissingProperties }) 9 | .then((errors: ValidationError[]) => { 10 | if (errors.length > 0) { 11 | const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(', '); 12 | next(new HttpException(400, message)); 13 | } else { 14 | next(); 15 | } 16 | }); 17 | }; 18 | } 19 | 20 | export default validationMiddleware; 21 | -------------------------------------------------------------------------------- /src/post/post.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction, Router } from 'express'; 2 | import PostNotFoundException from '../exceptions/PostNotFoundException'; 3 | import Controller from '../interfaces/controller.interface'; 4 | import RequestWithUser from '../interfaces/requestWithUser.interface'; 5 | import authMiddleware from '../middleware/auth.middleware'; 6 | import validationMiddleware from '../middleware/validation.middleware'; 7 | import CreatePostDto from './post.dto'; 8 | import Post from './post.interface'; 9 | import postModel from './post.model'; 10 | 11 | class PostController implements Controller { 12 | public path = '/posts'; 13 | public router = Router(); 14 | private post = postModel; 15 | 16 | constructor() { 17 | this.initializeRoutes(); 18 | } 19 | 20 | private initializeRoutes() { 21 | this.router.get(this.path, this.getAllPosts); 22 | this.router.get(`${this.path}/:id`, this.getPostById); 23 | this.router 24 | .all(`${this.path}/*`, authMiddleware) 25 | .patch(`${this.path}/:id`, validationMiddleware(CreatePostDto, true), this.modifyPost) 26 | .delete(`${this.path}/:id`, this.deletePost) 27 | .post(this.path, authMiddleware, validationMiddleware(CreatePostDto), this.createPost); 28 | } 29 | 30 | private getAllPosts = async (request: Request, response: Response) => { 31 | const posts = await this.post.find() 32 | .populate('author', '-password'); 33 | response.send(posts); 34 | } 35 | 36 | private getPostById = async (request: Request, response: Response, next: NextFunction) => { 37 | const id = request.params.id; 38 | const post = await this.post.findById(id); 39 | if (post) { 40 | response.send(post); 41 | } else { 42 | next(new PostNotFoundException(id)); 43 | } 44 | } 45 | 46 | private modifyPost = async (request: Request, response: Response, next: NextFunction) => { 47 | const id = request.params.id; 48 | const postData: Post = request.body; 49 | const post = await this.post.findByIdAndUpdate(id, postData, { new: true }); 50 | if (post) { 51 | response.send(post); 52 | } else { 53 | next(new PostNotFoundException(id)); 54 | } 55 | } 56 | 57 | private createPost = async (request: RequestWithUser, response: Response) => { 58 | const postData: CreatePostDto = request.body; 59 | const createdPost = new this.post({ 60 | ...postData, 61 | author: request.user._id, 62 | }); 63 | const savedPost = await createdPost.save(); 64 | await savedPost.populate('author', '-password').execPopulate(); 65 | response.send(savedPost); 66 | } 67 | 68 | private deletePost = async (request: Request, response: Response, next: NextFunction) => { 69 | const id = request.params.id; 70 | const successResponse = await this.post.findByIdAndDelete(id); 71 | if (successResponse) { 72 | response.send(200); 73 | } else { 74 | next(new PostNotFoundException(id)); 75 | } 76 | } 77 | } 78 | 79 | export default PostController; 80 | -------------------------------------------------------------------------------- /src/post/post.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | 3 | class CreatePostDto { 4 | @IsString() 5 | public content: string; 6 | 7 | @IsString() 8 | public title: string; 9 | } 10 | 11 | export default CreatePostDto; 12 | -------------------------------------------------------------------------------- /src/post/post.interface.ts: -------------------------------------------------------------------------------- 1 | interface Post { 2 | authorId: string; 3 | content: string; 4 | title: string; 5 | } 6 | 7 | export default Post; 8 | -------------------------------------------------------------------------------- /src/post/post.model.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | import Post from './post.interface'; 3 | 4 | const postSchema = new mongoose.Schema({ 5 | author: { 6 | ref: 'User', 7 | type: mongoose.Schema.Types.ObjectId, 8 | }, 9 | content: String, 10 | title: String, 11 | }); 12 | 13 | const postModel = mongoose.model('Post', postSchema); 14 | 15 | export default postModel; 16 | -------------------------------------------------------------------------------- /src/report/report.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, Router } from 'express'; 2 | import Controller from '../interfaces/controller.interface'; 3 | import userModel from '../user/user.model'; 4 | 5 | class ReportController implements Controller { 6 | public path = '/report'; 7 | public router = Router(); 8 | private user = userModel; 9 | 10 | constructor() { 11 | this.initializeRoutes(); 12 | } 13 | 14 | private initializeRoutes() { 15 | this.router.get(`${this.path}`, this.generateReport); 16 | } 17 | 18 | private generateReport = async (request: Request, response: Response) => { 19 | const usersByCountries = await this.user.aggregate( 20 | [ 21 | { 22 | $match: { 23 | 'address.country': { 24 | $exists: true, 25 | }, 26 | }, 27 | }, 28 | { 29 | $group: { 30 | _id: { 31 | country: '$address.country', 32 | }, 33 | users: { 34 | $push: { 35 | _id: '$_id', 36 | name: '$name', 37 | }, 38 | }, 39 | count: { 40 | $sum: 1, 41 | }, 42 | }, 43 | }, 44 | { 45 | $lookup: { 46 | from: 'posts', 47 | localField: 'users._id', 48 | foreignField: 'author', 49 | as: 'articles', 50 | }, 51 | }, 52 | { 53 | $addFields: { 54 | amountOfArticles: { 55 | $size: '$articles', 56 | }, 57 | }, 58 | }, 59 | { 60 | $sort: { 61 | amountOfArticles: 1, 62 | }, 63 | }, 64 | ], 65 | ); 66 | response.send({ 67 | usersByCountries, 68 | }); 69 | } 70 | 71 | } 72 | 73 | export default ReportController; 74 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import App from './app'; 3 | import AuthenticationController from './authentication/authentication.controller'; 4 | import PostController from './post/post.controller'; 5 | import ReportController from './report/report.controller'; 6 | import UserController from './user/user.controller'; 7 | import validateEnv from './utils/validateEnv'; 8 | 9 | validateEnv(); 10 | 11 | const app = new App( 12 | [ 13 | new PostController(), 14 | new AuthenticationController(), 15 | new UserController(), 16 | new ReportController(), 17 | ], 18 | ); 19 | 20 | app.listen(); 21 | -------------------------------------------------------------------------------- /src/user/address.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | 3 | class CreateAddressDto { 4 | @IsString() 5 | public street: string; 6 | @IsString() 7 | public city: string; 8 | @IsString() 9 | public country: string; 10 | } 11 | 12 | export default CreateAddressDto; 13 | -------------------------------------------------------------------------------- /src/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response, NextFunction } from 'express'; 2 | import NotAuthorizedException from '../exceptions/NotAuthorizedException'; 3 | import Controller from '../interfaces/controller.interface'; 4 | import RequestWithUser from '../interfaces/requestWithUser.interface'; 5 | import authMiddleware from '../middleware/auth.middleware'; 6 | import postModel from '../post/post.model'; 7 | import userModel from './user.model'; 8 | import UserNotFoundException from '../exceptions/UserNotFoundException'; 9 | 10 | class UserController implements Controller { 11 | public path = '/users'; 12 | public router = Router(); 13 | private post = postModel; 14 | private user = userModel; 15 | 16 | constructor() { 17 | this.initializeRoutes(); 18 | } 19 | 20 | private initializeRoutes() { 21 | this.router.get(`${this.path}/:id`, authMiddleware, this.getUserById); 22 | this.router.get(`${this.path}/:id/posts`, authMiddleware, this.getAllPostsOfUser); 23 | } 24 | 25 | private getUserById = async (request: Request, response: Response, next: NextFunction) => { 26 | const id = request.params.id; 27 | const userQuery = this.user.findById(id); 28 | if (request.query.withPosts === 'true') { 29 | userQuery.populate('posts').exec(); 30 | } 31 | const user = await userQuery; 32 | if (user) { 33 | response.send(user); 34 | } else { 35 | next(new UserNotFoundException(id)); 36 | } 37 | } 38 | 39 | private getAllPostsOfUser = async (request: RequestWithUser, response: Response, next: NextFunction) => { 40 | const userId = request.params.id; 41 | if (userId === request.user._id.toString()) { 42 | const posts = await this.post.find({ author: userId }); 43 | response.send(posts); 44 | } 45 | next(new NotAuthorizedException()); 46 | } 47 | } 48 | 49 | export default UserController; 50 | -------------------------------------------------------------------------------- /src/user/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, IsString, ValidateNested } from 'class-validator'; 2 | import CreateAddressDto from './address.dto'; 3 | 4 | class CreateUserDto { 5 | @IsString() 6 | public firstName: string; 7 | 8 | @IsString() 9 | public lastName: string; 10 | 11 | @IsString() 12 | public email: string; 13 | 14 | @IsString() 15 | public password: string; 16 | 17 | @IsOptional() 18 | @ValidateNested() 19 | public address?: CreateAddressDto; 20 | } 21 | 22 | export default CreateUserDto; 23 | -------------------------------------------------------------------------------- /src/user/user.interface.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | _id: string; 3 | firstName: string; 4 | lastName: string; 5 | fullName: string; 6 | email: string; 7 | password: string; 8 | address?: { 9 | street: string, 10 | city: string, 11 | }; 12 | } 13 | 14 | export default User; 15 | -------------------------------------------------------------------------------- /src/user/user.model.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | import User from './user.interface'; 3 | 4 | const addressSchema = new mongoose.Schema({ 5 | city: String, 6 | country: String, 7 | street: String, 8 | }); 9 | 10 | const userSchema = new mongoose.Schema( 11 | { 12 | address: addressSchema, 13 | email: String, 14 | firstName: String, 15 | lastName: String, 16 | password: { 17 | type: String, 18 | get: (): undefined => undefined, 19 | }, 20 | }, 21 | { 22 | toJSON: { 23 | virtuals: true, 24 | getters: true, 25 | }, 26 | }, 27 | ); 28 | 29 | userSchema.virtual('fullName').get(function () { 30 | return `${this.firstName} ${this.lastName}`; 31 | }); 32 | 33 | userSchema.virtual('posts', { 34 | ref: 'Post', 35 | localField: '_id', 36 | foreignField: 'author', 37 | }); 38 | 39 | const userModel = mongoose.model('User', userSchema); 40 | 41 | export default userModel; 42 | -------------------------------------------------------------------------------- /src/utils/validateEnv.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cleanEnv, port, str, 3 | } from 'envalid'; 4 | 5 | function validateEnv() { 6 | cleanEnv(process.env, { 7 | JWT_SECRET: str(), 8 | MONGO_PASSWORD: str(), 9 | MONGO_PATH: str(), 10 | MONGO_USER: str(), 11 | PORT: port(), 12 | }); 13 | } 14 | 15 | export default validateEnv; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "target": "es2017", 5 | "outDir": "./dist", 6 | "baseUrl": "./src", 7 | "alwaysStrict": true, 8 | "noImplicitAny": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true 11 | }, 12 | "include": [ 13 | "src/**/*.ts" 14 | ], 15 | "exclude": [ 16 | "node_modules" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended", 5 | "tslint-config-airbnb" 6 | ], 7 | "jsRules": { 8 | "no-unused-expression": true 9 | }, 10 | "rules": { 11 | "max-line-length": [ 12 | true, 13 | 130 14 | ], 15 | "prefer-template": false, 16 | "interface-name": [true, "never-prefix"], 17 | "import-name": false, 18 | "no-console": false, 19 | "object-literal-sort-keys": false 20 | }, 21 | "rulesDirectory": [] 22 | } 23 | --------------------------------------------------------------------------------