├── .env ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── nest-cli.json ├── ormconfig.js ├── package.json ├── src ├── api │ ├── api.module.ts │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── constants.ts │ │ ├── dto │ │ │ ├── index.ts │ │ │ ├── login-user.dto.ts │ │ │ └── register-user.dto.ts │ │ └── strategies │ │ │ └── jwt.strategy.ts │ ├── index.ts │ ├── lov │ │ ├── enum │ │ │ └── index.ts │ │ ├── lov.entity.ts │ │ ├── lov.module.ts │ │ └── lov.service.ts │ ├── project │ │ ├── dto │ │ │ ├── create-project.dto.ts │ │ │ ├── index.ts │ │ │ └── update-project.dto.ts │ │ ├── project.controller.ts │ │ ├── project.entity.ts │ │ ├── project.guard.ts │ │ ├── project.module.ts │ │ └── project.service.ts │ ├── task │ │ ├── dto │ │ │ ├── create-task.dto.ts │ │ │ ├── index.ts │ │ │ └── update-task.dto.ts │ │ ├── models │ │ │ └── index.ts │ │ ├── task.controller.ts │ │ ├── task.entity.ts │ │ ├── task.module.ts │ │ └── task.service.ts │ ├── ui │ │ ├── models │ │ │ ├── create-task-config.ts │ │ │ ├── index.ts │ │ │ ├── login-header.ts │ │ │ └── user-info.ts │ │ ├── ui.controller.ts │ │ └── ui.module.ts │ └── user │ │ ├── user.controller.ts │ │ ├── user.entity.ts │ │ ├── user.module.ts │ │ └── user.service.ts ├── app.module.ts ├── bootstrap.ts ├── database │ ├── database.module.ts │ └── seeds │ │ └── lov.seed.ts ├── main.ts └── shared │ ├── entities │ ├── date-audit.entity.ts │ └── index.ts │ ├── guards │ ├── index.ts │ └── jwt-auth.guard.ts │ ├── index.ts │ ├── models │ ├── index.ts │ └── validation-message-list.model.ts │ ├── pipes │ ├── index.ts │ └── validation.pipe.ts │ └── shared.module.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | # Application 2 | APP_PORT=3001 3 | 4 | # Database 5 | DB_DIALECT=mysql 6 | DB_HOST=localhost 7 | DB_PORT=3306 8 | DB_USERNAME=root 9 | DB_PASSWORD= 10 | DB_DATABASE=jira-db 11 | DB_LOGGING=false 12 | DB_SYNCHRONIZE=true 13 | DB_DROP_SCHEMA=false 14 | DB_MIGRATIONS_RUN=false 15 | TYPEORM_SEEDING_FACTORIES=src/database/factories/**/*{.ts,.js} 16 | TYPEORM_SEEDING_SEEDS=src/database/seeds/**/*{.ts,.js} 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | 'prettier/@typescript-eslint', 13 | ], 14 | root: true, 15 | env: { 16 | node: true, 17 | jest: true, 18 | }, 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "printWidth": 120, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | ## Project structure 6 | 7 | 8 | 9 | This repo is used as frontend in this repo: https://github.com/ozergul/jira-clone-frontend-angular 10 | 11 | ## First run 12 | 13 | To create some of entities please run: 14 | 15 | ```bash 16 | $ npm run seed:run 17 | ``` 18 | 19 | ## Running the app 20 | 21 | ```bash 22 | # development 23 | $ npm run start 24 | 25 | # watch mode 26 | $ npm run start:dev 27 | 28 | # production mode 29 | $ npm run start:prod 30 | ``` 31 | 32 | ## Test 33 | 34 | ```bash 35 | # unit tests 36 | $ npm run test 37 | 38 | # e2e tests 39 | $ npm run test:e2e 40 | 41 | # test coverage 42 | $ npm run test:cov 43 | ``` 44 | 45 | ## Stay in touch 46 | 47 | - Author - [Ozer Gul](https://ozergul.net) 48 | - Website - [https://nestjs.com](https://nestjs.com/) 49 | - Twitter - [@ozergul1](https://twitter.com/ozergul1) 50 | 51 | ## License 52 | 53 | Nest is [MIT licensed](LICENSE). 54 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /ormconfig.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const envConfig = require('dotenv').config({ 3 | path: path.resolve(__dirname, `.env${process.env.NODE_ENV ? `.${process.env.NODE_ENV}` : ''}`), 4 | }); 5 | 6 | function env(key) { 7 | return envConfig.parsed[key] || process.env[key]; 8 | } 9 | 10 | module.exports = { 11 | type: env('DB_DIALECT'), 12 | database: env('DB_DATABASE'), 13 | logger: 'advanced-console', 14 | logging: ['warn', 'error'], 15 | entities: ['dist/**/*.entity.js'], 16 | host: env('DB_HOST'), 17 | port: env('DB_PORT'), 18 | username: env('DB_USERNAME'), 19 | password: env('DB_PASSWORD'), 20 | synchronize: env('DB_SYNCHRONIZE') === 'true', 21 | seeds: [env('TYPEORM_SEEDING_SEEDS')], 22 | factories: [env('TYPEORM_SEEDING_FACTORIES')], 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jira-clone-backend", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "seed:config": "ts-node ./node_modules/typeorm-seeding/dist/cli.js config", 10 | "seed:run": "ts-node ./node_modules/typeorm-seeding/dist/cli.js seed", 11 | "prebuild": "rimraf dist", 12 | "build": "nest build", 13 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 14 | "start": "nest start", 15 | "start:dev": "nest start --watch", 16 | "start:debug": "nest start --debug --watch", 17 | "start:prod": "node dist/main", 18 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 19 | "test": "jest", 20 | "test:watch": "jest --watch", 21 | "test:cov": "jest --coverage", 22 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 23 | "test:e2e": "jest --config ./test/jest-e2e.json" 24 | }, 25 | "dependencies": { 26 | "@nestjs/common": "^7.4.4", 27 | "@nestjs/config": "^0.5.0", 28 | "@nestjs/core": "^7.0.0", 29 | "@nestjs/jwt": "^7.1.0", 30 | "@nestjs/passport": "^7.1.0", 31 | "@nestjs/platform-express": "^7.0.0", 32 | "@nestjs/swagger": "^4.6.0", 33 | "@nestjs/typeorm": "^7.1.4", 34 | "bcrypt": "^5.0.0", 35 | "class-transformer": "^0.3.1", 36 | "class-validator": "^0.12.2", 37 | "compression": "^1.7.4", 38 | "express-rate-limit": "^5.1.3", 39 | "helmet": "^4.1.1", 40 | "mysql2": "^2.2.2", 41 | "nestjs-typeorm-paginate": "^2.1.1", 42 | "nocache": "^2.1.0", 43 | "passport": "^0.4.1", 44 | "passport-jwt": "^4.0.0", 45 | "reflect-metadata": "^0.1.13", 46 | "rimraf": "^3.0.2", 47 | "rxjs": "^6.5.4", 48 | "swagger-ui-express": "^4.1.4", 49 | "typeorm": "^0.2.26", 50 | "typeorm-seeding": "^1.6.1" 51 | }, 52 | "devDependencies": { 53 | "@nestjs/cli": "^7.0.0", 54 | "@nestjs/schematics": "^7.0.0", 55 | "@nestjs/testing": "^7.0.0", 56 | "@types/express": "^4.17.3", 57 | "@types/jest": "26.0.10", 58 | "@types/node": "^13.9.1", 59 | "@types/passport": "^1.0.4", 60 | "@types/passport-jwt": "^3.0.3", 61 | "@types/passport-local": "^1.0.33", 62 | "@types/supertest": "^2.0.8", 63 | "@typescript-eslint/eslint-plugin": "3.9.1", 64 | "@typescript-eslint/parser": "3.9.1", 65 | "eslint": "7.7.0", 66 | "eslint-config-prettier": "^6.10.0", 67 | "eslint-plugin-import": "^2.20.1", 68 | "jest": "26.4.2", 69 | "prettier": "^1.19.1", 70 | "supertest": "^4.0.2", 71 | "ts-jest": "26.2.0", 72 | "ts-loader": "^6.2.1", 73 | "ts-node": "9.0.0", 74 | "tsconfig-paths": "^3.9.0", 75 | "typescript": "^3.7.4" 76 | }, 77 | "jest": { 78 | "moduleFileExtensions": [ 79 | "js", 80 | "json", 81 | "ts" 82 | ], 83 | "rootDir": "src", 84 | "testRegex": ".spec.ts$", 85 | "transform": { 86 | "^.+\\.(t|j)s$": "ts-jest" 87 | }, 88 | "coverageDirectory": "../coverage", 89 | "testEnvironment": "node" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/api/api.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserModule } from './user/user.module'; 3 | import { AuthModule } from './auth/auth.module'; 4 | import { ProjectModule } from './project/project.module'; 5 | import { TaskModule } from './task/task.module'; 6 | import { LovModule } from './lov/lov.module'; 7 | import { UiModule } from './ui/ui.module'; 8 | 9 | @Module({ 10 | imports: [ 11 | UserModule, 12 | AuthModule, 13 | ProjectModule, 14 | TaskModule, 15 | LovModule, 16 | UiModule, 17 | ], 18 | }) 19 | export class ApiModule {} 20 | -------------------------------------------------------------------------------- /src/api/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, HttpException, HttpStatus, Post, Req, Res, UseGuards } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { RegisterUserDto } from './dto'; 4 | import { AuthGuard } from '@nestjs/passport'; 5 | import { UserService } from '../user/user.service'; 6 | 7 | @Controller('/auth') 8 | export class AuthController { 9 | constructor(private readonly authService: AuthService, private readonly userService: UserService) {} 10 | 11 | @Post('/login') 12 | async login(@Req() req) { 13 | return this.authService.login(req.body); 14 | } 15 | 16 | @Post('/register') 17 | async register(@Res() res, @Body() registerUserDto: RegisterUserDto) { 18 | const isRegistered = await this.userService.findOneByEmail(registerUserDto.email); 19 | if (isRegistered) { 20 | res.status(HttpStatus.BAD_REQUEST).json({ 21 | message: 'Email you provided is existing.', 22 | }); 23 | return; 24 | } 25 | 26 | const user = await this.userService.create(registerUserDto); 27 | if (user) { 28 | const { password, ...result } = user; 29 | 30 | res.status(HttpStatus.OK).send(result); 31 | } else { 32 | res.status(HttpStatus.BAD_REQUEST).send(); 33 | } 34 | } 35 | 36 | @UseGuards(AuthGuard('jwt')) 37 | @Get('/me') 38 | getProfile(@Req() req) { 39 | const { password, ...user } = req.user; 40 | return user; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/api/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { JwtModule } from '@nestjs/jwt'; 3 | import { PassportModule } from '@nestjs/passport'; 4 | import { AuthService } from './auth.service'; 5 | import { jwtConstants } from './constants'; 6 | import { JwtStrategy } from './strategies/jwt.strategy'; 7 | import { UserService } from '../user/user.service'; 8 | import { TypeOrmModule } from '@nestjs/typeorm'; 9 | import { User } from '../user/user.entity'; 10 | import { AuthController } from './auth.controller'; 11 | import { SharedModule } from '../../shared'; 12 | 13 | @Module({ 14 | imports: [ 15 | SharedModule, 16 | PassportModule, 17 | JwtModule.register({ 18 | secret: jwtConstants.secret, 19 | signOptions: { expiresIn: '2 days' }, 20 | }), 21 | TypeOrmModule.forFeature([User]), 22 | ], 23 | controllers: [AuthController], 24 | providers: [AuthService, JwtStrategy, UserService], 25 | exports: [PassportModule, JwtModule], 26 | }) 27 | export class AuthModule {} 28 | -------------------------------------------------------------------------------- /src/api/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { UserService } from '../user/user.service'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | import { User } from '../user/user.entity'; 6 | import { Repository } from 'typeorm'; 7 | import * as bcrypt from 'bcrypt'; 8 | 9 | @Injectable() 10 | export class AuthService { 11 | constructor( 12 | private readonly usersService: UserService, 13 | private readonly jwtService: JwtService, 14 | @InjectRepository(User) 15 | private userRepository: Repository, 16 | ) {} 17 | 18 | async validateUser(payload: any): Promise { 19 | const user = await this.usersService.findOneByEmail(payload.email); 20 | const exception = new HttpException('Invalid token', HttpStatus.UNAUTHORIZED); 21 | if (!user) { 22 | throw exception; 23 | } else { 24 | const passwordMatched = await bcrypt.compare(payload.password, user.password); 25 | 26 | if (!passwordMatched) { 27 | throw exception; 28 | } 29 | } 30 | return user; 31 | } 32 | 33 | async login(user: any) { 34 | const checkedUser = await this.usersService.findOneByEmail(user.email); 35 | const exception = new HttpException('Bad Credentials', HttpStatus.UNAUTHORIZED); 36 | if (!checkedUser) { 37 | throw exception; 38 | } else { 39 | const passwordMatched = await bcrypt.compare(user.password, checkedUser.password); 40 | 41 | if (!passwordMatched) { 42 | throw exception; 43 | } 44 | } 45 | 46 | const payload = { email: user.email, password: user.password, sub: user.id }; 47 | return { 48 | access_token: this.jwtService.sign(payload), 49 | }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/api/auth/constants.ts: -------------------------------------------------------------------------------- 1 | export const jwtConstants = { 2 | secret: 'secretKey', 3 | }; 4 | -------------------------------------------------------------------------------- /src/api/auth/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-user.dto'; 2 | export * from './register-user.dto'; 3 | -------------------------------------------------------------------------------- /src/api/auth/dto/login-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class LoginUserDto { 5 | @ApiProperty() 6 | @IsNotEmpty() 7 | readonly email: string; 8 | 9 | @ApiProperty() 10 | @IsNotEmpty() 11 | readonly password: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/api/auth/dto/register-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class RegisterUserDto { 5 | @ApiProperty() 6 | @IsNotEmpty() 7 | @IsEmail() 8 | readonly email: string; 9 | 10 | @ApiProperty() 11 | @IsNotEmpty() 12 | readonly password: string; 13 | 14 | @ApiProperty() 15 | @IsNotEmpty() 16 | readonly firstName: string; 17 | 18 | @ApiProperty() 19 | @IsNotEmpty() 20 | readonly lastName: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/api/auth/strategies/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { ExtractJwt, Strategy } from 'passport-jwt'; 4 | import { jwtConstants } from '../constants'; 5 | import { AuthService } from '../auth.service'; 6 | 7 | @Injectable() 8 | export class JwtStrategy extends PassportStrategy(Strategy) { 9 | constructor(private readonly authService: AuthService) { 10 | super({ 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | ignoreExpiration: false, 13 | secretOrKey: jwtConstants.secret, 14 | }); 15 | } 16 | 17 | async validate(payload: any): Promise<{ access_token: string }> { 18 | const user = await this.authService.validateUser(payload); 19 | if (!user) { 20 | throw new HttpException('Invalid token', HttpStatus.UNAUTHORIZED); 21 | } 22 | return user; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user/user.module'; 2 | export * from './auth/auth.module'; 3 | export * from './project/project.module'; 4 | export * from './task/task.module'; 5 | -------------------------------------------------------------------------------- /src/api/lov/enum/index.ts: -------------------------------------------------------------------------------- 1 | export enum LovType { 2 | TASK_TYPE = 'TASK_TYPE', 3 | TASK_PRIORITY = 'TASK_PRIORITY', 4 | } 5 | 6 | export enum TaskPriority { 7 | LOW, 8 | MEDIUM, 9 | URGENT, 10 | } 11 | 12 | export enum TaskType { 13 | TASK, 14 | BUG, 15 | STORY, 16 | } 17 | -------------------------------------------------------------------------------- /src/api/lov/lov.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { LovType, TaskPriority, TaskType } from './enum'; 3 | import { DateAudit } from '../../shared/entities'; 4 | 5 | @Entity('lovs') 6 | export class Lov extends DateAudit { 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | @Column({ 11 | type: 'enum', 12 | enum: LovType, 13 | }) 14 | type: LovType; 15 | 16 | @Column('text') 17 | value: TaskPriority | TaskType; 18 | 19 | @Column('text') 20 | text: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/api/lov/lov.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { LovService } from './lov.service'; 4 | import { Lov } from './lov.entity'; 5 | import { SharedModule } from '../../shared'; 6 | 7 | @Module({ 8 | imports: [SharedModule, TypeOrmModule.forFeature([Lov])], 9 | providers: [LovService], 10 | }) 11 | export class LovModule {} 12 | -------------------------------------------------------------------------------- /src/api/lov/lov.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { Lov } from './lov.entity'; 5 | import { LovType } from './enum'; 6 | 7 | @Injectable() 8 | export class LovService { 9 | constructor( 10 | @InjectRepository(Lov) 11 | private lovRepository: Repository, 12 | ) {} 13 | 14 | async findAllByType(lovType: LovType): Promise { 15 | return this.lovRepository.find({ 16 | where: { type: lovType }, 17 | order: { 18 | value: 'DESC', 19 | }, 20 | }); 21 | } 22 | 23 | async findById(id: number): Promise { 24 | return this.lovRepository.findOne({ id }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/api/project/dto/create-project.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNotEmpty } from 'class-validator'; 3 | 4 | export class CreateProjectDto { 5 | @ApiProperty() 6 | @IsNotEmpty() 7 | readonly code: string; 8 | 9 | @ApiProperty() 10 | @IsNotEmpty() 11 | readonly title: string; 12 | 13 | @ApiProperty() 14 | readonly description: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/api/project/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-project.dto'; 2 | -------------------------------------------------------------------------------- /src/api/project/dto/update-project.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class UpdateProjectDto { 5 | @ApiProperty() 6 | @IsNotEmpty() 7 | readonly id: number; 8 | 9 | @ApiProperty() 10 | @IsNotEmpty() 11 | readonly code: string; 12 | 13 | @ApiProperty() 14 | @IsNotEmpty() 15 | readonly title: string; 16 | 17 | @ApiProperty() 18 | readonly description: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/api/project/project.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | HttpStatus, 7 | Param, 8 | Post, 9 | Put, 10 | Query, 11 | Req, 12 | Res, 13 | UseGuards, 14 | } from '@nestjs/common'; 15 | import { ApiTags } from '@nestjs/swagger'; 16 | import { Response } from 'express'; 17 | import { Pagination } from 'nestjs-typeorm-paginate'; 18 | import { CreateProjectDto } from './dto'; 19 | import { UpdateProjectDto } from './dto/update-project.dto'; 20 | import { Project } from './project.entity'; 21 | import { ProjectService } from './project.service'; 22 | import { AuthGuard } from '@nestjs/passport'; 23 | import { User } from '../user/user.entity'; 24 | import { ProjectGuard } from './project.guard'; 25 | 26 | @ApiTags('projects') 27 | @Controller('/projects') 28 | export class ProjectController { 29 | constructor(private readonly projectService: ProjectService) {} 30 | 31 | @Get() 32 | @UseGuards(AuthGuard('jwt')) 33 | async paginate(@Req() req, @Query('page') page = 1, @Query('limit') limit = 10): Promise> { 34 | const user = req.user as User; 35 | return await this.projectService.paginate({ 36 | options: { 37 | page, 38 | limit, 39 | }, 40 | userId: user.id, 41 | }); 42 | } 43 | 44 | @Get('/:code') 45 | @UseGuards(AuthGuard('jwt')) 46 | async getByCode(@Param() params): Promise { 47 | return await this.projectService.findByCode(params.code); 48 | } 49 | 50 | @Post('/create') 51 | @UseGuards(AuthGuard('jwt')) 52 | async create(@Req() req, @Res() res: Response, @Body() createProjectDto: CreateProjectDto) { 53 | const user = req.user as User; 54 | const code = createProjectDto.code.toLocaleUpperCase(); 55 | const isExist = await this.projectService.findByCode(code); 56 | 57 | if (isExist) { 58 | return res.status(HttpStatus.BAD_REQUEST).json({ 59 | message: 'Code you provided is existing.', 60 | }); 61 | } 62 | 63 | const project = await this.projectService.create({ ...createProjectDto, createdBy: user.id }); 64 | 65 | if (project) { 66 | res.status(HttpStatus.OK).send(); 67 | } else { 68 | res.status(HttpStatus.BAD_REQUEST).send(); 69 | } 70 | } 71 | 72 | @Put('/update') 73 | @UseGuards(AuthGuard('jwt'), ProjectGuard) 74 | async update(@Res() res: Response, @Body() updateProjectDto: UpdateProjectDto) { 75 | const updated = await this.projectService.update(updateProjectDto); 76 | 77 | if (updated) { 78 | res.status(HttpStatus.OK).send(); 79 | } else { 80 | res.status(HttpStatus.BAD_REQUEST).send(); 81 | } 82 | } 83 | 84 | @Post('/complete/:id') 85 | @UseGuards(AuthGuard('jwt'), ProjectGuard) 86 | async completeProject(@Res() res: Response, @Param() params) { 87 | const id = params.id; 88 | const completed = await this.projectService.complete(id); 89 | if (completed) { 90 | const updatedLastEntity = await this.projectService.findById(id); 91 | 92 | res.status(HttpStatus.OK).send(updatedLastEntity); 93 | } else { 94 | res.status(HttpStatus.BAD_REQUEST).send(); 95 | } 96 | } 97 | 98 | @Delete('/:id') 99 | @UseGuards(AuthGuard('jwt'), ProjectGuard) 100 | async delete(@Res() res: Response, @Param() params) { 101 | const deleted = await this.projectService.delete(params.id); 102 | if (deleted) { 103 | res.status(HttpStatus.OK).send(deleted); 104 | } else { 105 | res.status(HttpStatus.BAD_REQUEST).send(); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/api/project/project.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { DateAudit } from '../../shared/entities'; 3 | 4 | @Entity('projects') 5 | export class Project extends DateAudit { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column('text') 10 | code: string; 11 | 12 | @Column('text') 13 | title: string; 14 | 15 | @Column('text', { nullable: true }) 16 | description: string; 17 | 18 | @Column('boolean', { default: false }) 19 | isCompleted: boolean; 20 | 21 | @Column('int') 22 | createdBy: number; 23 | } 24 | -------------------------------------------------------------------------------- /src/api/project/project.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { ProjectService } from './project.service'; 3 | 4 | @Injectable() 5 | export class ProjectGuard implements CanActivate { 6 | constructor(private readonly projectService: ProjectService) {} 7 | canActivate(context: ExecutionContext): Promise { 8 | const request = context.switchToHttp().getRequest(); 9 | const id = request.body.id || request.params.id; 10 | return new Promise(resolve => { 11 | return this.projectService 12 | .findById(id) 13 | .then(project => { 14 | const userId = request.user.id; 15 | if (!userId) { 16 | resolve(false); 17 | } else { 18 | resolve(project?.createdBy === userId); 19 | } 20 | }) 21 | .catch(_ => { 22 | resolve(false); 23 | }); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/api/project/project.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { ProjectController } from './project.controller'; 4 | import { Project } from './project.entity'; 5 | import { ProjectService } from './project.service'; 6 | import { SharedModule } from '../../shared'; 7 | 8 | @Module({ 9 | imports: [SharedModule, TypeOrmModule.forFeature([Project])], 10 | controllers: [ProjectController], 11 | providers: [ProjectService], 12 | }) 13 | export class ProjectModule {} 14 | -------------------------------------------------------------------------------- /src/api/project/project.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { IPaginationOptions, paginate, Pagination } from 'nestjs-typeorm-paginate'; 4 | import { Repository, UpdateResult } from 'typeorm'; 5 | import { CreateProjectDto } from './dto'; 6 | import { UpdateProjectDto } from './dto/update-project.dto'; 7 | import { Project } from './project.entity'; 8 | import { DeleteResult } from 'typeorm/query-builder/result/DeleteResult'; 9 | 10 | @Injectable() 11 | export class ProjectService { 12 | constructor( 13 | @InjectRepository(Project) 14 | private projectRepository: Repository, 15 | ) {} 16 | 17 | async create(params: CreateProjectDto & { createdBy: number }): Promise { 18 | const entity = Object.assign(new Project(), params); 19 | return this.save(entity); 20 | } 21 | 22 | async findByCode(code: string): Promise { 23 | code = code.toLocaleUpperCase(); 24 | return this.projectRepository.findOne({ code }); 25 | } 26 | 27 | async findById(id: number): Promise { 28 | return this.projectRepository.findOne({ id }); 29 | } 30 | 31 | async paginate({ options, userId }: { options: IPaginationOptions; userId: number }): Promise> { 32 | const queryBuilder = this.projectRepository.createQueryBuilder('project'); 33 | queryBuilder.orderBy('project.updatedAt', 'DESC'); 34 | queryBuilder.where('project.createdBy = :createdBy', { createdBy: userId }); 35 | return paginate(queryBuilder, options); 36 | } 37 | 38 | async update(updateProjectDto: UpdateProjectDto): Promise { 39 | const entity = Object.assign(new Project(), updateProjectDto); 40 | return this.projectRepository.save(entity); 41 | } 42 | 43 | async complete(id: number): Promise { 44 | return this.projectRepository.update(id, { isCompleted: true }); 45 | } 46 | 47 | async delete(id: number): Promise { 48 | return this.projectRepository.delete(id); 49 | } 50 | 51 | async getAll(): Promise { 52 | return this.projectRepository.find(); 53 | } 54 | 55 | async save(entity: Partial): Promise { 56 | return this.projectRepository.save(entity); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/api/task/dto/create-task.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNotEmpty } from 'class-validator'; 3 | 4 | export class CreateTaskDto { 5 | @ApiProperty() 6 | @IsNotEmpty() 7 | readonly title: string; 8 | 9 | @ApiProperty() 10 | readonly description: string; 11 | 12 | @ApiProperty() 13 | @IsNotEmpty() 14 | readonly projectId: number; 15 | 16 | @ApiProperty() 17 | @IsNotEmpty() 18 | readonly priorityId: number; 19 | 20 | @ApiProperty() 21 | @IsNotEmpty() 22 | readonly typeId: number; 23 | 24 | @ApiProperty() 25 | readonly assigneeId: number; 26 | } 27 | -------------------------------------------------------------------------------- /src/api/task/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-task.dto'; 2 | export * from './update-task.dto'; 3 | -------------------------------------------------------------------------------- /src/api/task/dto/update-task.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNotEmpty } from 'class-validator'; 3 | import { CreateTaskDto } from './create-task.dto'; 4 | 5 | export class UpdateTaskDto extends CreateTaskDto { 6 | @ApiProperty() 7 | @IsNotEmpty() 8 | readonly id: number; 9 | 10 | @ApiProperty() 11 | @IsNotEmpty() 12 | readonly taskId: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/api/task/models/index.ts: -------------------------------------------------------------------------------- 1 | export enum State { 2 | ASSIGNED = 'ASSIGNED', 3 | REPORTED = 'REPORTED', 4 | } 5 | -------------------------------------------------------------------------------- /src/api/task/task.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, HttpStatus, Param, Post, Put, Query, Req, Res, UseGuards } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { Pagination } from 'nestjs-typeorm-paginate'; 4 | import { Task } from './task.entity'; 5 | import { TaskService } from './task.service'; 6 | import { Response } from 'express'; 7 | import { CreateTaskDto, UpdateTaskDto } from './dto'; 8 | import { AuthGuard } from '@nestjs/passport'; 9 | import { User } from '../user/user.entity'; 10 | import { State } from './models'; 11 | import { LovService } from '../lov/lov.service'; 12 | import { ProjectService } from '../project/project.service'; 13 | 14 | @ApiTags('tasks') 15 | @Controller('/tasks') 16 | export class TaskController { 17 | constructor( 18 | private readonly taskService: TaskService, 19 | private readonly lovService: LovService, 20 | private readonly projectService: ProjectService, 21 | ) {} 22 | 23 | @Get('/') 24 | @UseGuards(AuthGuard('jwt')) 25 | async paginate( 26 | @Req() req, 27 | @Query('page') page = 1, 28 | @Query('limit') limit = 10, 29 | @Query('state') state: State, 30 | ): Promise> { 31 | const user = req.user as User; 32 | return await this.taskService.paginate({ 33 | options: { 34 | page, 35 | limit, 36 | }, 37 | userId: user.id, 38 | state, 39 | }); 40 | } 41 | 42 | @Post('/create') 43 | @UseGuards(AuthGuard('jwt')) 44 | async create(@Req() req, @Res() res: Response, @Body() createTaskDto: CreateTaskDto) { 45 | const user = req.user as User; 46 | const task = await this.taskService.create(createTaskDto); 47 | 48 | if (task) { 49 | if (!task.assigneeId) { 50 | task.assigneeId = user.id; 51 | } 52 | 53 | task.reporterId = user.id; 54 | 55 | const type = await this.lovService.findById(createTaskDto.typeId); 56 | task.type = type; 57 | 58 | const priority = await this.lovService.findById(createTaskDto.priorityId); 59 | task.priority = priority; 60 | 61 | const project = await this.projectService.findById(createTaskDto.projectId); 62 | const taskId = `${project.code.toLocaleUpperCase()}-${task.id}`; 63 | 64 | task.taskId = taskId; 65 | 66 | const savedTask = await this.taskService.save(task); 67 | res.status(HttpStatus.OK).send(savedTask); 68 | } else { 69 | res.status(HttpStatus.BAD_REQUEST).send(); 70 | } 71 | } 72 | 73 | @Get('/:taskId') 74 | async getByTaskId(@Param() params): Promise { 75 | return await this.taskService.getByTaskId(params.taskId); 76 | } 77 | 78 | @Put('/update') 79 | @UseGuards(AuthGuard('jwt')) 80 | async update(@Res() res: Response, @Body() updateTaskDto: UpdateTaskDto) { 81 | const updated = await this.taskService.update(updateTaskDto); 82 | 83 | if (updated) { 84 | res.status(HttpStatus.OK).send(); 85 | } else { 86 | res.status(HttpStatus.BAD_REQUEST).send(); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/api/task/task.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, JoinTable, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { DateAudit } from '../../shared/entities'; 3 | import { Lov } from '../lov/lov.entity'; 4 | 5 | @Entity('tasks') 6 | export class Task extends DateAudit { 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | @Column('text', { nullable: true }) 11 | taskId: string; 12 | 13 | @Column('text') 14 | title: string; 15 | 16 | @Column('text', { nullable: true }) 17 | description: string; 18 | 19 | @Column('int') 20 | reporterId: number; 21 | 22 | @Column('int', { nullable: true }) 23 | assigneeId: number; 24 | 25 | @ManyToOne( 26 | type => Lov, 27 | lov => lov.id, 28 | ) 29 | @JoinTable() 30 | type: Lov; 31 | 32 | @Column('int') 33 | projectId: number; 34 | 35 | @ManyToOne( 36 | type => Lov, 37 | lov => lov.id, 38 | ) 39 | @JoinTable() 40 | priority: Lov; 41 | } 42 | -------------------------------------------------------------------------------- /src/api/task/task.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Task } from './task.entity'; 4 | import { TaskController } from './task.controller'; 5 | import { TaskService } from './task.service'; 6 | import { SharedModule } from '../../shared'; 7 | import { Lov } from '../lov/lov.entity'; 8 | import { LovService } from '../lov/lov.service'; 9 | import { Project } from '../project/project.entity'; 10 | import { ProjectService } from '../project/project.service'; 11 | 12 | @Module({ 13 | imports: [SharedModule, TypeOrmModule.forFeature([Task, Lov, Project])], 14 | controllers: [TaskController], 15 | providers: [TaskService, LovService, ProjectService], 16 | }) 17 | export class TaskModule {} 18 | -------------------------------------------------------------------------------- /src/api/task/task.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { IPaginationOptions, paginate, Pagination } from 'nestjs-typeorm-paginate'; 4 | import { Repository } from 'typeorm'; 5 | import { Task } from './task.entity'; 6 | import { CreateTaskDto, UpdateTaskDto } from './dto'; 7 | import { State } from './models'; 8 | import { LovService } from '../lov/lov.service'; 9 | 10 | @Injectable() 11 | export class TaskService { 12 | constructor( 13 | @InjectRepository(Task) 14 | private taskRepository: Repository, 15 | private lovService: LovService, 16 | ) {} 17 | 18 | async paginate({ 19 | options, 20 | userId, 21 | state, 22 | }: { 23 | options: IPaginationOptions; 24 | userId: number; 25 | state: State; 26 | }): Promise> { 27 | const queryBuilder = this.taskRepository.createQueryBuilder('task'); 28 | if (state === State.ASSIGNED) { 29 | queryBuilder.where('task.assigneeId = :assigneeId', { assigneeId: userId }); 30 | } else if (state === State.REPORTED) { 31 | queryBuilder.where('task.reporterId = :reporterId', { reporterId: userId }); 32 | } 33 | 34 | queryBuilder.orderBy('task.updatedAt', 'DESC'); 35 | 36 | queryBuilder.leftJoinAndSelect('task.type', 'type'); 37 | queryBuilder.leftJoinAndSelect('task.priority', 'priority'); 38 | 39 | return await paginate(queryBuilder, options); 40 | } 41 | 42 | async create(createTaskDto: CreateTaskDto): Promise { 43 | const entity = Object.assign(new Task(), createTaskDto); 44 | return this.taskRepository.save(entity); 45 | } 46 | 47 | async getByTaskId(taskId: string): Promise { 48 | return this.taskRepository.findOne({ taskId }, { relations: ['type', 'priority'] }); 49 | } 50 | 51 | async save(entity: Partial): Promise { 52 | return this.taskRepository.save(entity); 53 | } 54 | 55 | async update(updateTaskDto: UpdateTaskDto) { 56 | const priority = await this.lovService.findById(updateTaskDto.priorityId); 57 | const type = await this.lovService.findById(updateTaskDto.typeId); 58 | 59 | const entity: Partial = { 60 | id: updateTaskDto.id, 61 | taskId: updateTaskDto.taskId, 62 | title: updateTaskDto.title, 63 | description: updateTaskDto.description, 64 | priority, 65 | type, 66 | }; 67 | return this.taskRepository.update(updateTaskDto.id, entity); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/api/ui/models/create-task-config.ts: -------------------------------------------------------------------------------- 1 | import { Lov } from '../../lov/lov.entity'; 2 | import { Project } from '../../project/project.entity'; 3 | import { UserInfo } from './user-info'; 4 | 5 | export interface CreateTaskConfig { 6 | priorities: Lov[]; 7 | types: Lov[]; 8 | projects: Project[]; 9 | users: UserInfo[]; 10 | } 11 | -------------------------------------------------------------------------------- /src/api/ui/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-task-config'; 2 | export * from './login-header'; 3 | export * from './user-info'; 4 | -------------------------------------------------------------------------------- /src/api/ui/models/login-header.ts: -------------------------------------------------------------------------------- 1 | import { Project } from '../../project/project.entity'; 2 | import { Task } from '../../task/task.entity'; 3 | 4 | export interface LoginHeader { 5 | tasks: Task[]; 6 | projects: Project[]; 7 | } 8 | -------------------------------------------------------------------------------- /src/api/ui/models/user-info.ts: -------------------------------------------------------------------------------- 1 | export interface UserInfo { 2 | id: number; 3 | fullName: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/api/ui/ui.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Req, UseGuards } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { LovService } from '../lov/lov.service'; 4 | import { LovType } from '../lov/enum'; 5 | import { CreateTaskConfig, LoginHeader } from './models'; 6 | import { ProjectService } from '../project/project.service'; 7 | import { TaskService } from '../task/task.service'; 8 | import { AuthGuard } from '@nestjs/passport'; 9 | import { User } from '../user/user.entity'; 10 | import { Project } from '../project/project.entity'; 11 | import { State } from '../task/models'; 12 | import { UserService } from '../user/user.service'; 13 | 14 | @ApiTags('ui') 15 | @Controller('/ui') 16 | export class UiController { 17 | constructor( 18 | private readonly lovService: LovService, 19 | private readonly projectService: ProjectService, 20 | private readonly taskService: TaskService, 21 | private readonly userService: UserService, 22 | ) {} 23 | 24 | @Get('/create-task') 25 | @UseGuards(AuthGuard('jwt')) 26 | async createTask(@Req() req): Promise { 27 | const user = req.user as User; 28 | const priorities = await this.lovService.findAllByType(LovType.TASK_PRIORITY); 29 | const types = await this.lovService.findAllByType(LovType.TASK_TYPE); 30 | const projects = await this.last3Projects(user.id); 31 | const users = await this.userService.findAll(); 32 | 33 | return { 34 | priorities, 35 | types, 36 | projects, 37 | users: users.map(user => ({ 38 | id: user.id, 39 | fullName: `${user.firstName || ''} ${user.lastName || ''} - ${user.email}`, 40 | })), 41 | }; 42 | } 43 | 44 | @Get('/login-header') 45 | @UseGuards(AuthGuard('jwt')) 46 | async loginHeader(@Req() req): Promise { 47 | const user = req.user as User; 48 | const projects = await this.last3Projects(user.id); 49 | const tasks = await this.taskService.paginate({ 50 | options: { 51 | page: 1, 52 | limit: 5, 53 | }, 54 | userId: user.id, 55 | state: State.ASSIGNED, 56 | }); 57 | return { 58 | projects, 59 | tasks: tasks.items, 60 | }; 61 | } 62 | 63 | private async last3Projects(userId): Promise { 64 | const projects = await this.projectService.paginate({ 65 | options: { 66 | limit: 3, 67 | page: 1, 68 | }, 69 | userId, 70 | }); 71 | return projects.items; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/api/ui/ui.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { UiController } from './ui.controller'; 4 | import { LovService } from '../lov/lov.service'; 5 | import { Lov } from '../lov/lov.entity'; 6 | import { ProjectService } from '../project/project.service'; 7 | import { Project } from '../project/project.entity'; 8 | import { SharedModule } from '../../shared'; 9 | import { TaskService } from '../task/task.service'; 10 | import { Task } from '../task/task.entity'; 11 | import { User } from '../user/user.entity'; 12 | import { UserService } from '../user/user.service'; 13 | 14 | @Module({ 15 | imports: [SharedModule, TypeOrmModule.forFeature([Lov, Project, Task, User])], 16 | controllers: [UiController], 17 | providers: [LovService, ProjectService, TaskService, UserService], 18 | }) 19 | export class UiModule {} 20 | -------------------------------------------------------------------------------- /src/api/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { ClassSerializerInterceptor, Controller, Get, UseInterceptors } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { User } from './user.entity'; 4 | import { UserService } from './user.service'; 5 | 6 | @ApiTags('users') 7 | @Controller('/users') 8 | export class UserController { 9 | constructor(private readonly usersService: UserService) {} 10 | 11 | @UseInterceptors(ClassSerializerInterceptor) 12 | @Get() 13 | async findAll(): Promise { 14 | return await this.usersService.findAll(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/api/user/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Exclude } from 'class-transformer'; 2 | import { BeforeInsert, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 3 | import { DateAudit } from '../../shared/entities'; 4 | import * as bcrypt from 'bcrypt'; 5 | 6 | @Entity('users') 7 | export class User extends DateAudit { 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @Column('text') 12 | firstName: string; 13 | 14 | @Column('text') 15 | lastName: string; 16 | 17 | @Column('text') 18 | email: string; 19 | 20 | @Column() 21 | @Exclude() 22 | password: string; 23 | 24 | @Column({ type: 'text', default: 'en-US' }) 25 | language: string; 26 | 27 | @BeforeInsert() 28 | async hashPassword() { 29 | this.password = await bcrypt.hash(this.password, 10); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/api/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { UserController } from './user.controller'; 4 | import { User } from './user.entity'; 5 | import { UserService } from './user.service'; 6 | import { SharedModule } from '../../shared'; 7 | 8 | @Module({ 9 | imports: [SharedModule, TypeOrmModule.forFeature([User])], 10 | controllers: [UserController], 11 | providers: [UserService], 12 | }) 13 | export class UserModule {} 14 | -------------------------------------------------------------------------------- /src/api/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { User } from './user.entity'; 5 | import { RegisterUserDto } from '../auth/dto'; 6 | 7 | @Injectable() 8 | export class UserService { 9 | constructor( 10 | @InjectRepository(User) 11 | private userRepository: Repository, 12 | ) {} 13 | 14 | async create(registerUserDto: RegisterUserDto): Promise { 15 | const entity = Object.assign(new User(), registerUserDto); 16 | return this.userRepository.save(entity); 17 | } 18 | 19 | async findAll(): Promise { 20 | return this.userRepository.find(); 21 | } 22 | 23 | async findOneByEmail(email: string): Promise { 24 | return this.userRepository.findOne({ email }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ApiModule } from './api/api.module'; 3 | import { DatabaseModule } from './database/database.module'; 4 | 5 | @Module({ 6 | imports: [DatabaseModule, ApiModule], 7 | }) 8 | export class AppModule {} 9 | -------------------------------------------------------------------------------- /src/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { NestExpressApplication } from '@nestjs/platform-express'; 4 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 5 | import * as compression from 'compression'; 6 | import * as rateLimit from 'express-rate-limit'; 7 | import * as helmet from 'helmet'; 8 | import * as nocache from 'nocache'; 9 | import { resolve } from 'path'; 10 | 11 | import { AppModule } from './app.module'; 12 | import { CustomValidationPipe } from './shared/pipes'; 13 | 14 | export async function bootstrap() { 15 | const app = await NestFactory.create(AppModule); 16 | 17 | app.enableCors(); 18 | app.use(helmet()); 19 | app.use(nocache()); 20 | app.use(compression()); 21 | app.use( 22 | (rateLimit as any)({ 23 | windowMs: 15 * 60 * 1000, // 15 minutes 24 | max: 100, // limit each IP to 100 requests per windowMs 25 | }), 26 | ); 27 | 28 | app.useGlobalPipes(new CustomValidationPipe()); 29 | app.useStaticAssets(resolve(__dirname, '..', 'resources')); 30 | 31 | const options = new DocumentBuilder() 32 | .build(); 33 | const document = SwaggerModule.createDocument(app, options); 34 | SwaggerModule.setup('docs', app, document); 35 | 36 | const logger = new Logger('bootstrap'); 37 | const port = parseInt(process.env.APP_PORT, 10) 38 | await app.listen(port, () => { 39 | logger.log(`Server is listen on http://localhost:${port}`); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | @Module({ 5 | imports: [TypeOrmModule.forRoot()], 6 | }) 7 | export class DatabaseModule {} 8 | -------------------------------------------------------------------------------- /src/database/seeds/lov.seed.ts: -------------------------------------------------------------------------------- 1 | import { Factory, Seeder } from 'typeorm-seeding'; 2 | import { Connection } from 'typeorm'; 3 | import { LovType, TaskPriority, TaskType } from '../../api/lov/enum'; 4 | import { Lov } from '../../api/lov/lov.entity'; 5 | 6 | export default class LovSeed implements Seeder { 7 | public async run(factory: Factory, connection: Connection): Promise { 8 | await connection 9 | .createQueryBuilder() 10 | .insert() 11 | .into('lovs') 12 | .values([ 13 | // Priorities 14 | { text: 'Low', type: LovType.TASK_PRIORITY, value: TaskPriority.LOW }, 15 | { text: 'Medium', type: LovType.TASK_PRIORITY, value: TaskPriority.MEDIUM }, 16 | { text: 'Urgent', type: LovType.TASK_PRIORITY, value: TaskPriority.URGENT }, 17 | // Types 18 | { text: 'Task', type: LovType.TASK_TYPE, value: TaskType.TASK }, 19 | { text: 'Bug', type: LovType.TASK_TYPE, value: TaskType.BUG }, 20 | { text: 'Story', type: LovType.TASK_TYPE, value: TaskType.STORY }, 21 | ] as Lov[]) 22 | .execute(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrap } from './bootstrap'; 2 | 3 | bootstrap(); 4 | -------------------------------------------------------------------------------- /src/shared/entities/date-audit.entity.ts: -------------------------------------------------------------------------------- 1 | import { BeforeUpdate, Column } from 'typeorm'; 2 | 3 | export abstract class DateAudit { 4 | @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) 5 | createdAt: Date; 6 | 7 | @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) 8 | updatedAt: Date; 9 | 10 | @BeforeUpdate() 11 | updateTimestamp() { 12 | this.updatedAt = new Date(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/shared/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './date-audit.entity'; 2 | -------------------------------------------------------------------------------- /src/shared/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt-auth.guard'; 2 | -------------------------------------------------------------------------------- /src/shared/guards/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class JwtAuthGuard extends AuthGuard('jwt') {} 6 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entities'; 2 | export * from './guards'; 3 | export * from './models'; 4 | export * from './pipes'; 5 | export * from './shared.module'; 6 | -------------------------------------------------------------------------------- /src/shared/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './validation-message-list.model'; 2 | -------------------------------------------------------------------------------- /src/shared/models/validation-message-list.model.ts: -------------------------------------------------------------------------------- 1 | export type ValidationMessageList = Partial<{ 2 | property: string; 3 | error: string; 4 | message: string; 5 | }>; 6 | -------------------------------------------------------------------------------- /src/shared/pipes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './validation.pipe'; 2 | -------------------------------------------------------------------------------- /src/shared/pipes/validation.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common'; 2 | import { validate } from 'class-validator'; 3 | import { plainToClass } from 'class-transformer'; 4 | import { ValidationError } from 'class-validator/types/validation/ValidationError'; 5 | import { ValidationMessageList } from '../models'; 6 | 7 | @Injectable() 8 | export class CustomValidationPipe implements PipeTransform { 9 | async transform(value, metadata: ArgumentMetadata) { 10 | if (!value) { 11 | throw new BadRequestException('No data submitted'); 12 | } 13 | 14 | const { metatype } = metadata; 15 | if (!metatype || !this.toValidate(metatype)) { 16 | return value; 17 | } 18 | const object = plainToClass(metatype, value); 19 | const errors = await validate(object); 20 | if (errors.length > 0) { 21 | throw new BadRequestException(this.buildError(errors)); 22 | } 23 | return value; 24 | } 25 | 26 | private buildError(errors: ValidationError[]): ValidationMessageList[] { 27 | return errors 28 | .map((error) => 29 | Object.entries(error.constraints).map((item) => ({ 30 | property: error.property, 31 | error: item[0], 32 | message: item[1], 33 | })), 34 | ) 35 | .reduce((acc, item) => [...acc, ...item], []); 36 | } 37 | 38 | private toValidate(metatype): boolean { 39 | const types = [String, Boolean, Number, Array, Object]; 40 | return !types.find((type) => metatype === type); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | @Module({ 4 | imports: [], 5 | exports: [], 6 | providers: [], 7 | }) 8 | export class SharedModule {} 9 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "declaration": true, 6 | "removeComments": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "es2017", 11 | "sourceMap": false, 12 | "outDir": "./dist", 13 | "baseUrl": "./", 14 | "incremental": true 15 | }, 16 | "exclude": ["node_modules", "tmp"] 17 | } 18 | --------------------------------------------------------------------------------