├── .gitignore ├── .prettierrc ├── README.md ├── nodemon.json ├── package-lock.json ├── package.json ├── src ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── auth │ ├── AppAuthGuard.ts │ ├── SessionGuard.ts │ ├── auth.module.ts │ ├── auth.service.ts │ ├── cookie-serializer.ts │ └── http.strategy.ts ├── common │ ├── error │ │ ├── AppError.ts │ │ ├── AppErrorTypeEnum.ts │ │ └── IErrorMessage.ts │ └── filters │ │ └── DispatchError.ts ├── main.hmr.ts ├── main.ts ├── project │ ├── IProjectService.ts │ ├── models │ │ └── CreateProjectDto.ts │ ├── project.controller.ts │ ├── project.entity.ts │ ├── project.module.ts │ └── project.service.ts └── user │ ├── IUserService.ts │ ├── models │ └── CreateUserDto.ts │ ├── user.controller.ts │ ├── user.decorator.ts │ ├── user.entity.ts │ ├── user.module.ts │ └── user.service.ts ├── tsconfig.json ├── tslint.json ├── tutorial.sqlite └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # IDE 5 | /.idea 6 | /.awcache 7 | /.vscode 8 | 9 | # misc 10 | npm-debug.log 11 | tutorial.sqlite 12 | 13 | # example 14 | /quick-start 15 | 16 | # tests 17 | /test 18 | /coverage 19 | /.nyc_output 20 | 21 | # dist 22 | /dist 23 | 24 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nestjs-session-tutorial-finished 2 | 3 | ## Description 4 | 5 | Source code for NestJS Basic Auth and Sessions Tutorial 6 | http://blog.exceptionfound.com/index.php/2018/06/07/nestjs-basic-auth-and-sessions/ 7 | 8 | ## Installation 9 | 10 | ```bash 11 | $ npm install 12 | ``` 13 | 14 | ## Running the app 15 | 16 | ```bash 17 | # development 18 | $ npm run start 19 | 20 | # watch mode 21 | $ npm run start:dev 22 | 23 | # production mode 24 | npm run start:prod 25 | ``` 26 | 27 | ## Test 28 | 29 | ```bash 30 | # unit tests 31 | $ npm run test 32 | 33 | # e2e tests 34 | $ npm run test:e2e 35 | 36 | # test coverage 37 | $ npm run test:cov 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src" 4 | ], 5 | "ext": "ts", 6 | "ignore": [ 7 | "src/**/*.spec.ts" 8 | ], 9 | "exec": "ts-node --inspect=5858 -r tsconfig-paths/register src/main.ts" 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-session-tutorial-finished", 3 | "version": "1.0.0", 4 | "description": "finished-source-code-for-nest-js-sessions-tutorial", 5 | "author": "anton-sukhovatkin", 6 | "license": "MIT", 7 | "scripts": { 8 | "format": "prettier --write \"**/*.ts\"", 9 | "start": "ts-node -r tsconfig-paths/register src/main.ts", 10 | "start:dev": "nodemon", 11 | "prestart:prod": "rm -rf dist && tsc", 12 | "start:prod": "node dist/main.js", 13 | "start:hmr": "node dist/server", 14 | "test": "jest", 15 | "test:cov": "jest --coverage", 16 | "test:e2e": "jest --config ./test/jest-e2e.json", 17 | "webpack": "webpack --config webpack.config.js" 18 | }, 19 | "dependencies": { 20 | "@nestjs/common": "^5.0.0", 21 | "@nestjs/core": "^5.0.0", 22 | "@nestjs/microservices": "^5.0.0", 23 | "@nestjs/passport": "^1.0.11", 24 | "@nestjs/swagger": "^2.0.2", 25 | "@nestjs/testing": "^5.0.0", 26 | "@nestjs/typeorm": "^5.0.0", 27 | "@nestjs/websockets": "^5.0.0", 28 | "express-session": "^1.15.6", 29 | "fastify-formbody": "^2.0.0", 30 | "passport": "^0.4.0", 31 | "passport-http-bearer": "^1.0.1", 32 | "reflect-metadata": "^0.1.12", 33 | "rxjs": "^6.0.0", 34 | "sqlite3": "^4.0.0", 35 | "typeorm": "^0.2.7", 36 | "typescript": "^2.6.2" 37 | }, 38 | "devDependencies": { 39 | "@types/express": "^4.0.39", 40 | "@types/jest": "^21.1.8", 41 | "@types/node": "^9.3.0", 42 | "@types/supertest": "^2.0.4", 43 | "jest": "^21.2.1", 44 | "nodemon": "^1.14.1", 45 | "prettier": "^1.11.1", 46 | "supertest": "^3.0.0", 47 | "ts-jest": "^21.2.4", 48 | "ts-loader": "^4.1.0", 49 | "ts-node": "^4.1.0", 50 | "tsconfig-paths": "^3.1.1", 51 | "tslint": "5.3.2", 52 | "webpack": "^4.2.0", 53 | "webpack-cli": "^2.0.13", 54 | "webpack-node-externals": "^1.6.0" 55 | }, 56 | "jest": { 57 | "moduleFileExtensions": [ 58 | "js", 59 | "json", 60 | "ts" 61 | ], 62 | "rootDir": "src", 63 | "testRegex": ".spec.ts$", 64 | "transform": { 65 | "^.+\\.(t|j)s$": "ts-jest" 66 | }, 67 | "coverageDirectory": "../coverage" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | 6 | describe('AppController', () => { 7 | let app: TestingModule; 8 | 9 | beforeAll(async () => { 10 | app = await Test.createTestingModule({ 11 | controllers: [AppController], 12 | providers: [AppService], 13 | }).compile(); 14 | }); 15 | 16 | describe('root', () => { 17 | it('should return "Hello World!"', () => { 18 | const appController = app.get(AppController); 19 | expect(appController.root()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Get, Controller } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | root(): string { 10 | return this.appService.root(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import {TypeOrmModule} from '@nestjs/typeorm'; 5 | import { UserModule } from './user/user.module'; 6 | import { ProjectModule } from './project/project.module'; 7 | import {UserEntity} from './user/user.entity'; 8 | import {ProjectEntity} from './project/project.entity'; 9 | import { AuthModule } from './auth/auth.module'; 10 | 11 | @Module({ 12 | imports: [ 13 | TypeOrmModule.forRoot({ 14 | type: 'sqlite', 15 | database: `${process.cwd()}/tutorial.sqlite`, 16 | entities: [UserEntity, ProjectEntity], 17 | synchronize: true, 18 | // logging: 'all' 19 | }), 20 | UserModule, 21 | ProjectModule, 22 | AuthModule, 23 | ], 24 | controllers: [AppController], 25 | providers: [ AppService ], 26 | }) 27 | export class AppModule {} 28 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | root(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/AppAuthGuard.ts: -------------------------------------------------------------------------------- 1 | import {CanActivate, ExecutionContext, UnauthorizedException} from '@nestjs/common'; 2 | import * as passport from 'passport'; 3 | 4 | export class AppAuthGuard implements CanActivate { 5 | async canActivate(context: ExecutionContext): Promise { 6 | const options = { ...defaultOptions }; 7 | const httpContext = context.switchToHttp(); 8 | const [request, response] = [ 9 | httpContext.getRequest(), 10 | httpContext.getResponse() 11 | ]; 12 | const passportFn = createPassportContext(request, response); 13 | 14 | const user = await passportFn( 15 | 'bearer', 16 | options 17 | ); 18 | if (user) { 19 | request.login(user, (res) => {}); 20 | } 21 | 22 | return true; 23 | } 24 | 25 | } 26 | 27 | const createPassportContext = (request, response) => (type, options) => 28 | new Promise((resolve, reject) => 29 | passport.authenticate(type, options, (err, user, info) => { 30 | try { 31 | return resolve(options.callback(err, user, info)); 32 | } catch (err) { 33 | reject(err); 34 | } 35 | })(request, response, resolve) 36 | ); 37 | 38 | const defaultOptions = { 39 | session: true, 40 | property: 'user', 41 | callback: (err, user, info) => { 42 | if (err || !user) { 43 | throw err || new UnauthorizedException(); 44 | } 45 | return user; 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/auth/SessionGuard.ts: -------------------------------------------------------------------------------- 1 | import {CanActivate, ExecutionContext} from '@nestjs/common'; 2 | import {AppError} from '../common/error/AppError'; 3 | import {AppErrorTypeEnum} from '../common/error/AppErrorTypeEnum'; 4 | 5 | export class SessionGuard implements CanActivate { 6 | canActivate(context: ExecutionContext): boolean | Promise { 7 | const httpContext = context.switchToHttp(); 8 | const request = httpContext.getRequest(); 9 | 10 | try { 11 | if (request.session.passport.user) 12 | return true; 13 | } catch (e) { 14 | throw new AppError(AppErrorTypeEnum.NOT_IN_SESSION); 15 | } 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import {HttpStrategy} from './http.strategy'; 4 | import {AppAuthGuard} from './AppAuthGuard'; 5 | import {CookieSerializer} from './cookie-serializer'; 6 | 7 | @Module({ 8 | providers: [AuthService, HttpStrategy, AppAuthGuard, CookieSerializer] 9 | }) 10 | export class AuthModule {} 11 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import {UserEntity} from '../user/user.entity'; 3 | 4 | @Injectable() 5 | export class AuthService { 6 | async validateUser(user: {username: string, password: string}): Promise { 7 | return await UserEntity.authenticateUser(user); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/auth/cookie-serializer.ts: -------------------------------------------------------------------------------- 1 | import {PassportSerializer} from '@nestjs/passport/dist/passport.serializer'; 2 | import {Injectable} from '@nestjs/common'; 3 | 4 | @Injectable() 5 | export class CookieSerializer extends PassportSerializer { 6 | serializeUser(user: any, done: Function): any { 7 | done(null, user); 8 | } 9 | 10 | deserializeUser(payload: any, done: Function): any { 11 | done(null, payload); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/auth/http.strategy.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, UnauthorizedException} from '@nestjs/common'; 2 | import {PassportStrategy} from '@nestjs/passport'; 3 | import { Strategy } from 'passport-http-bearer'; 4 | import {AuthService} from './auth.service'; 5 | 6 | @Injectable() 7 | export class HttpStrategy extends PassportStrategy(Strategy) { 8 | 9 | constructor(private readonly authService: AuthService) { 10 | super(); 11 | } 12 | 13 | async validate(token: any, done: Function) { 14 | let authObject: {username: string, password: string} = null; 15 | const decoded = Buffer.from(token, 'base64').toString(); 16 | try { 17 | authObject = JSON.parse(decoded); 18 | const user = await this.authService.validateUser(authObject); 19 | if (!user) { 20 | return done(new UnauthorizedException(), false); 21 | } 22 | done(null, user); 23 | } catch (e) { 24 | return done(new UnauthorizedException(), false); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/common/error/AppError.ts: -------------------------------------------------------------------------------- 1 | import {AppErrorTypeEnum} from './AppErrorTypeEnum'; 2 | import {IErrorMessage} from './IErrorMessage'; 3 | import {HttpStatus} from '@nestjs/common'; 4 | 5 | export class AppError extends Error { 6 | 7 | public errorCode: AppErrorTypeEnum; 8 | public httpStatus: number; 9 | public errorMessage: string; 10 | public userMessage: string; 11 | 12 | constructor(errorCode: AppErrorTypeEnum) { 13 | super(); 14 | const errorMessageConfig: IErrorMessage = this.getError(errorCode); 15 | if (!errorMessageConfig) throw new Error('Unable to find message code error.'); 16 | 17 | Error.captureStackTrace(this, this.constructor); 18 | this.name = this.constructor.name; 19 | this.httpStatus = errorMessageConfig.httpStatus; 20 | this.errorCode = errorCode; 21 | this.errorMessage = errorMessageConfig.errorMessage; 22 | this.userMessage = errorMessageConfig.userMessage; 23 | } 24 | 25 | private getError(errorCode: AppErrorTypeEnum): IErrorMessage { 26 | 27 | let res: IErrorMessage; 28 | 29 | switch (errorCode) { 30 | case AppErrorTypeEnum.USER_NOT_FOUND: 31 | res = { 32 | type: AppErrorTypeEnum.USER_NOT_FOUND, 33 | httpStatus: HttpStatus.NOT_FOUND, 34 | errorMessage: 'User not found', 35 | userMessage: 'Unable to find the user with the provided information.' 36 | }; 37 | break; 38 | case AppErrorTypeEnum.USER_EXISTS: 39 | res = { 40 | type: AppErrorTypeEnum.USER_EXISTS, 41 | httpStatus: HttpStatus.UNPROCESSABLE_ENTITY, 42 | errorMessage: 'User exisists', 43 | userMessage: 'Username exists' 44 | }; 45 | break; 46 | case AppErrorTypeEnum.NOT_IN_SESSION: 47 | res = { 48 | type: AppErrorTypeEnum.NOT_IN_SESSION, 49 | httpStatus: HttpStatus.UNAUTHORIZED, 50 | errorMessage: 'No Session', 51 | userMessage: 'Session Expired' 52 | }; 53 | break; 54 | case AppErrorTypeEnum.NO_USERS_IN_DB: 55 | res = { 56 | type: AppErrorTypeEnum.NO_USERS_IN_DB, 57 | httpStatus: HttpStatus.NOT_FOUND, 58 | errorMessage: 'No Users exits in the database', 59 | userMessage: 'No Users. Create some.' 60 | }; 61 | break; 62 | case AppErrorTypeEnum.WRONG_PASSWORD: 63 | res = { 64 | type: AppErrorTypeEnum.WRONG_PASSWORD, 65 | httpStatus: HttpStatus.UNAUTHORIZED, 66 | errorMessage: 'Wrong password', 67 | userMessage: 'Password does not match' 68 | }; 69 | break; 70 | } 71 | return res; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/common/error/AppErrorTypeEnum.ts: -------------------------------------------------------------------------------- 1 | export const enum AppErrorTypeEnum { 2 | USER_NOT_FOUND, 3 | USER_EXISTS, 4 | NOT_IN_SESSION, 5 | NO_USERS_IN_DB, 6 | WRONG_PASSWORD 7 | } 8 | -------------------------------------------------------------------------------- /src/common/error/IErrorMessage.ts: -------------------------------------------------------------------------------- 1 | import {AppErrorTypeEnum} from './AppErrorTypeEnum'; 2 | import {HttpStatus} from '@nestjs/common'; 3 | 4 | export interface IErrorMessage { 5 | type: AppErrorTypeEnum; 6 | httpStatus: HttpStatus; 7 | errorMessage: string; 8 | userMessage: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/filters/DispatchError.ts: -------------------------------------------------------------------------------- 1 | import {ArgumentsHost, Catch, ExceptionFilter, HttpStatus, UnauthorizedException} from '@nestjs/common'; 2 | import {AppError} from '../error/AppError'; 3 | 4 | @Catch() 5 | export class DispatchError implements ExceptionFilter { 6 | catch(exception: any, host: ArgumentsHost): any { 7 | const ctx = host.switchToHttp(); 8 | const res = ctx.getResponse(); 9 | 10 | if (exception instanceof AppError) { 11 | return res.status(exception.httpStatus).json({ 12 | errorCode: exception.errorCode, 13 | errorMsg: exception.errorMessage, 14 | usrMsg: exception.userMessage, 15 | httpCode: exception.httpStatus 16 | }); 17 | } else if (exception instanceof UnauthorizedException) { 18 | console.log(exception.message); 19 | console.error(exception.stack); 20 | return res.status(HttpStatus.UNAUTHORIZED).json(exception.message); 21 | } else if (exception.status === 403) { 22 | return res.status(HttpStatus.FORBIDDEN).json(exception.message); 23 | } 24 | 25 | else { 26 | console.error(exception.message); 27 | console.error(exception.stack); 28 | return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main.hmr.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | declare const module: any; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | await app.listen(3000); 9 | 10 | if (module.hot) { 11 | module.hot.accept(); 12 | module.hot.dispose(() => app.close()); 13 | } 14 | } 15 | bootstrap(); 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import {DocumentBuilder, SwaggerModule} from '@nestjs/swagger'; 4 | import {DispatchError} from './common/filters/DispatchError'; 5 | 6 | import * as session from 'express-session'; 7 | import * as passport from 'passport'; 8 | 9 | async function bootstrap() { 10 | const app = await NestFactory.create(AppModule); 11 | app.use(session({ 12 | secret: 'keyboard cat', 13 | name: 'sess-tutorial', 14 | resave: false, 15 | saveUninitialized: false, 16 | })); 17 | app.use(passport.initialize()); 18 | app.use(passport.session()); 19 | app.useGlobalFilters(new DispatchError()); 20 | const options = new DocumentBuilder() 21 | .setTitle('User Session Tutorial') 22 | .setDescription('Basic Auth and session management') 23 | .setVersion('1.0') 24 | .addTag('nestjs') 25 | .addBearerAuth('Authorization', 'header') 26 | .build(); 27 | const document = SwaggerModule.createDocument(app, options); 28 | SwaggerModule.setup('api', app, document); 29 | await app.listen(3000); 30 | } 31 | bootstrap(); 32 | -------------------------------------------------------------------------------- /src/project/IProjectService.ts: -------------------------------------------------------------------------------- 1 | import {CreateProjectDto} from './models/CreateProjectDto'; 2 | import {UserEntity} from '../user/user.entity'; 3 | import {ProjectEntity} from './project.entity'; 4 | 5 | export interface IProjectService { 6 | createProject(projects: CreateProjectDto[], user: UserEntity): Promise; 7 | getProjectsForUser(user: UserEntity): Promise; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/project/models/CreateProjectDto.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | 3 | export class CreateProjectDto { 4 | @ApiModelProperty() 5 | readonly name: string; 6 | 7 | @ApiModelProperty() 8 | readonly description: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/project/project.controller.ts: -------------------------------------------------------------------------------- 1 | import {Body, Controller, Get, HttpStatus, Post, Res, UseGuards} from '@nestjs/common'; 2 | import {SessionGuard} from '../auth/SessionGuard'; 3 | import {SessionUser} from '../user/user.decorator'; 4 | import {UserEntity} from '../user/user.entity'; 5 | import {ApiOperation, ApiUseTags} from '@nestjs/swagger'; 6 | import {CreateProjectDto} from './models/CreateProjectDto'; 7 | import {ProjectService} from './project.service'; 8 | import {ProjectEntity} from './project.entity'; 9 | 10 | @ApiUseTags('project') 11 | @Controller('project') 12 | export class ProjectController { 13 | 14 | constructor(private readonly projectService: ProjectService) {} 15 | 16 | @Get('') 17 | @UseGuards(SessionGuard) 18 | @ApiOperation({title: 'Get Projects for User'}) 19 | public async getProjects(@Res() res, @SessionUser() user: UserEntity) { 20 | const projects: ProjectEntity[] = await this.projectService.getProjectsForUser(user); 21 | return res.status(HttpStatus.OK).send(projects); 22 | } 23 | 24 | @Post('') 25 | @UseGuards(SessionGuard) 26 | @ApiOperation({title: 'Create a project for the logged in user'}) 27 | public async createProject(@Body() createProjects: CreateProjectDto[], @Res() res, @SessionUser() user: UserEntity) { 28 | const projects: ProjectEntity[] = await this.projectService.createProject(createProjects, user); 29 | return res.status(HttpStatus.OK).send(projects); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/project/project.entity.ts: -------------------------------------------------------------------------------- 1 | import {BaseEntity, Column, Entity, ManyToOne, PrimaryGeneratedColumn} from 'typeorm'; 2 | import {UserEntity} from '../user/user.entity'; 3 | import {AppError} from '../common/error/AppError'; 4 | import {AppErrorTypeEnum} from '../common/error/AppErrorTypeEnum'; 5 | import {CreateProjectDto} from './models/CreateProjectDto'; 6 | 7 | @Entity({name: 'projects'}) 8 | export class ProjectEntity extends BaseEntity{ 9 | @PrimaryGeneratedColumn() 10 | id: number; 11 | 12 | @Column() 13 | name: string; 14 | 15 | @Column() 16 | description: string; 17 | 18 | @ManyToOne(type => UserEntity) 19 | user: UserEntity; 20 | 21 | public static async createProjects(projects: CreateProjectDto[], user: UserEntity): Promise { 22 | const u: UserEntity = await UserEntity.findOne(user.id); 23 | if (!u) throw new AppError(AppErrorTypeEnum.USER_NOT_FOUND); 24 | const projectEntities: ProjectEntity[] = []; 25 | projects.forEach((p: CreateProjectDto) => { 26 | const pr: ProjectEntity = new ProjectEntity(); 27 | pr.name = p.name; 28 | pr.description = p.description; 29 | projectEntities.push(pr); 30 | }); 31 | u.projects = projectEntities; 32 | const result: ProjectEntity[] = await ProjectEntity.save(projectEntities); 33 | await UserEntity.save([u]); 34 | return Promise.all(result); 35 | } 36 | 37 | public static async getProjects(user: UserEntity): Promise { 38 | const u: UserEntity = await UserEntity.findOne(user.id, { relations: ['projects']}); 39 | if (!u) throw new AppError(AppErrorTypeEnum.USER_NOT_FOUND); 40 | return Promise.all(u.projects); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/project/project.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ProjectController } from './project.controller'; 3 | import { ProjectService } from './project.service'; 4 | import {TypeOrmModule} from '@nestjs/typeorm'; 5 | import {ProjectEntity} from './project.entity'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([ProjectEntity])], 9 | controllers: [ProjectController], 10 | providers: [ProjectService] 11 | }) 12 | export class ProjectModule {} 13 | -------------------------------------------------------------------------------- /src/project/project.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import {IProjectService} from './IProjectService'; 3 | import {CreateProjectDto} from './models/CreateProjectDto'; 4 | import {UserEntity} from '../user/user.entity'; 5 | import {ProjectEntity} from './project.entity'; 6 | 7 | @Injectable() 8 | export class ProjectService implements IProjectService{ 9 | 10 | public async createProject(projects: CreateProjectDto[], user: UserEntity): Promise { 11 | return ProjectEntity.createProjects(projects, user); 12 | } 13 | 14 | public async getProjectsForUser(user: UserEntity): Promise { 15 | return ProjectEntity.getProjects(user); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/user/IUserService.ts: -------------------------------------------------------------------------------- 1 | import {CreateUserDto} from './models/CreateUserDto'; 2 | import {UserEntity} from './user.entity'; 3 | import {ProjectEntity} from '../project/project.entity'; 4 | 5 | export interface IUserService { 6 | findAll(): Promise; 7 | createUser(user: CreateUserDto): Promise; 8 | getProjectsForUser(user: UserEntity): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /src/user/models/CreateUserDto.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | 3 | export class CreateUserDto { 4 | @ApiModelProperty() 5 | readonly firstName: string; 6 | 7 | @ApiModelProperty() 8 | readonly lastName: string; 9 | 10 | @ApiModelProperty() 11 | readonly username: string; 12 | 13 | @ApiModelProperty() 14 | readonly password: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import {Body, Controller, Get, HttpStatus, Post, Req, Res, Session, UseGuards} from '@nestjs/common'; 2 | import {UserService} from './user.service'; 3 | import {ApiBearerAuth, ApiOperation, ApiResponse, ApiUseTags} from '@nestjs/swagger'; 4 | import {UserEntity} from './user.entity'; 5 | import {CreateUserDto} from './models/CreateUserDto'; 6 | import {Request, Response} from 'express'; 7 | import {AppAuthGuard} from '../auth/AppAuthGuard'; 8 | 9 | @ApiUseTags('user') 10 | @Controller('user') 11 | export class UserController { 12 | constructor(private readonly usersService: UserService) {} 13 | 14 | @Get('') 15 | @ApiOperation({title: 'Get List of All Users'}) 16 | @ApiResponse({ status: 200, description: 'User Found.'}) 17 | @ApiResponse({ status: 404, description: 'No Users found.'}) 18 | public async getAllUsers(@Req() req: Request, @Res() res, @Session() session) { 19 | const users: UserEntity[] = await this.usersService.findAll(); 20 | return res 21 | .status(HttpStatus.OK) 22 | .send(users); 23 | 24 | } 25 | 26 | @Post('') 27 | @ApiOperation({title: 'Create User'}) 28 | public async create(@Body() createUser: CreateUserDto, @Res() res) { 29 | await this.usersService.createUser(createUser); 30 | return res.status(HttpStatus.CREATED).send(); 31 | } 32 | 33 | @Post('login') 34 | @UseGuards(AppAuthGuard) 35 | @ApiOperation({title: 'Authenticate'}) 36 | @ApiBearerAuth() 37 | public async login(@Req() req: Request, @Res() res: Response, @Session() session) { 38 | const ses = session; 39 | return res.status(HttpStatus.OK).send(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/user/user.decorator.ts: -------------------------------------------------------------------------------- 1 | import {createParamDecorator} from '@nestjs/common'; 2 | 3 | export const SessionUser = createParamDecorator((data, req) => { 4 | return req.session.passport.user; 5 | }) 6 | -------------------------------------------------------------------------------- /src/user/user.entity.ts: -------------------------------------------------------------------------------- 1 | import {BaseEntity, Column, Entity, OneToMany, PrimaryGeneratedColumn} from 'typeorm'; 2 | import * as crypto from 'crypto'; 3 | import {ProjectEntity} from '../project/project.entity'; 4 | import {CreateUserDto} from './models/CreateUserDto'; 5 | import {AppErrorTypeEnum} from '../common/error/AppErrorTypeEnum'; 6 | import {AppError} from '../common/error/AppError'; 7 | 8 | @Entity({name: 'users'}) 9 | export class UserEntity extends BaseEntity { 10 | @PrimaryGeneratedColumn() 11 | id: number; 12 | 13 | @Column({ 14 | length: 30 15 | }) 16 | public firstName: string; 17 | 18 | @Column({ 19 | length: 50 20 | }) 21 | public lastName: string; 22 | 23 | @Column({ 24 | length: 50 25 | }) 26 | public username: string; 27 | 28 | @Column({ 29 | length: 250, 30 | select: false, 31 | name: 'password' 32 | }) 33 | public password_hash: string; 34 | 35 | set password(password: string) { 36 | const passHash = crypto.createHmac('sha256', password).digest('hex'); 37 | this.password_hash = passHash; 38 | } 39 | 40 | @OneToMany(type => ProjectEntity, project => project.user) 41 | projects: ProjectEntity[]; 42 | 43 | public static async findAll(): Promise { 44 | const users: UserEntity[] = await UserEntity.find(); 45 | if (users.length > 0) { 46 | return Promise.resolve(users); 47 | } else { 48 | throw new AppError(AppErrorTypeEnum.NO_USERS_IN_DB); 49 | } 50 | 51 | } 52 | 53 | public static async createUser(user: CreateUserDto): Promise { 54 | let u: UserEntity; 55 | u = await UserEntity.findOne({username: user.username}); 56 | if (u) { 57 | throw new AppError(AppErrorTypeEnum.USER_EXISTS); 58 | } else { 59 | u = new UserEntity(); 60 | Object.assign(u, user); 61 | return await UserEntity.save(u); 62 | } 63 | } 64 | 65 | public static async authenticateUser(user: {username: string, password: string}): Promise { 66 | let u: UserEntity; 67 | u = await UserEntity.findOne({ 68 | select: ['id', 'username', 'password_hash'], 69 | where: { username: user.username} 70 | }); 71 | const passHash = crypto.createHmac('sha256', user.password).digest('hex'); 72 | if (u.password_hash === passHash) { 73 | delete u.password_hash; 74 | return u; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserController } from './user.controller'; 3 | import { UserService } from './user.service'; 4 | import {TypeOrmModule} from '@nestjs/typeorm'; 5 | import {UserEntity} from './user.entity'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([UserEntity])], 9 | controllers: [UserController], 10 | providers: [UserService] 11 | }) 12 | export class UserModule {} 13 | -------------------------------------------------------------------------------- /src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import {UserEntity} from './user.entity'; 3 | import {IUserService} from './IUserService'; 4 | import {CreateUserDto} from './models/CreateUserDto'; 5 | import {ProjectEntity} from '../project/project.entity'; 6 | 7 | @Injectable() 8 | export class UserService implements IUserService{ 9 | public async findAll(): Promise { 10 | return await UserEntity.findAll(); 11 | } 12 | 13 | public async createUser(user: CreateUserDto): Promise { 14 | return await UserEntity.createUser(user); 15 | } 16 | 17 | public async getProjectsForUser(user: UserEntity): Promise { 18 | return undefined; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": false, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": true, 13 | "allowJs": true, 14 | "outDir": "./dist", 15 | "baseUrl": "./src" 16 | }, 17 | "include": [ 18 | "src/**/*" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "**/*.spec.ts" 23 | ] 24 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": { 7 | "no-unused-expression": true 8 | }, 9 | "rules": { 10 | "eofline": false, 11 | "quotemark": [ 12 | true, 13 | "single" 14 | ], 15 | "indent": false, 16 | "member-access": [ 17 | false 18 | ], 19 | "ordered-imports": [ 20 | false 21 | ], 22 | "max-line-length": [ 23 | true, 24 | 150 25 | ], 26 | "member-ordering": [ 27 | false 28 | ], 29 | "curly": false, 30 | "interface-name": [ 31 | false 32 | ], 33 | "array-type": [ 34 | false 35 | ], 36 | "no-empty-interface": false, 37 | "no-empty": false, 38 | "arrow-parens": false, 39 | "object-literal-sort-keys": false, 40 | "no-unused-expression": false, 41 | "max-classes-per-file": [ 42 | false 43 | ], 44 | "variable-name": [ 45 | false 46 | ], 47 | "one-line": [ 48 | false 49 | ], 50 | "one-variable-per-declaration": [ 51 | false 52 | ], 53 | "trailing-comma": [false, {"multiline": "always", "singleline": "never"}], 54 | "no-console": [false, "log", "error"] 55 | }, 56 | "rulesDirectory": [] 57 | } 58 | -------------------------------------------------------------------------------- /tutorial.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artonio/nestjs-session-tutorial-finished/bb63fd0778c8ba05a2f9e0fe508650e0b3bd988b/tutorial.sqlite -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const nodeExternals = require('webpack-node-externals'); 4 | 5 | module.exports = { 6 | entry: ['webpack/hot/poll?1000', './src/main.hmr.ts'], 7 | watch: true, 8 | target: 'node', 9 | externals: [ 10 | nodeExternals({ 11 | whitelist: ['webpack/hot/poll?1000'], 12 | }), 13 | ], 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | use: 'ts-loader', 19 | exclude: /node_modules/, 20 | }, 21 | ], 22 | }, 23 | mode: "development", 24 | resolve: { 25 | extensions: ['.tsx', '.ts', '.js'], 26 | }, 27 | plugins: [ 28 | new webpack.HotModuleReplacementPlugin(), 29 | ], 30 | output: { 31 | path: path.join(__dirname, 'dist'), 32 | filename: 'server.js', 33 | }, 34 | }; 35 | --------------------------------------------------------------------------------