├── .env ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── Procfile ├── README.md ├── custom.d.ts ├── docker-compose.yml ├── nest-cli.json ├── package.json ├── prisma ├── migrations │ ├── 20220803011804_v0 │ │ └── migration.sql │ ├── 20220803232350_v1 │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── src ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── auth │ ├── auth.controller.ts │ ├── auth.module.ts │ ├── auth.service.ts │ ├── dto │ │ └── auth.dto.ts │ ├── interfaces │ │ └── auth.interface.ts │ └── strategy │ │ └── jwt.strategy.ts ├── main.ts ├── prisma │ ├── prisma.module.ts │ └── prisma.service.ts ├── todo │ ├── dto │ │ ├── create-task.dto.ts │ │ └── update-task.dto.ts │ ├── todo.controller.ts │ ├── todo.module.ts │ └── todo.service.ts └── user │ ├── dto │ └── update-user.dto.ts │ ├── user.controller.ts │ ├── user.module.ts │ └── user.service.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | # DATABASE_URL="postgresql://udemy:udemy@localhost:5434/udemy?schema=public" 8 | DATABASE_URL="postgres://tkawcvceqtdlrt:b63809a94a8d366a92eca1d489dbc43bcc7a58fd3fcdabf9ff088b595ea97ada@ec2-44-205-112-253.compute-1.amazonaws.com:5432/dbnuh1vqlb9grs" 9 | SHADOW_DATABASE_URL="postgres://kyrhydpwvpavke:7917c48b5ad1e3cfc294df930e053075270752c19bd13c1ea6fd31280722735c@ec2-44-205-112-253.compute-1.amazonaws.com:5432/dfdm5lo7eed2pb" 10 | 11 | 12 | JWT_SECRET='mSSS9Zrd' -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 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 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run start:prod 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Create new NestJS project 2 | ```bash 3 | $ npm i -g @nestjs/cli 4 | $ npm i -g yarn 5 | 6 | $ nest new api-lesson 7 | # set strict true in tsconfig.json 8 | ``` 9 | ### Install packages 10 | ```bash 11 | # install prisma 12 | $ yarn add -D prisma 13 | $ yarn add @prisma/client 14 | $ npx prisma init 15 | 16 | # add docker-compose.yml file 17 | # start db 18 | $ docker compose up -d 19 | # reset db 20 | $ docker compose rm -s -f -v 21 | 22 | # edit DATABASE_URL of .env 23 | # add model definition to schema file 24 | 25 | # prisma migrate and type generation 26 | $ npx prisma migrate dev 27 | $ npx prisma studio 28 | $ npx prisma generate 29 | 30 | # install packages 31 | $ yarn add @nestjs/config @nestjs/jwt @nestjs/passport 32 | $ yarn add cookie-parser csurf passport passport-jwt bcrypt class-validator 33 | $ yarn add -D @types/express @types/cookie-parser @types/csurf @types/passport-jwt @types/bcrypt 34 | ``` 35 | ### Create module, controller, service 36 | ```bash 37 | $ nest g module auth 38 | $ nest g module user 39 | $ nest g module todo 40 | $ nest g module prisma 41 | $ nest g controller auth --no-spec 42 | $ nest g controller user --no-spec 43 | $ nest g controller todo --no-spec 44 | $ nest g service auth --no-spec 45 | $ nest g service user --no-spec 46 | $ nest g service todo --no-spec 47 | $ nest g service prisma --no-spec 48 | ``` 49 | 50 | ### Deploy to Heroku 51 | create Procfile 52 | ```bash 53 | web: npm run start:prod 54 | ``` 55 | create new heroku app 56 | ```bash 57 | # add config vars 58 | JWT_SECRET : yours 59 | # Add two Heroku Postgres in add-on 60 | heroku git:remote -a yours 61 | git push heroku main 62 | # edit .env file 63 | DATABASE_URL=yours 64 | SHADOW_DATABASE_URL=yours 65 | # prisma migrate 66 | npx prisma migrate deploy 67 | npx prisma studio 68 | # edit .env.local of Next.js 69 | NEXT_PUBLIC_API_URL=https://yours.herokuapp.com 70 | # deploy to Vercel 71 | # add Vercel domain to cors middleware in NestJS 72 | ``` 73 | -------------------------------------------------------------------------------- /custom.d.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@prisma/client'; 2 | 3 | declare module 'express-serve-static-core' { 4 | interface Request { 5 | user?: Omit; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | dev-postgres: 4 | image: postgres:14.4-alpine 5 | ports: 6 | - 5434:5432 7 | environment: 8 | POSTGRES_USER: udemy 9 | POSTGRES_PASSWORD: udemy 10 | POSTGRES_DB: udemy 11 | restart: always 12 | networks: 13 | - lesson 14 | networks: 15 | lesson: 16 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-lesson", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^9.0.0", 25 | "@nestjs/config": "^2.2.0", 26 | "@nestjs/core": "^9.0.0", 27 | "@nestjs/jwt": "^9.0.0", 28 | "@nestjs/passport": "^9.0.0", 29 | "@nestjs/platform-express": "^9.0.0", 30 | "@prisma/client": "^4.1.1", 31 | "bcrypt": "^5.0.1", 32 | "class-transformer": "^0.5.1", 33 | "class-validator": "^0.13.2", 34 | "cookie-parser": "^1.4.6", 35 | "csurf": "^1.11.0", 36 | "passport": "^0.6.0", 37 | "passport-jwt": "^4.0.0", 38 | "reflect-metadata": "^0.1.13", 39 | "rimraf": "^3.0.2", 40 | "rxjs": "^7.2.0" 41 | }, 42 | "devDependencies": { 43 | "@nestjs/cli": "^9.0.0", 44 | "@nestjs/schematics": "^9.0.0", 45 | "@nestjs/testing": "^9.0.0", 46 | "@types/bcrypt": "^5.0.0", 47 | "@types/cookie-parser": "^1.4.3", 48 | "@types/csurf": "^1.11.2", 49 | "@types/express": "^4.17.13", 50 | "@types/jest": "28.1.4", 51 | "@types/node": "^16.0.0", 52 | "@types/passport-jwt": "^3.0.6", 53 | "@types/supertest": "^2.0.11", 54 | "@typescript-eslint/eslint-plugin": "^5.0.0", 55 | "@typescript-eslint/parser": "^5.0.0", 56 | "eslint": "^8.0.1", 57 | "eslint-config-prettier": "^8.3.0", 58 | "eslint-plugin-prettier": "^4.0.0", 59 | "jest": "28.1.2", 60 | "prettier": "^2.3.2", 61 | "prisma": "^4.1.1", 62 | "source-map-support": "^0.5.20", 63 | "supertest": "^6.1.3", 64 | "ts-jest": "28.0.5", 65 | "ts-loader": "^9.2.3", 66 | "ts-node": "^10.0.0", 67 | "tsconfig-paths": "4.0.0", 68 | "typescript": "^4.3.5" 69 | }, 70 | "jest": { 71 | "moduleFileExtensions": [ 72 | "js", 73 | "json", 74 | "ts" 75 | ], 76 | "rootDir": "src", 77 | "testRegex": ".*\\.spec\\.ts$", 78 | "transform": { 79 | "^.+\\.(t|j)s$": "ts-jest" 80 | }, 81 | "collectCoverageFrom": [ 82 | "**/*.(t|j)s" 83 | ], 84 | "coverageDirectory": "../coverage", 85 | "testEnvironment": "node" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /prisma/migrations/20220803011804_v0/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" SERIAL NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" TIMESTAMP(3) NOT NULL, 6 | "email" TEXT NOT NULL, 7 | "hashedPassword" TEXT NOT NULL, 8 | 9 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- CreateTable 13 | CREATE TABLE "Task" ( 14 | "id" SERIAL NOT NULL, 15 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 16 | "updatedAt" TIMESTAMP(3) NOT NULL, 17 | "title" TEXT NOT NULL, 18 | "description" TEXT, 19 | "userId" INTEGER NOT NULL, 20 | 21 | CONSTRAINT "Task_pkey" PRIMARY KEY ("id") 22 | ); 23 | 24 | -- CreateIndex 25 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 26 | 27 | -- AddForeignKey 28 | ALTER TABLE "Task" ADD CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 29 | -------------------------------------------------------------------------------- /prisma/migrations/20220803232350_v1/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "nickName" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model User { 14 | id Int @id @default(autoincrement()) 15 | createdAt DateTime @default(now()) 16 | updatedAt DateTime @updatedAt 17 | email String @unique 18 | hashedPassword String 19 | nickName String? 20 | tasks Task[] 21 | } 22 | 23 | model Task { 24 | id Int @id @default(autoincrement()) 25 | createdAt DateTime @default(now()) 26 | updatedAt DateTime @updatedAt 27 | title String 28 | description String? 29 | userId Int 30 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 31 | } -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } 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 | getHello(): string { 10 | return this.appService.getHello(); 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 { AuthModule } from './auth/auth.module'; 5 | import { UserModule } from './user/user.module'; 6 | import { TodoModule } from './todo/todo.module'; 7 | import { PrismaModule } from './prisma/prisma.module'; 8 | import { ConfigModule } from '@nestjs/config'; 9 | 10 | @Module({ 11 | imports: [ 12 | ConfigModule.forRoot({ isGlobal: true }), 13 | AuthModule, 14 | UserModule, 15 | TodoModule, 16 | PrismaModule, 17 | ], 18 | controllers: [AppController], 19 | providers: [AppService], 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Post, 4 | Body, 5 | HttpCode, 6 | HttpStatus, 7 | Res, 8 | Req, 9 | Get, 10 | } from '@nestjs/common'; 11 | import { Request, Response } from 'express'; 12 | import { AuthService } from './auth.service'; 13 | import { AuthDto } from './dto/auth.dto'; 14 | import { Csrf, Msg } from './interfaces/auth.interface'; 15 | 16 | @Controller('auth') 17 | export class AuthController { 18 | constructor(private readonly authService: AuthService) {} 19 | 20 | @Get('/csrf') 21 | getCsrfToken(@Req() req: Request): Csrf { 22 | return { csrfToken: req.csrfToken() }; 23 | } 24 | 25 | @Post('signup') 26 | signUp(@Body() dto: AuthDto): Promise { 27 | return this.authService.signUp(dto); 28 | } 29 | 30 | @HttpCode(HttpStatus.OK) 31 | @Post('login') 32 | async login( 33 | @Body() dto: AuthDto, 34 | @Res({ passthrough: true }) res: Response, 35 | ): Promise { 36 | const jwt = await this.authService.login(dto); 37 | res.cookie('access_token', jwt.accessToken, { 38 | httpOnly: true, 39 | secure: true, 40 | sameSite: 'none', 41 | path: '/', 42 | }); 43 | return { 44 | message: 'ok', 45 | }; 46 | } 47 | 48 | @HttpCode(HttpStatus.OK) 49 | @Post('/logout') 50 | logout(@Req() req: Request, @Res({ passthrough: true }) res: Response): Msg { 51 | res.cookie('access_token', '', { 52 | httpOnly: true, 53 | secure: true, 54 | sameSite: 'none', 55 | path: '/', 56 | }); 57 | return { 58 | message: 'ok', 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PrismaModule } from 'src/prisma/prisma.module'; 3 | import { AuthController } from './auth.controller'; 4 | import { AuthService } from './auth.service'; 5 | import { JwtModule } from '@nestjs/jwt'; 6 | import { JwtStrategy } from './strategy/jwt.strategy'; 7 | 8 | @Module({ 9 | imports: [PrismaModule, JwtModule.register({})], 10 | controllers: [AuthController], 11 | providers: [AuthService, JwtStrategy], 12 | }) 13 | export class AuthModule {} 14 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ForbiddenException } from '@nestjs/common'; 2 | import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { JwtService } from '@nestjs/jwt'; 5 | import * as bcrypt from 'bcrypt'; 6 | import { PrismaService } from '../prisma/prisma.service'; 7 | import { AuthDto } from './dto/auth.dto'; 8 | import { Msg, Jwt } from './interfaces/auth.interface'; 9 | 10 | @Injectable() 11 | export class AuthService { 12 | constructor( 13 | private readonly prisma: PrismaService, 14 | private readonly jwt: JwtService, 15 | private readonly config: ConfigService, 16 | ) {} 17 | async signUp(dto: AuthDto): Promise { 18 | const hashed = await bcrypt.hash(dto.password, 12); 19 | try { 20 | await this.prisma.user.create({ 21 | data: { 22 | email: dto.email, 23 | hashedPassword: hashed, 24 | }, 25 | }); 26 | return { 27 | message: 'ok', 28 | }; 29 | } catch (error) { 30 | if (error instanceof PrismaClientKnownRequestError) { 31 | if (error.code === 'P2002') { 32 | throw new ForbiddenException('This email is already taken'); 33 | } 34 | } 35 | throw error; 36 | } 37 | } 38 | async login(dto: AuthDto): Promise { 39 | const user = await this.prisma.user.findUnique({ 40 | where: { 41 | email: dto.email, 42 | }, 43 | }); 44 | if (!user) throw new ForbiddenException('Email or password incorrect'); 45 | const isValid = await bcrypt.compare(dto.password, user.hashedPassword); 46 | if (!isValid) throw new ForbiddenException('Email or password incorrect'); 47 | return this.generateJwt(user.id, user.email); 48 | } 49 | 50 | async generateJwt(userId: number, email: string): Promise { 51 | const payload = { 52 | sub: userId, 53 | email, 54 | }; 55 | const secret = this.config.get('JWT_SECRET'); 56 | const token = await this.jwt.signAsync(payload, { 57 | expiresIn: '5m', 58 | secret: secret, 59 | }); 60 | return { 61 | accessToken: token, 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/auth/dto/auth.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; 2 | export class AuthDto { 3 | @IsEmail() 4 | @IsNotEmpty() 5 | email: string; 6 | 7 | @IsString() 8 | @IsNotEmpty() 9 | @MinLength(5) 10 | password: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/auth/interfaces/auth.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Msg { 2 | message: string; 3 | } 4 | export interface Csrf { 5 | csrfToken: string; 6 | } 7 | export interface Jwt { 8 | accessToken: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/auth/strategy/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { PassportStrategy } from '@nestjs/passport'; 4 | import { ExtractJwt, Strategy } from 'passport-jwt'; 5 | import { PrismaService } from '../../prisma/prisma.service'; 6 | 7 | @Injectable() 8 | export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { 9 | constructor( 10 | private readonly config: ConfigService, 11 | private readonly prisma: PrismaService, 12 | ) { 13 | super({ 14 | jwtFromRequest: ExtractJwt.fromExtractors([ 15 | (req) => { 16 | let jwt = null; 17 | if (req && req.cookies) { 18 | jwt = req.cookies['access_token']; 19 | } 20 | return jwt; 21 | }, 22 | ]), 23 | ignoreExpiration: false, 24 | secretOrKey: config.get('JWT_SECRET'), 25 | }); 26 | } 27 | 28 | async validate(payload: { sub: number; email: string }) { 29 | const user = await this.prisma.user.findUnique({ 30 | where: { 31 | id: payload.sub, 32 | }, 33 | }); 34 | delete user.hashedPassword; 35 | return user; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ValidationPipe } from '@nestjs/common'; 4 | import { Request } from 'express'; 5 | import * as cookieParser from 'cookie-parser'; 6 | import * as csurf from 'csurf'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(AppModule); 10 | app.useGlobalPipes(new ValidationPipe({ whitelist: true })); 11 | app.enableCors({ 12 | credentials: true, 13 | origin: [ 14 | 'http://localhost:3000', 15 | 'https://frontend-todo-nextjs.vercel.app', 16 | ], 17 | }); 18 | app.use(cookieParser()); 19 | app.use( 20 | csurf({ 21 | cookie: { 22 | httpOnly: true, 23 | sameSite: 'none', 24 | secure: true, 25 | }, 26 | value: (req: Request) => { 27 | return req.header('csrf-token'); 28 | }, 29 | }), 30 | ); 31 | await app.listen(process.env.PORT || 3005); 32 | } 33 | bootstrap(); 34 | -------------------------------------------------------------------------------- /src/prisma/prisma.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PrismaService } from './prisma.service'; 3 | 4 | @Module({ 5 | providers: [PrismaService], 6 | exports: [PrismaService], 7 | }) 8 | export class PrismaModule {} 9 | -------------------------------------------------------------------------------- /src/prisma/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { PrismaClient } from '@prisma/client'; 4 | 5 | @Injectable() 6 | export class PrismaService extends PrismaClient { 7 | constructor(private readonly config: ConfigService) { 8 | super({ 9 | datasources: { 10 | db: { 11 | url: config.get('DATABASE_URL'), 12 | }, 13 | }, 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/todo/dto/create-task.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; 2 | 3 | export class CreateTaskDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | title: string; 7 | 8 | @IsString() 9 | @IsOptional() 10 | description?: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/todo/dto/update-task.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, IsString, IsNotEmpty } from 'class-validator'; 2 | 3 | export class UpdateTaskDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | title: string; 7 | 8 | @IsString() 9 | @IsOptional() 10 | description?: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/todo/todo.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | HttpCode, 7 | HttpStatus, 8 | Param, 9 | ParseIntPipe, 10 | Patch, 11 | Post, 12 | UseGuards, 13 | Req, 14 | } from '@nestjs/common'; 15 | import { AuthGuard } from '@nestjs/passport'; 16 | import { Request } from 'express'; 17 | import { TodoService } from './todo.service'; 18 | import { CreateTaskDto } from './dto/create-task.dto'; 19 | import { UpdateTaskDto } from './dto/update-task.dto'; 20 | import { Task } from '@prisma/client'; 21 | 22 | @UseGuards(AuthGuard('jwt')) 23 | @Controller('todo') 24 | export class TodoController { 25 | constructor(private readonly todoService: TodoService) {} 26 | 27 | @Get() 28 | getTasks(@Req() req: Request): Promise { 29 | return this.todoService.getTasks(req.user.id); 30 | } 31 | 32 | @Get(':id') 33 | getTaskById( 34 | @Req() req: Request, 35 | @Param('id', ParseIntPipe) taskId: number, 36 | ): Promise { 37 | return this.todoService.getTaskById(req.user.id, taskId); 38 | } 39 | 40 | @Post() 41 | createTask(@Req() req: Request, @Body() dto: CreateTaskDto): Promise { 42 | return this.todoService.createTask(req.user.id, dto); 43 | } 44 | 45 | @Patch(':id') 46 | updateTaskById( 47 | @Req() req: Request, 48 | @Param('id', ParseIntPipe) taskId: number, 49 | @Body() dto: UpdateTaskDto, 50 | ): Promise { 51 | return this.todoService.updateTaskById(req.user.id, taskId, dto); 52 | } 53 | 54 | @HttpCode(HttpStatus.NO_CONTENT) 55 | @Delete(':id') 56 | deleteTaskById( 57 | @Req() req: Request, 58 | @Param('id', ParseIntPipe) taskId: number, 59 | ): Promise { 60 | return this.todoService.deleteTaskById(req.user.id, taskId); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/todo/todo.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PrismaModule } from 'src/prisma/prisma.module'; 3 | import { TodoController } from './todo.controller'; 4 | import { TodoService } from './todo.service'; 5 | 6 | @Module({ 7 | imports: [PrismaModule], 8 | controllers: [TodoController], 9 | providers: [TodoService], 10 | }) 11 | export class TodoModule {} 12 | -------------------------------------------------------------------------------- /src/todo/todo.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ForbiddenException } from '@nestjs/common'; 2 | import { PrismaService } from '../prisma/prisma.service'; 3 | import { CreateTaskDto } from './dto/create-task.dto'; 4 | import { UpdateTaskDto } from './dto/update-task.dto'; 5 | import { Task } from '@prisma/client'; 6 | 7 | @Injectable() 8 | export class TodoService { 9 | constructor(private prisma: PrismaService) {} 10 | 11 | getTasks(userId: number): Promise { 12 | return this.prisma.task.findMany({ 13 | where: { 14 | userId, 15 | }, 16 | orderBy: { 17 | createdAt: 'desc', 18 | }, 19 | }); 20 | } 21 | getTaskById(userId: number, taskId: number): Promise { 22 | return this.prisma.task.findFirst({ 23 | where: { 24 | userId, 25 | id: taskId, 26 | }, 27 | }); 28 | } 29 | 30 | async createTask(userId: number, dto: CreateTaskDto): Promise { 31 | const task = await this.prisma.task.create({ 32 | data: { 33 | userId, 34 | ...dto, 35 | }, 36 | }); 37 | return task; 38 | } 39 | 40 | async updateTaskById( 41 | userId: number, 42 | taskId: number, 43 | dto: UpdateTaskDto, 44 | ): Promise { 45 | const task = await this.prisma.task.findUnique({ 46 | where: { 47 | id: taskId, 48 | }, 49 | }); 50 | 51 | if (!task || task.userId !== userId) 52 | throw new ForbiddenException('No permission to update'); 53 | 54 | return this.prisma.task.update({ 55 | where: { 56 | id: taskId, 57 | }, 58 | data: { 59 | ...dto, 60 | }, 61 | }); 62 | } 63 | async deleteTaskById(userId: number, taskId: number): Promise { 64 | const task = await this.prisma.task.findUnique({ 65 | where: { 66 | id: taskId, 67 | }, 68 | }); 69 | 70 | if (!task || task.userId !== userId) 71 | throw new ForbiddenException('No permission to delete'); 72 | 73 | await this.prisma.task.delete({ 74 | where: { 75 | id: taskId, 76 | }, 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/user/dto/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, IsString } from 'class-validator'; 2 | 3 | export class UpdateUserDto { 4 | @IsString() 5 | @IsOptional() 6 | nickName?: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Patch, Req, UseGuards } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { Request } from 'express'; 4 | import { UserService } from './user.service'; 5 | import { UpdateUserDto } from './dto/update-user.dto'; 6 | import { User } from '@prisma/client'; 7 | 8 | @UseGuards(AuthGuard('jwt')) 9 | @Controller('user') 10 | export class UserController { 11 | constructor(private readonly userService: UserService) {} 12 | 13 | @Get() 14 | getLoginUser(@Req() req: Request): Omit { 15 | return req.user; 16 | } 17 | 18 | @Patch() 19 | updateUser( 20 | @Req() req: Request, 21 | @Body() dto: UpdateUserDto, 22 | ): Promise> { 23 | return this.userService.updateUser(req.user.id, dto); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PrismaModule } from 'src/prisma/prisma.module'; 3 | import { UserController } from './user.controller'; 4 | import { UserService } from './user.service'; 5 | 6 | @Module({ 7 | imports: [PrismaModule], 8 | controllers: [UserController], 9 | providers: [UserService], 10 | }) 11 | export class UserModule {} 12 | -------------------------------------------------------------------------------- /src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PrismaService } from '../prisma/prisma.service'; 3 | import { UpdateUserDto } from './dto/update-user.dto'; 4 | import { User } from '@prisma/client'; 5 | 6 | @Injectable() 7 | export class UserService { 8 | constructor(private prisma: PrismaService) {} 9 | 10 | async updateUser( 11 | userId: number, 12 | dto: UpdateUserDto, 13 | ): Promise> { 14 | const user = await this.prisma.user.update({ 15 | where: { 16 | id: userId, 17 | }, 18 | data: { 19 | ...dto, 20 | }, 21 | }); 22 | delete user.hashedPassword; 23 | return user; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": true, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false, 20 | "strict": true 21 | } 22 | } 23 | --------------------------------------------------------------------------------