├── .gitignore ├── .mocharc.js ├── .nycrc ├── .travis.yml ├── Dockerfile ├── docker-compose.yml ├── package.json ├── readme.md ├── scripts └── mocha │ └── register.js ├── src ├── Server.ts ├── controllers │ ├── passport │ │ ├── PassportCtrl.integration.spec.ts │ │ └── PassportCtrl.ts │ └── users │ │ └── UsersCtrl.ts ├── entities │ └── User.ts ├── index.ts ├── models │ ├── Credentials.ts │ └── UserCreation.ts ├── protocols │ ├── LoginLocalProtocol.spec.ts │ ├── LoginLocalProtocol.ts │ └── SignupLocalProtocol.ts └── repositories │ └── UserRepository.ts ├── test └── helpers │ └── bootstrapServer.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node template 2 | .DS_Store 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 30 | node_modules 31 | .npmrc 32 | *.log 33 | 34 | # Typings 35 | typings/ 36 | 37 | # Typescript 38 | src/**/*.js 39 | src/**/*.js.map 40 | test/**/*.js 41 | test/**/*.js.map 42 | 43 | # Test 44 | /coverage 45 | /stdout 46 | /stderr 47 | /es6/ 48 | /lib/ 49 | /dts/ 50 | /.tmp 51 | /.nyc_output/ 52 | /mongodb 53 | 54 | # IDE 55 | .vscode 56 | .idea 57 | 58 | # Project 59 | public 60 | /dist 61 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | require: [ 3 | "ts-node/register/transpile-only", 4 | "tsconfig-paths/register", 5 | "scripts/mocha/register" 6 | ], 7 | recursive: true, 8 | reporter: "dot", 9 | spec: [ 10 | "src/**/*.spec.ts" 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*.ts" 4 | ], 5 | "exclude": [ 6 | "**/*.d.ts", 7 | "node_modules", 8 | "**/index.ts", 9 | "**/interfaces/**", 10 | "**/*.spec.ts", 11 | "src/test/tools.js" 12 | ], 13 | "reporter": [ 14 | "text-summary", 15 | "html", 16 | "lcov" 17 | ], 18 | "extension": [ 19 | ".ts" 20 | ], 21 | "check-coverage": true, 22 | "lines": 99, 23 | "statements": 100, 24 | "functions": 99, 25 | "branches": 89, 26 | "all": true 27 | } 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | cache: 4 | yarn: true 5 | 6 | node_js: 7 | - '12.13.0' 8 | 9 | jobs: 10 | include: 11 | - stage: test 12 | name: Unit tests 13 | script: yarn test 14 | - stage: build 15 | name: Build 16 | script: yarn docker:build 17 | - stage: deploy 18 | name: Deploy 19 | script: yarn docker:build && yarn deploy 20 | 21 | stages: 22 | - test 23 | - name: build 24 | if: branch != master 25 | - name: deploy 26 | if: branch = master 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | ############################################################################### 3 | ## _______ _____ ______ _____ ## 4 | ## |__ __/ ____| ____| __ \ ## 5 | ## | | | (___ | |__ | | | | ## 6 | ## | | \___ \| __| | | | | ## 7 | ## | | ____) | |____| |__| | ## 8 | ## |_| |_____/|______|_____/ ## 9 | ## ## 10 | ## description : Dockerfile for TsED Application ## 11 | ## author : TsED team ## 12 | ## date : 20190820 ## 13 | ## version : 1.0 ## 14 | ############################################################################### 15 | ############################################################################### 16 | FROM node:12.13.0-alpine 17 | 18 | RUN apk update && apk add build-base git python 19 | 20 | COPY package.json . 21 | COPY yarn.lock . 22 | COPY ./src ./src 23 | COPY ./dist ./dist 24 | 25 | RUN yarn install --production 26 | 27 | EXPOSE 8083 28 | ENV PORT 8083 29 | ENV NODE_ENV production 30 | 31 | CMD ["yarn", "start:prod"] 32 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | postgres: 5 | image: postgres:11 6 | container_name: postgres_container 7 | environment: 8 | POSTGRES_USER: postgres 9 | POSTGRES_PASSWORD: changeme 10 | PGDATA: /data/postgres 11 | volumes: 12 | - postgres:/data/postgres 13 | ports: 14 | - "5432:5432" 15 | networks: 16 | - postgres 17 | restart: unless-stopped 18 | 19 | pgadmin: 20 | image: dpage/pgadmin4 21 | container_name: pgadmin_container 22 | environment: 23 | PGADMIN_DEFAULT_EMAIL: pgadmin4@pgadmin.org 24 | PGADMIN_DEFAULT_PASSWORD: admin 25 | volumes: 26 | - pgadmin:/root/.pgadmin 27 | ports: 28 | - "5050:80" 29 | restart: unless-stopped 30 | networks: 31 | - postgres 32 | 33 | server: 34 | build: 35 | context: . 36 | dockerfile: ./Dockerfile 37 | args: 38 | - http_proxy 39 | - https_proxy 40 | - no_proxy 41 | image: tsed/typeorm:latest 42 | container_name: server_container 43 | restart: always 44 | environment: 45 | POSTGRES_HOST: postgres 46 | POSTGRES_USER: postgres 47 | POSTGRES_PASSWORD: changeme 48 | depends_on: 49 | - postgres 50 | ports: 51 | - "8083:8083" 52 | stdin_open: true # -i 53 | tty: true # -t 54 | networks: 55 | - postgres 56 | 57 | networks: 58 | postgres: 59 | driver: bridge 60 | 61 | volumes: 62 | postgres: 63 | pgadmin: 64 | 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tsed/typeorm-example", 3 | "version": "1.0.0", 4 | "description": "Here an example project with TypeORM and Ts.ED framework.", 5 | "scripts": { 6 | "clean": "rimraf '{src,test}/**/*.{js,js.map}'", 7 | "build": "yarn tsc", 8 | "test": "yarn clean && yarn test:lint && yarn test:coverage", 9 | "test:unit": "cross-env NODE_ENV=test mocha", 10 | "test:coverage": "cross-env NODE_ENV=test nyc mocha", 11 | "test:lint": "tslint --project tsconfig.json", 12 | "test:lint:fix": "tslint --project tsconfig.json --fix", 13 | "travis:deploy-once": "travis-deploy-once", 14 | "travis:coveralls": "nyc report --reporter=text-lcov | coveralls", 15 | "tsc": "tsc --project tsconfig.json", 16 | "tsc:w": "tsc --project tsconfig.json -w", 17 | "start:dev": "nodemon --watch \"src/**/*.ts\" --ignore \"node_modules/**/*\" --exec ts-node src/index.ts", 18 | "start": "ts-node src/index.ts", 19 | "start:prod": "cross-env NODE_ENV=production node dist/index.js", 20 | "docker:build": "yarn build && docker-compose build", 21 | "deploy": "exit 0" 22 | }, 23 | "author": "", 24 | "license": "MIT", 25 | "dependencies": { 26 | "@tsed/common": "5.67.2", 27 | "@tsed/core": "5.67.2", 28 | "@tsed/di": "5.67.2", 29 | "@tsed/platform-express": "5.67.2", 30 | "@tsed/exceptions": "5.67.2", 31 | "@tsed/swagger": "5.65.3", 32 | "@tsed/testing": "5.67.2", 33 | "@tsed/typeorm": "5.65.3", 34 | "@tsed/passport": "5.65.3", 35 | "@types/swagger-schema-official": "2.0.21", 36 | "body-parser": "1.19.0", 37 | "compression": "1.7.4", 38 | "cookie-parser": "1.4.5", 39 | "cors": "2.8.5", 40 | "express": "4.17.1", 41 | "express-session": "1.17.1", 42 | "method-override": "^3.0.0", 43 | "pg": "^7.4.3", 44 | "sqlite3": "4.2.0", 45 | "serve-static": "^1.13.1", 46 | "typeorm": "0.2.26", 47 | "cross-env": "7.0.2", 48 | "passport": "0.4.1", 49 | "passport-local": "1.0.0", 50 | "passport-http": "0.3.0" 51 | }, 52 | "devDependencies": { 53 | "@types/chai": "4.2.12", 54 | "@types/chai-as-promised": "7.1.3", 55 | "@types/cors": "2.8.6", 56 | "@types/express": "4.17.7", 57 | "@types/http-proxy": "^1.16.2", 58 | "@types/mocha": "8.0.1", 59 | "@types/node": "14.0.27", 60 | "@types/request-promise": "^4.1.42", 61 | "@types/sinon": "9.0.4", 62 | "@types/sinon-chai": "3.2.4", 63 | "@types/supertest": "2.0.10", 64 | "@types/passport": "1.0.4", 65 | "@types/passport-local": "^1.0.33", 66 | "@types/passport-http": "0.3.8", 67 | "chai": "4.2.0", 68 | "chai-as-promised": "^7.1.1", 69 | "concurrently": "5.3.0", 70 | "mocha": "8.1.1", 71 | "nodemon": "1.19.1", 72 | "nyc": "15.1.0", 73 | "rimraf": "^2.6.2", 74 | "sinon": "9.0.2", 75 | "sinon-chai": "3.5.0", 76 | "supertest": "4.0.2", 77 | "ts-node": "9.0.0", 78 | "tslint": "6.1.3", 79 | "typescript": "3.9.4" 80 | } 81 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Ts.ED - TypeORM 2 | 3 | Here an example project with TypeORM and Ts.ED framework. 4 | 5 | See [Ts.ED](https://tsed.io) project for more information. 6 | 7 | ## Features 8 | 9 | - Docker and Docker compose 10 | - Travis CI 11 | - TypeORM 12 | 13 | [](https://docker.com) 14 | [](https://travis-ci.org) 15 | [](https://typeorm.io/) 16 | 17 | 18 | ## Checkout 19 | 20 | This repository provide getting started project example for each Ts.ED version since `v5.18.1`. 21 | 22 | ```bash 23 | git checkout -b https://github.com/tsedio/tsed-example-typeorm/tree/v5.18.1 24 | ``` 25 | 26 | To checkout another version just replace `v5.18.1` by the desired version. 27 | 28 | ## Install 29 | 30 | > **Important!** Ts.ED requires Node >= 8, Express >= 4 and TypeScript >= 3. 31 | 32 | ```batch 33 | yarn install 34 | ``` 35 | 36 | # Start with Docker 37 | 38 | Run: 39 | ``` 40 | yarn docker:build 41 | docker-compose up 42 | ``` 43 | 44 | ## Access to postgres: 45 | 46 | * `localhost:5432` 47 | * **Username:** postgres (as a default) 48 | * **Password:** changeme (as a default) 49 | 50 | ## Access to PgAdmin: 51 | * **URL:** `http://localhost:5050` 52 | * **Username:** pgadmin4@pgadmin.org (as a default) 53 | * **Password:** admin (as a default) 54 | 55 | ## Add a new server in PgAdmin: 56 | * **Host name/address** `postgres` 57 | * **Port** `5432` 58 | * **Username** as `POSTGRES_USER`, by default: `postgres` 59 | * **Password** as `POSTGRES_PASSWORD`, by default `changeme` 60 | 61 | ## Contributing 62 | 63 | You can make a PR directly on https://github.com/tsedio/ts-express-decorators repository. 64 | 65 | ## Backers 66 | 67 | Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/tsed#backer)] 68 | 69 | 70 | 71 | 72 | ## Sponsors 73 | 74 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/tsed#sponsor)] 75 | 76 | ## License 77 | 78 | The MIT License (MIT) 79 | 80 | Copyright (c) 2016 - 2020 Romain Lenzotti 81 | 82 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 83 | 84 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 85 | 86 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 87 | 88 | [travis]: https://travis-ci.org/ 89 | -------------------------------------------------------------------------------- /scripts/mocha/register.js: -------------------------------------------------------------------------------- 1 | const Chai = require("chai"); 2 | const ChaiAsPromised = require("chai-as-promised"); 3 | const SinonChai = require("sinon-chai"); 4 | 5 | Chai.should(); 6 | Chai.use(SinonChai); 7 | Chai.use(ChaiAsPromised); 8 | 9 | process.on("unhandledRejection", (reason, p) => { 10 | console.log("Unhandled Rejection at: Promise", p, "reason:", reason); 11 | // application specific logging, throwing an error, or other logic here 12 | }); 13 | -------------------------------------------------------------------------------- /src/Server.ts: -------------------------------------------------------------------------------- 1 | import {GlobalAcceptMimesMiddleware, PlatformApplication} from "@tsed/common"; 2 | import {Configuration, Inject} from "@tsed/di"; 3 | import "@tsed/passport"; 4 | import "@tsed/platform-express"; 5 | import "@tsed/swagger"; 6 | import "@tsed/typeorm"; 7 | import * as bodyParser from "body-parser"; 8 | import * as compress from "compression"; 9 | import * as cookieParser from "cookie-parser"; 10 | import * as cors from "cors"; 11 | import * as session from "express-session"; 12 | import * as methodOverride from "method-override"; 13 | import {User} from "./entities/User"; 14 | 15 | export const rootDir = __dirname; 16 | 17 | @Configuration({ 18 | rootDir, 19 | httpPort: process.env.PORT || 8083, 20 | httpsPort: false, 21 | acceptMimes: ["application/json"], 22 | mount: { 23 | "/v1": [ 24 | `${rootDir}/controllers/**/**Ctrl.{ts,js}` 25 | ] 26 | }, 27 | componentsScan: [ 28 | `${rootDir}/services/*{.ts,.js}`, 29 | `${rootDir}/repositories/*{.ts,.js}`, 30 | `${rootDir}/protocols/*{.ts,.js}` 31 | ], 32 | passport: { 33 | userInfoModel: User 34 | }, 35 | typeorm: [ 36 | { 37 | name: "default", 38 | type: "postgres", 39 | host: process.env.POSTGRES_HOST || "localhost", 40 | port: 5432, 41 | username: process.env.POSTGRES_USER || "postgres", 42 | password: process.env.POSTGRES_PASSWORD || "changeme", 43 | database: process.env.POSTGRES_DB || "postgres", 44 | logging: false, 45 | synchronize: true, 46 | entities: [ 47 | `${rootDir}/entities/*{.ts,.js}` 48 | ], 49 | migrations: [ 50 | `${rootDir}/migrations/*{.ts,.js}` 51 | ], 52 | subscribers: [ 53 | `${rootDir}/subscriber/*{.ts,.js}` 54 | ] 55 | } 56 | ], 57 | swagger: { 58 | path: "/api-docs", 59 | spec: { 60 | securityDefinitions: { 61 | "auth:basic": { 62 | type: "basic" 63 | } 64 | } 65 | } 66 | } 67 | }) 68 | export class Server { 69 | @Inject() 70 | app: PlatformApplication; 71 | 72 | $beforeRoutesInit(): void | Promise { 73 | this.app 74 | .use(GlobalAcceptMimesMiddleware) 75 | .use(cors()) 76 | .use(cookieParser()) 77 | .use(compress({})) 78 | .use(methodOverride()) 79 | .use(bodyParser.json()) 80 | .use(bodyParser.urlencoded({ 81 | extended: true 82 | })) 83 | .use(session({ 84 | secret: "mysecretkey", 85 | resave: true, 86 | saveUninitialized: true, 87 | // maxAge: 36000, 88 | cookie: { 89 | path: "/", 90 | httpOnly: true, 91 | secure: false, 92 | maxAge: null 93 | } 94 | })); 95 | 96 | return null; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/controllers/passport/PassportCtrl.integration.spec.ts: -------------------------------------------------------------------------------- 1 | import {PlatformTest} from "@tsed/common"; 2 | import {expect} from "chai"; 3 | import * as SuperTest from "supertest"; 4 | import {bootstrapServer} from "../../../test/helpers/bootstrapServer"; 5 | import {User} from "../../entities/User"; 6 | import {UserRepository} from "../../repositories/UserRepository"; 7 | import {PassportCtrl} from "./PassportCtrl"; 8 | 9 | describe("PassportCtrl", () => { 10 | let request: SuperTest.SuperTest; 11 | 12 | before(bootstrapServer({ 13 | mount: { 14 | "/rest": [PassportCtrl] 15 | } 16 | })); 17 | 18 | before(() => { 19 | request = SuperTest(PlatformTest.callback()); 20 | 21 | // create initial user 22 | const usersRepository = PlatformTest.get(UserRepository); 23 | const user = new User(); 24 | user.id = 1; 25 | user.password = "password"; 26 | user.email = "admin@tsed.io"; 27 | user.firstName = "John"; 28 | user.lastName = "Doe"; 29 | user.age = 18; 30 | 31 | console.log(usersRepository) 32 | 33 | return usersRepository.save(user); 34 | }); 35 | after(PlatformTest.reset); 36 | 37 | describe("POST /rest/auth/login", () => { 38 | it("should return a user", async () => { 39 | const response = await request.post("/rest/auth/login").send({ 40 | email: "admin@tsed.io", 41 | password: "password" 42 | }) 43 | .expect(200); 44 | 45 | expect(response.body).to.deep.eq({ 46 | "age": 18, 47 | "email": "admin@tsed.io", 48 | "firstName": "John", 49 | "id": 1, 50 | "lastName": "Doe" 51 | }); 52 | }); 53 | 54 | it("should throw an error if user doesn\'t exists", async () => { 55 | const response = await request.post("/rest/auth/login").send({ 56 | email: "admin2@tsed.io", 57 | password: "password" 58 | }) 59 | .expect(401); 60 | 61 | expect(response.text).to.deep.eq("Unauthorized"); 62 | }); 63 | 64 | it("should throw an error if the password is missing", async () => { 65 | const response = await request.post("/rest/auth/login").send({ 66 | email: "admin2@tsed.io" 67 | }) 68 | .expect(400); 69 | 70 | expect(response.text).to.deep.eq("Bad Request"); 71 | }); 72 | 73 | it("should throw an error if the email is missing", async () => { 74 | const response = await request.post("/rest/auth/login").send({}) 75 | .expect(400); 76 | 77 | expect(response.text).to.deep.eq("Bad Request"); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/controllers/passport/PassportCtrl.ts: -------------------------------------------------------------------------------- 1 | import {BodyParams, Controller, Get, Post, Req, Status, Returns} from "@tsed/common"; 2 | import {Authenticate, Authorize} from "@tsed/passport"; 3 | import {User} from "../../entities/User"; 4 | import {Credentials} from "../../models/Credentials"; 5 | import {UserCreation} from "../../models/UserCreation"; 6 | 7 | @Controller("/auth") 8 | export class PassportCtrl { 9 | constructor() { 10 | } 11 | 12 | @Post("/login") 13 | @Authenticate("login", {failWithError: false}) 14 | @Returns(200, User) 15 | @Returns(400, {description: "Validation error"}) 16 | login(@Req() req: Req, @BodyParams() credentials: Credentials) { 17 | // FACADE 18 | return req.user; 19 | } 20 | 21 | @Post("/signup") 22 | @Authenticate("signup") 23 | @Returns(201, User) 24 | signup(@Req() req: Req, @BodyParams() user: UserCreation) { 25 | // FACADE 26 | return req.user; 27 | } 28 | 29 | @Get("/userinfo") 30 | @Authenticate("basic", {security: ["auth:basic"]}) 31 | @Returns(200, User) 32 | getUserInfo(@Req() req: Req): any { 33 | // FACADE 34 | return req.user; 35 | } 36 | 37 | 38 | @Get("/logout") 39 | logout(@Req() req: Req) { 40 | req.logout(); 41 | } 42 | 43 | @Get("/connect/:protocol") 44 | @Authorize(":protocol") 45 | @Returns(200, User) 46 | connectProtocol(@Req() req: Req): any { 47 | // FACADE 48 | return req.user; 49 | } 50 | 51 | 52 | @Get("/connect/:protocol/callback") 53 | @Authorize(":protocol") 54 | @Returns(200, User) 55 | connectProtocolCallback(@Req() req: Req): any { 56 | // FACADE 57 | return req.user; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/controllers/users/UsersCtrl.ts: -------------------------------------------------------------------------------- 1 | import {Returns, ReturnsArray, BodyParams, Controller, Get, PathParams, Post} from "@tsed/common"; 2 | import {User} from "../../entities/User"; 3 | import {UserCreation} from "../../models/UserCreation"; 4 | import {UserRepository} from "../../repositories/UserRepository"; 5 | 6 | @Controller("/users") 7 | export class UsersCtrl { 8 | constructor(private userRepository: UserRepository) { 9 | } 10 | 11 | @Post("/") 12 | @Returns(200, User) 13 | create(@BodyParams() user: UserCreation): Promise { 14 | return this.userRepository.save(user); 15 | } 16 | 17 | @Get("/:id") 18 | @Returns(200, User) 19 | async get(@PathParams('id') id: string): Promise { 20 | return this.userRepository.findByID(id); 21 | } 22 | 23 | @Get("/") 24 | @ReturnsArray(200, User) 25 | async getList(): Promise { 26 | return this.userRepository.find(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/entities/User.ts: -------------------------------------------------------------------------------- 1 | import {Ignore, Description} from "@tsed/common"; 2 | import {Entity, PrimaryGeneratedColumn} from "typeorm"; 3 | import {UserCreation} from "../models/UserCreation"; 4 | 5 | @Entity() 6 | export class User extends UserCreation { 7 | @Description("Database assigned id") 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @Ignore() 12 | password: string; 13 | 14 | verifyPassword(password: string) { 15 | return this.password === password; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {$log} from "@tsed/common"; 2 | import {PlatformExpress} from "@tsed/platform-express"; 3 | import {Server} from "./Server"; 4 | 5 | async function bootstrap() { 6 | try { 7 | $log.debug("Start server..."); 8 | const platform = await PlatformExpress.bootstrap(Server, {}); 9 | 10 | await platform.listen(); 11 | $log.debug("Server initialized"); 12 | } catch (er) { 13 | $log.error(er); 14 | } 15 | } 16 | 17 | bootstrap(); 18 | -------------------------------------------------------------------------------- /src/models/Credentials.ts: -------------------------------------------------------------------------------- 1 | import {Format, Required, Description, Example} from "@tsed/common"; 2 | import {Column} from "typeorm"; 3 | 4 | export class Credentials { 5 | @Description("User password") 6 | @Example("/5gftuD/") 7 | @Column() 8 | @Required() 9 | password: string; 10 | 11 | @Description("User email") 12 | @Example("user@domain.com") 13 | @Format("email") 14 | @Column({unique: true}) 15 | email: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/models/UserCreation.ts: -------------------------------------------------------------------------------- 1 | import {Description, Example, Minimum, Required} from "@tsed/common"; 2 | import {Column} from "typeorm"; 3 | import {Credentials} from "./Credentials"; 4 | 5 | export class UserCreation extends Credentials { 6 | @Description("User first name") 7 | @Column() 8 | @Required() 9 | firstName: string; 10 | 11 | @Description("User last name") 12 | @Column() 13 | @Required() 14 | lastName: string; 15 | 16 | @Description("User age") 17 | @Column() 18 | @Minimum(18) 19 | @Example(18) 20 | age: number; 21 | } 22 | -------------------------------------------------------------------------------- /src/protocols/LoginLocalProtocol.spec.ts: -------------------------------------------------------------------------------- 1 | import {PlatformTest} from "@tsed/common"; 2 | import * as Sinon from "sinon"; 3 | import {User} from "../entities/User"; 4 | import {UserRepository} from "../repositories/UserRepository"; 5 | import {LoginLocalProtocol} from "./LoginLocalProtocol"; 6 | 7 | describe("LoginLocalProtocol", () => { 8 | beforeEach(() => PlatformTest.create()); 9 | afterEach(() => PlatformTest.reset()); 10 | 11 | describe(".$onVerify()", () => { 12 | it("should return a user", async () => { 13 | // GIVEN 14 | const request = {}; 15 | const email = "email@domain.fr"; 16 | const password = "password"; 17 | const user = new User(); 18 | user.email = email; 19 | user.password = password; 20 | 21 | const userRepository = { 22 | findOne: Sinon.stub().resolves(user) 23 | }; 24 | 25 | const protocol: LoginLocalProtocol = await PlatformTest.invoke(LoginLocalProtocol, [ 26 | { 27 | token: UserRepository, 28 | use: userRepository 29 | } 30 | ]); 31 | 32 | // WHEN 33 | const result = await protocol.$onVerify(request as any, {email, password}); 34 | 35 | // THEN 36 | userRepository.findOne.should.be.calledWithExactly({email: "email@domain.fr"}); 37 | result.should.deep.equal(user); 38 | }); 39 | it("should return a user", async () => { 40 | // GIVEN 41 | const request = {}; 42 | const email = "email@domain.fr"; 43 | const password = "password"; 44 | const user = new User(); 45 | user.email = email; 46 | user.password = `${password}2`; 47 | 48 | const userRepository = { 49 | findOne: Sinon.stub().resolves(user) 50 | }; 51 | 52 | const protocol: LoginLocalProtocol = await PlatformTest.invoke(LoginLocalProtocol, [ 53 | { 54 | token: UserRepository, 55 | use: userRepository 56 | } 57 | ]); 58 | 59 | // WHEN 60 | const result = await protocol.$onVerify(request as any, {email, password}); 61 | 62 | // THEN 63 | userRepository.findOne.should.be.calledWithExactly({email: "email@domain.fr"}); 64 | result.should.deep.equal(false); 65 | }); 66 | it("should return a false when user isn't found", async () => { 67 | // GIVEN 68 | const request = {}; 69 | const email = "email@domain.fr"; 70 | const password = "password"; 71 | 72 | const userRepository = { 73 | findOne: Sinon.stub().resolves(undefined) 74 | }; 75 | 76 | const protocol: LoginLocalProtocol = await PlatformTest.invoke(LoginLocalProtocol, [ 77 | { 78 | token: UserRepository, 79 | use: userRepository 80 | } 81 | ]); 82 | 83 | // WHEN 84 | const result = await protocol.$onVerify(request as any, {email, password}); 85 | 86 | // THEN 87 | userRepository.findOne.should.be.calledWithExactly({email: "email@domain.fr"}); 88 | result.should.deep.equal(false); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /src/protocols/LoginLocalProtocol.ts: -------------------------------------------------------------------------------- 1 | import {BodyParams, Req} from "@tsed/common"; 2 | import {OnInstall, OnVerify, Protocol} from "@tsed/passport"; 3 | import {IStrategyOptions, Strategy} from "passport-local"; 4 | import {Credentials} from "../models/Credentials"; 5 | import {UserRepository} from "../repositories/UserRepository"; 6 | 7 | @Protocol({ 8 | name: "login", 9 | useStrategy: Strategy, 10 | settings: { 11 | usernameField: "email", 12 | passwordField: "password" 13 | } 14 | }) 15 | export class LoginLocalProtocol implements OnVerify, OnInstall { 16 | constructor(private userRepository: UserRepository) { 17 | } 18 | 19 | async $onVerify(@Req() request: Req, @BodyParams() credentials: Credentials) { 20 | const {email, password} = credentials; 21 | 22 | const user = await this.userRepository.findOne({email}); 23 | 24 | if (!user) { 25 | return false; 26 | // OR throw new NotAuthorized("Wrong credentials") 27 | } 28 | 29 | if (!user.verifyPassword(password)) { 30 | return false; 31 | // OR throw new NotAuthorized("Wrong credentials") 32 | } 33 | 34 | return user; 35 | } 36 | 37 | $onInstall(strategy: Strategy): void { 38 | // intercept the strategy instance to adding extra configuration 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/protocols/SignupLocalProtocol.ts: -------------------------------------------------------------------------------- 1 | import {BodyParams, Req} from "@tsed/common"; 2 | import {OnInstall, OnVerify, Protocol} from "@tsed/passport"; 3 | import {Strategy} from "passport-local"; 4 | import {Forbidden} from "@tsed/exceptions"; 5 | import {UserCreation} from "../models/UserCreation"; 6 | import {UserRepository} from "../repositories/UserRepository"; 7 | 8 | @Protocol({ 9 | name: "signup", 10 | useStrategy: Strategy, 11 | settings: { 12 | usernameField: "email", 13 | passwordField: "password" 14 | } 15 | }) 16 | export class SignupLocalProtocol implements OnVerify, OnInstall { 17 | constructor(private userRepository: UserRepository) { 18 | } 19 | 20 | async $onVerify(@Req() request: Req, @BodyParams() user: UserCreation) { 21 | const {email} = user; 22 | const found = await this.userRepository.findOne({email}); 23 | 24 | if (found) { 25 | throw new Forbidden("Email is already registered"); 26 | } 27 | 28 | return this.userRepository.create(user); 29 | } 30 | 31 | $onInstall(strategy: Strategy): void { 32 | // intercept the strategy instance to adding extra configuration 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/repositories/UserRepository.ts: -------------------------------------------------------------------------------- 1 | import {EntityRepository, Repository} from "typeorm"; 2 | import {User} from "../entities/User"; 3 | 4 | @EntityRepository(User) 5 | export class UserRepository extends Repository { 6 | findByID(id: string): Promise { 7 | return this.findOne(id); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/helpers/bootstrapServer.ts: -------------------------------------------------------------------------------- 1 | import {PlatformTest} from "@tsed/common"; 2 | import {rootDir, Server} from "../../src/Server"; 3 | 4 | export function bootstrapServer(options: any) { 5 | return PlatformTest.bootstrap(Server, { 6 | ...options, 7 | 8 | typeorm: [{ 9 | name: "default", 10 | type: "sqlite", 11 | database: ":memory:", 12 | dropSchema: true, 13 | synchronize: true, 14 | logging: false, 15 | entities: [ 16 | `${rootDir}/entities/*{.ts,.js}` 17 | ], 18 | migrations: [ 19 | `${rootDir}/migrations/*{.ts,.js}` 20 | ], 21 | subscribers: [ 22 | `${rootDir}/subscriber/*{.ts,.js}` 23 | ] 24 | }] 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "./dist", 5 | "target": "es2016", 6 | "lib": [ 7 | "es2016", 8 | "dom" 9 | ], 10 | "typeRoots": [ 11 | "./node_modules/@types" 12 | ], 13 | "module": "commonjs", 14 | "moduleResolution": "node", 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true, 17 | "sourceMap": true, 18 | "declaration": false, 19 | "allowSyntheticDefaultImports": true 20 | }, 21 | "include": [ 22 | "./src/**/*.ts" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | "./public", 27 | "dist", 28 | "test" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "target": "es2015", 5 | "lib": [ 6 | "es2015", 7 | "dom" 8 | ], 9 | "typeRoots": [ 10 | "./node_modules/@types" 11 | ], 12 | "module": "commonjs", 13 | "moduleResolution": "node", 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "sourceMap": true, 17 | "declaration": false 18 | }, 19 | "include": [ 20 | "./src/**/*.ts", 21 | "./test/**/*.ts" 22 | ], 23 | "exclude": [ 24 | "node_modules", 25 | "./public", 26 | "dist" 27 | ] 28 | } 29 | --------------------------------------------------------------------------------