├── .gitignore ├── README.md ├── __tests__ ├── controllers │ └── AuthController.spec.ts └── helpers │ ├── FakeRepository.ts │ └── helpers.ts ├── package-lock.json ├── package.json ├── src ├── application │ ├── repositories │ │ └── IUserReadOnlyRepository.ts │ └── usecase │ │ ├── ISigninUseCase.ts │ │ ├── IUserDto.ts │ │ └── SignInUseCase.ts ├── configuration │ └── usecases │ │ └── AuthServiceLocator.ts ├── constants │ └── types.ts ├── domain │ └── User.ts ├── entrypoint │ └── controllers │ │ └── AuthController.ts ├── index.ts └── infrastructure │ └── UserRepository.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | #node modules 2 | node_modules/ 3 | dist/ 4 | .nyc_output/ 5 | .vscode/ 6 | coverage 7 | 8 | .package-lock.json 9 | 10 | # environment variables 11 | .env 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clean-authentication-flow 2 | An architectural approach to creating a signUp/SignIn flow API 3 | 4 | -------------------------------------------------------------------------------- /__tests__/controllers/AuthController.spec.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | // tslint:disable-next-line:ordered-imports 3 | import chai from "chai"; 4 | import "mocha"; 5 | import { it } from "mocha"; 6 | import sinon, { SinonSandbox } from "sinon"; 7 | import sinonChai from "sinon-chai"; 8 | import AuthServiceLocator from "../../src/configuration/usecases/AuthServiceLocator"; 9 | import AuthController from "../../src/entrypoint/controllers/AuthController"; 10 | import FakeUserRepository from "../helpers/FakeRepository"; 11 | import {mockRequest, mockResponse } from "../helpers/helpers"; 12 | 13 | const { expect } = chai; 14 | 15 | chai.use(sinonChai); 16 | 17 | describe("Auth Controller", () => { 18 | let sut: AuthController; 19 | let sandbox: SinonSandbox = null; 20 | let serviceLocator: AuthServiceLocator; 21 | let fakeRepository: FakeUserRepository; 22 | 23 | const user = { 24 | email: "baller@gg.com", 25 | id : "1234", 26 | name: "Ken", 27 | password: "pass", 28 | type: "email" 29 | }; 30 | 31 | const req: any = mockRequest(user); 32 | const res: any = mockResponse(); 33 | 34 | beforeEach(() => { 35 | fakeRepository = new FakeUserRepository(); 36 | serviceLocator = new AuthServiceLocator(fakeRepository); 37 | sandbox = sinon.createSandbox(); 38 | 39 | sut = new AuthController(serviceLocator); 40 | 41 | }); 42 | 43 | afterEach(() => { 44 | sandbox.restore(); 45 | }); 46 | 47 | describe("sign", () => { 48 | it("should return 400 on empty request", async () => { 49 | sandbox.spy(res, "status"); 50 | sandbox.spy(res, "json"); 51 | 52 | const emptyReq: any = { body: {} }; 53 | await sut.sigin(emptyReq, res); 54 | 55 | expect(res.status).to.have.been.calledWith(400); 56 | }); 57 | 58 | it("should return 200 and a user", async () => { 59 | sandbox.spy(res, "status"); 60 | sandbox.spy(res, "json"); 61 | 62 | await sut.sigin(req, res); 63 | 64 | expect(res.status).to.have.been.calledWith(200); 65 | expect(res.json).to.have.been.calledWith(user); 66 | }); 67 | }); 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /__tests__/helpers/FakeRepository.ts: -------------------------------------------------------------------------------- 1 | import IUserReadOnlyRepository from "../../src/application/repositories/IUserReadOnlyRepository"; 2 | import User from "../../src/domain/User"; 3 | 4 | export default class FakeUserRepository implements IUserReadOnlyRepository { 5 | 6 | public users = [{ 7 | email: "baller@gg.com", 8 | id : "1234", 9 | name: "Ken", 10 | password: "pass", 11 | type: "email" 12 | }, 13 | { 14 | email: "tester@gmail.com", 15 | id : "1556", 16 | name: "Ren", 17 | password: "pass123", 18 | type: "email" 19 | }]; 20 | 21 | public async fetch(user: User): Promise { 22 | const res = await this.users.find((x) => x.email === user.email); 23 | if (!res) { 24 | return null; 25 | } 26 | 27 | if (res.password !== user.password) { 28 | throw Error("Invalid email or password"); 29 | } 30 | 31 | user.id = res.id; 32 | user.name = res.name; 33 | return user; 34 | } 35 | 36 | public async add(user: User): Promise { 37 | const max = 9999; 38 | const min = 1000; 39 | user.id = (Math.floor(Math.random() * (+max - +min)) + +min).toString(); 40 | 41 | this.users.push({ 42 | email: user.email, 43 | id: user.id, 44 | name: user.name, 45 | password: user.password, 46 | type: user.type 47 | }); 48 | return user; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /__tests__/helpers/helpers.ts: -------------------------------------------------------------------------------- 1 | const mockRequest = (data: any) => { 2 | const req: any = { }; 3 | req.body = data; 4 | return req; 5 | }; 6 | 7 | const mockResponse = () => { 8 | const res = { 9 | json() { 10 | return this; 11 | }, 12 | status() { 13 | return this; 14 | }, 15 | }; 16 | return res; 17 | }; 18 | 19 | export {mockRequest, mockResponse }; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clean-authentication-flow", 3 | "version": "1.0.0", 4 | "description": "architectural approach to creating a sign in flow", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "clean": "rimraf dist/*", 8 | "lint": "tslint -c tslint.json -p tsconfig.json --fix", 9 | "tsc": "tsc", 10 | "build": "npm-run-all clean lint tsc", 11 | "dev:start": "npm-run-all build start", 12 | "dev": "nodemon --watch src -e ts,ejs --exec npm run dev:start", 13 | "start": "node .", 14 | "test": "nyc --clean --all --require ts-node/register --require tsconfig-paths/register --require reflect-metadata/Reflect --extension .ts -- mocha --exit --timeout 5000", 15 | "test:all": "npm test __tests__/**/*.spec.ts" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "dependencies": { 21 | "@types/express-promise-router": "^2.0.1", 22 | "@types/joi": "^14.3.2", 23 | "@types/jsonwebtoken": "^8.3.2", 24 | "@types/webpack-env": "^1.13.9", 25 | "body-parser": "^1.18.3", 26 | "dotenv": "^7.0.0", 27 | "express": "^4.16.4", 28 | "express-promise-router": "^3.0.3", 29 | "inversify": "^5.0.1", 30 | "inversify-express-utils": "^6.3.2", 31 | "joi": "^14.3.1", 32 | "jsonwebtoken": "^8.5.1", 33 | "module-alias": "^2.2.0", 34 | "reflect-metadata": "^0.1.13" 35 | }, 36 | "devDependencies": { 37 | "@types/chai": "^4.1.7", 38 | "@types/chai-http": "^4.2.0", 39 | "@types/dotenv": "^6.1.1", 40 | "@types/express": "^4.16.1", 41 | "@types/faker": "^4.1.5", 42 | "@types/mocha": "^5.2.6", 43 | "@types/node": "^11.12.2", 44 | "@types/sinon-chai": "^3.2.2", 45 | "chai": "^4.2.0", 46 | "chai-http": "^4.2.1", 47 | "faker": "^4.1.0", 48 | "mocha": "^6.0.2", 49 | "nodemon": "^1.18.10", 50 | "npm-run-all": "^4.1.5", 51 | "nyc": "^13.3.0", 52 | "rimraf": "^2.6.3", 53 | "sinon": "^7.3.1", 54 | "sinon-chai": "^3.3.0", 55 | "ts-mocha": "^6.0.0", 56 | "ts-node": "^8.0.3", 57 | "tsconfig-paths": "^3.8.0", 58 | "tslint": "^5.14.0", 59 | "typescript": "^3.4.1" 60 | }, 61 | "_moduleAliases": { 62 | "@pbb": "dist" 63 | }, 64 | "nyc": { 65 | "exclude": [ 66 | "__tests__/**/*.spec.ts", 67 | "dist/**" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/application/repositories/IUserReadOnlyRepository.ts: -------------------------------------------------------------------------------- 1 | import User from "@pbb/domain/User"; 2 | 3 | export default interface IUserReadOnlyRepository { 4 | fetch(user: User): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/application/usecase/ISigninUseCase.ts: -------------------------------------------------------------------------------- 1 | import IUserDto from "./IUserDto"; 2 | 3 | export default interface ISigninUseCase { 4 | signin(userDto: IUserDto): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/application/usecase/IUserDto.ts: -------------------------------------------------------------------------------- 1 | export default interface IUserDto { 2 | id: string; 3 | email: string; 4 | name: string; 5 | password: string; 6 | type: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/application/usecase/SignInUseCase.ts: -------------------------------------------------------------------------------- 1 | import User from "@pbb/domain/User"; 2 | import IUserReadOnlyRepository from "../repositories/IUserReadOnlyRepository"; 3 | import ISigninUseCase from "./ISigninUseCase"; 4 | import IUserDto from "./IUserDto"; 5 | 6 | export default class SigninUseCase implements ISigninUseCase { 7 | 8 | private userReadOnlyRepository: IUserReadOnlyRepository; 9 | 10 | constructor(userReadOnlyRepository: IUserReadOnlyRepository) { 11 | this.userReadOnlyRepository = userReadOnlyRepository; 12 | } 13 | 14 | public async signin(userDto: IUserDto): Promise { 15 | let user = new User(userDto.id, userDto.name, 16 | userDto.email, userDto.password, 17 | userDto.type); 18 | user = await this.userReadOnlyRepository.fetch(user); 19 | 20 | if (!user) { 21 | throw Error("user not found"); 22 | } 23 | 24 | const foundUserDto = user; 25 | 26 | return foundUserDto; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/configuration/usecases/AuthServiceLocator.ts: -------------------------------------------------------------------------------- 1 | import IUserReadOnlyRepository from "@pbb/application/repositories/IUserReadOnlyRepository"; 2 | import SigninUseCase from "@pbb/application/usecase/SignInUseCase"; 3 | import { TYPES } from "@pbb/constants/types"; 4 | import { inject, injectable } from "inversify"; 5 | 6 | @injectable() 7 | export default class AuthServiceLocator { 8 | 9 | constructor(@inject(TYPES.IUserReadOnlyRepository) 10 | private readRepository: IUserReadOnlyRepository) { } 11 | 12 | public GetSignInUseCase() { 13 | return new SigninUseCase(this.readRepository); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/constants/types.ts: -------------------------------------------------------------------------------- 1 | export const TYPES = { 2 | AuthServiceLocator : Symbol.for("AuthServiceLocator"), 3 | IUserReadOnlyRepository: Symbol.for("IUserReadOnlyRepository") 4 | }; 5 | -------------------------------------------------------------------------------- /src/domain/User.ts: -------------------------------------------------------------------------------- 1 | export default class User { 2 | constructor( 3 | public id: string, 4 | public name: string, 5 | public email: string, public password: string, 6 | public type: string) { } 7 | } 8 | -------------------------------------------------------------------------------- /src/entrypoint/controllers/AuthController.ts: -------------------------------------------------------------------------------- 1 | import ISigninUseCase from "@pbb/application/usecase/ISigninUseCase"; 2 | import IUserDto from "@pbb/application/usecase/IUserDto"; 3 | import AuthServiceLocator from "@pbb/configuration/usecases/AuthServiceLocator"; 4 | import { TYPES } from "@pbb/constants/types"; 5 | import * as express from "express"; 6 | import { inject } from "inversify"; 7 | import {controller, httpPost, interfaces, request, response } from "inversify-express-utils"; 8 | 9 | @controller("/auth") 10 | export default class AuthController implements interfaces.Controller { 11 | private readonly signInUseCase: ISigninUseCase; 12 | 13 | constructor(@inject(TYPES.AuthServiceLocator) serviceLocator: AuthServiceLocator) { 14 | this.signInUseCase = serviceLocator.GetSignInUseCase(); 15 | } 16 | 17 | @httpPost("/signin") 18 | public async sigin(@request() req: express.Request, @response() res: express.Response) { 19 | const userDto: IUserDto = req.body; 20 | return this.signInUseCase.signin(userDto) 21 | .then((foundUserDto: IUserDto) => res.status(200).json(foundUserDto)) 22 | .catch((err: Error) => res.status(400).json({error: err.message})); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | // tslint:disable-next-line:ordered-imports 3 | import * as bodyParser from "body-parser"; 4 | import * as express from "express"; 5 | import { Container } from "inversify"; 6 | import { InversifyExpressServer } from "inversify-express-utils"; 7 | import IUserReadOnlyRepository from "./application/repositories/IUserReadOnlyRepository"; 8 | import AuthServiceLocator from "./configuration/usecases/AuthServiceLocator"; 9 | import { TYPES } from "./constants/types"; 10 | import UserRepository from "./infrastructure/UserRepository"; 11 | 12 | const container = new Container(); 13 | 14 | // set up bindings 15 | container.bind(TYPES.AuthServiceLocator).to(AuthServiceLocator); 16 | container.bind(TYPES.IUserReadOnlyRepository).to(UserRepository); 17 | 18 | const server = new InversifyExpressServer(container); 19 | server.setConfig((application: express.Application) => { 20 | application.use(bodyParser.urlencoded({extended: true})); 21 | application.use(bodyParser.json()); 22 | }); 23 | 24 | const app = server.build(); 25 | 26 | app.listen(5000, () => { 27 | // tslint:disable-next-line:no-console 28 | console.log(`server started at http://localhost:${5000}`); 29 | }); 30 | -------------------------------------------------------------------------------- /src/infrastructure/UserRepository.ts: -------------------------------------------------------------------------------- 1 | import IUserReadOnlyRepository from "@pbb/application/repositories/IUserReadOnlyRepository"; 2 | import User from "@pbb/domain/User"; 3 | import { injectable } from "inversify"; 4 | 5 | @injectable() 6 | export default class UserRepository implements IUserReadOnlyRepository { 7 | 8 | public async fetch(user: User): Promise { 9 | throw new Error("Method not implemented."); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": false, 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "importHelpers": true, 12 | "allowSyntheticDefaultImports": true, 13 | "emitDecoratorMetadata": true, 14 | "experimentalDecorators": true, 15 | "lib": ["es6", "dom"], 16 | "types": [ 17 | "webpack-env", 18 | "node", 19 | "mocha", 20 | "chai", 21 | "reflect-metadata" 22 | ], 23 | "paths": { 24 | "@pbb/*": [ 25 | "src/*" 26 | ] 27 | }, 28 | }, 29 | "include": [ 30 | "src/**/*","index.ts" 31 | ], 32 | "exclude": [ 33 | "node_modules", 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "trailing-comma": [ false ] 9 | }, 10 | "rulesDirectory": [] 11 | } 12 | --------------------------------------------------------------------------------