nestjs_auth init..

This commit is contained in:
최준흠 2022-09-01 15:56:22 +09:00
commit 881d4dee74
43 changed files with 16193 additions and 0 deletions

16
.env Normal file
View File

@ -0,0 +1,16 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="mysql://root:@localhost:3306/test"
CORS_ALLOW_ORIGINS = ['http://localhost:8080']
CORS_ALLOW_METHOD = "GET,PUT,POST,DELETE,PATCH,OPTIONS"
JWT_SECURITY_KEY = "security_key"
JWT_EXPIRE_MAX = "600s"
AUTH_USERNAME_FIELD="email"
DEFAULT_TABLE_PERPAGE = 10
DEFAULT_TABLE_PAGE = 1

25
.eslintrc.js Normal file
View File

@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir : __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

35
.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

8
.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"semi": false,
"bracketSapcing": true,
"singleQuote": true,
"useTabs": false,
"trailingComma": "none",
"printWith": 80
}

114
README.md Normal file
View File

@ -0,0 +1,114 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).
## 추가 설치과정
.prettierrc
{
"semi": false,
"bracketSapcing": true,
"singleQuote": true,
"useTabs": false,
"trailingComma": "none",
"printWith": 80
}
참조:https://www.bottlehs.com/vue/vue-js-jwt-기반-사용자-인증/
JWT ( JSON WEB TOKEN ) 이란?
JWT 는 JSON Web Token의 약자로 전자 서명 된 URL-safe (URL로 이용할 수있는 문자 만 구성된)의 JSON이다. 전자 서명은 JSON 의 변조를 체크 할 수 있게되어 있다. JWT는 속성 정보 (Claim)를 JSON 데이터 구조로 표현한 토큰으로 RFC7519 표준 이다. JWT는 서버와 클라이언트 간 정보를 주고 받을 때 Http 리퀘스트 헤더에 JSON 토큰을 넣은 후 서버는 별도의 인증 과정없이 헤더에 포함되어 있는 JWT 정보를 통해 인증한다. 이때 사용되는 JSON 데이터는 URL-Safe 하도록 URL에 포함할 수 있는 문자만으로 만든다. JWT는 HMAC 알고리즘을 사용하여 비밀키 또는 RSA를 이용한 Public Key/ Private Key 쌍으로 서명할 수 있다
토큰 구성
JWT 토큰 구성
JWT는 세 파트로 나누어지며, 각 파트는 점로 구분하여 xxxxx.yyyyy.zzzzz 이런식으로 표현된다. 순서대로 헤더 (Header), 페이로드 (Payload), 서명 (Sinature)로 구성한다. Base64 인코딩의 경우 “+”, “/”, “=”이 포함되지만 JWT는 URI에서 파라미터로 사용할 수 있도록 URL-Safe 한 Base64url 인코딩을 사용한다. Header는 토큰의 타입과 해시 암호화 알고리즘으로 구성되어 있습니다. 첫째는 토큰의 유형 (JWT)을 나타내고, 두 번째는 HMAC, SHA256 또는 RSA와 같은 해시 알고리즘을 나타내는 부분이다. Payload는 토큰에 담을 클레임(claim) 정보를 포함하고 있다. Payload 에 담는 정보의 한 ‘조각’ 을 클레임이라고 부르고, 이는 name / value 의 한 쌍으로 이뤄져있다. 토큰에는 여러개의 클레임 들을 넣을 수 있다. 클레임의 정보는 등록된 (registered) 클레임, 공개 (public) 클레임, 비공개 (private) 클레임으로 세 종류가 있다. 마지막으로 Signature는 secret key를 포함하여 암호화되어 있다.
1. npm i --save @nestjs/jwt
"@nestjs/jwt": "^9.0.0",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0"
2. npm install @nestjs/passport passport passport-jwt passport-local
"@nestjs/passport": "^9.0.0",
"passport": "^0.6.0",
3. npm install prisma --save-dev
npm install @prisma/client
--> npx prisma init
schema.prisma에 필요한 모델 생성
--> npx prisma generate
--> npx prisma studio => 임시로 DB확인할때 사용(localhost:555)
schema.prisma에 필요한 DB Table 생성
--> npx prisma migrate deploy [dev|reset|deploy|status]
4. CORS-enabled
npm install cors
npm install -g webpack webpack-cli

5
nest-cli.json Normal file
View File

@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}

15058
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

78
package.json Normal file
View File

@ -0,0 +1,78 @@
{
"name": "nestjs_auth",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^9.0.0",
"@nestjs/core": "^9.0.0",
"@nestjs/jwt": "^9.0.0",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"@prisma/client": "^4.3.0",
"cors": "^2.8.5",
"passport": "^0.6.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@types/express": "^4.17.13",
"@types/jest": "28.1.4",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "28.1.2",
"prettier": "^2.3.2",
"prisma": "^4.3.0",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "28.0.5",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.0.0",
"typescript": "^4.3.5"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

30
prisma.service.ts Normal file
View File

@ -0,0 +1,30 @@
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
constructor() {
//SQL 로그를 출력하기위해 추가
super({
log: [
{ emit: 'event', level: 'query' },
{ emit: 'stdout', level: 'info' },
{ emit: 'stdout', level: 'warn' },
{ emit: 'stdout', level: 'error' }
],
errorFormat: 'colorless'
})
}
async onModuleInit() {
await this.$connect()
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async (event) => {
//SQL 로그를 출력하기위해 추가
console.log(event.name)
await app.close()
})
}
}

View File

@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE `User` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`email` VARCHAR(191) NOT NULL,
`password` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`role` VARCHAR(191) NOT NULL DEFAULT 'USER',
`is_done` BOOLEAN NULL DEFAULT false,
`updatedAt` TIMESTAMP(0) NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE INDEX `User_email_key`(`email`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "mysql"

23
prisma/schema.prisma Normal file
View File

@ -0,0 +1,23 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
//만들려는 모델
model User {
id Int @id @default(autoincrement())
email String @unique
password String
name String
role String @default("USER")
is_done Boolean? @default(false)
updatedAt DateTime? @db.Timestamp(0)
createdAt DateTime @default(now())
}

56
prisma/seed.ts Normal file
View File

@ -0,0 +1,56 @@
import { Prisma, PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
//필요 설치 : npm install -D typescript ts-node @types/node
//Project 디렉토리에서 실행: npx prisma db seed
const userDatas: Prisma.UserCreateInput[] = [
{
email: 'choi.jh@idcjp.jp',
name: '최준흠',
password: '1234',
role: 'ADMIN'
},
{
email: 'user1@idcjp.jp',
name: '사용자1',
password: '1234',
role: 'USER'
},
{
email: 'user2@idcjp.jp',
name: '사용자2',
password: '1234',
role: 'USER'
},
{
email: 'user3@idcjp.jp',
name: '사용자3',
password: '1234',
role: 'USER'
}
]
const transferUser = async () => {
const users = []
for (const userData of userDatas) {
const user = prisma.user.create({ data: userData })
users.push(user)
}
return await prisma.$transaction(users)
}
const main = async () => {
console.log(`Start User seeding ...`)
await transferUser()
console.log(`Seeding User finished.`)
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})

View File

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing'
import { AppController } from './app.controller'
import { AppService } from './app.service'
describe('AppController', () => {
let appController: AppController
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService]
}).compile()
appController = app.get<AppController>(AppController)
})
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!')
})
})
})

12
src/app.controller.ts Normal file
View File

@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common'
import { AppService } from './app.service'
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello()
}
}

12
src/app.module.ts Normal file
View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { AuthModule } from './auth/auth.module'
import { UsersModule } from './user/user.module'
@Module({
imports: [AuthModule, UsersModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {}

8
src/app.service.ts Normal file
View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common'
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!'
}
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'
import { AuthController } from './auth.controller'
describe('AuthController', () => {
let controller: AuthController
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController]
}).compile()
controller = module.get<AuthController>(AuthController)
})
it('should be defined', () => {
expect(controller).toBeDefined()
})
})

View File

@ -0,0 +1,36 @@
import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common'
import { AuthService } from './auth.service'
import { JwtAuthGuard } from './guards/jwt.authguard'
import { LocalAuthGuard } from './guards/local-auth.guard'
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
//local.strategy.ts 사용
// @UseGuards(AuthGuard('local'))
// @UseGuards(LocalAuthGuard)
// @Post('login')
// async login(@Request() req) {
// return req.user
// }
//Login용
//local-auth.guard.ts 사용
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Request() req) {
console.log(req.user)
const response = this.authService.login(req.user)
console.log(response)
return response
}
//Login여부 확인용
@UseGuards(JwtAuthGuard)
@Get('islogin')
getProfile(@Request() req) {
//console.log(req)
return req.user
}
}

27
src/auth/auth.module.ts Normal file
View File

@ -0,0 +1,27 @@
//참고 : https://velog.io/@junguksim/NestJS-노트-3-Authentication
// https://docs.nestjs.com/security/authorization
import { Module } from '@nestjs/common'
import { PassportModule } from '@nestjs/passport'
import { UsersModule } from 'src/user/user.module'
import { AuthService } from './auth.service'
import { JwtModule } from '@nestjs/jwt'
import { jwtConstants } from './guards/constants'
import { AuthController } from './auth.controller'
import { LocalStrategy } from './guards/local.strategy'
import { JwtStrategy } from './guards/jwt.strategy'
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: jwtConstants.expiresIn }
})
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
controllers: [AuthController]
})
export class AuthModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'
import { AuthService } from './auth.service'
describe('AuthService', () => {
let service: AuthService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService]
}).compile()
service = module.get<AuthService>(AuthService)
})
it('should be defined', () => {
expect(service).toBeDefined()
})
})

36
src/auth/auth.service.ts Normal file
View File

@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common'
import { UserService } from 'src/user/user.service'
import { JwtService } from '@nestjs/jwt'
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService
) {}
//app.controller.ts에서 @UseGuards(AuthGuard('local'))용
async validateUser(email: string, password: string): Promise<any> {
const user = await this.userService.fetchOneByEmail(email)
if (user && user.password === password) {
const { password, ...result } = user
// result는 password 를 제외한 user의 모든 정보를 포함한다.
//console.log(result)
console.log(password)
return result
}
return null
}
async login(user: any) {
//console.log(user)
const payload = {
id: user.id,
email: user.email,
name: user.name,
roles: [user.role]
}
// console.log(payload)
return { access_token: this.jwtService.sign(payload) }
}
}

View File

@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common'
import { Role } from '../guards/role.enum'
export const HasRoles = (...roles: Role[]) => SetMetadata('has-roles', roles)

View File

@ -0,0 +1,6 @@
import { env } from 'process'
export const jwtConstants = {
secret: env.JWT_SECURITY_KEY,
expiresIn: env.JWT_EXPIRE_MAX
}

View File

@ -0,0 +1,24 @@
import {
ExecutionContext,
Injectable,
UnauthorizedException
} from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
// Add your custom authentication logic here
// for example, call super.logIn(request) to establish a session.
return super.canActivate(context)
}
handleRequest(err, user, info) {
// You can throw an exception based on either "info" or "err" arguments
if (err || !user) {
throw err || new UnauthorizedException()
}
console.log(info)
return user
}
}

View File

@ -0,0 +1,24 @@
import { ExtractJwt, Strategy } from 'passport-jwt'
import { PassportStrategy } from '@nestjs/passport'
import { Injectable } from '@nestjs/common'
import { jwtConstants } from './constants'
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret
})
}
async validate(payload: any) {
return {
id: payload.id,
email: payload.email,
name: payload.name,
roles: payload.roles
}
}
}

View File

@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

View File

@ -0,0 +1,22 @@
import { Strategy } from 'passport-local'
import { PassportStrategy } from '@nestjs/passport'
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { AuthService } from 'src/auth/auth.service'
import { env } from 'process'
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
//super()
//If you want to check user authenticate with custom column like 'email', try pass it.
super({ usernameField: env.AUTH_USERNAME_FIELD })
}
async validate(email: string, password: string): Promise<any> {
const user = await this.authService.validateUser(email, password)
if (!user) {
throw new UnauthorizedException()
}
return user
}
}

View File

@ -0,0 +1,4 @@
export enum Role {
USER = 'USER',
ADMIN = 'ADMIN'
}

View File

@ -0,0 +1,7 @@
import { RolesGuard } from './roles.guard'
describe('RolesGuard', () => {
it('should be defined', () => {
expect(new RolesGuard()).toBeDefined()
})
})

View File

@ -0,0 +1,27 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { Observable } from 'rxjs'
import { Role } from './role.enum'
//참고: https://shpota.com/2022/07/16/role-based-authorization-with-jwt-using-nestjs.html
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(
'has-roles',
[context.getHandler(), context.getClass()]
)
if (!requiredRoles) {
return true
}
const { user } = context.switchToHttp().getRequest()
//console.log(requiredRoles)
//console.log(user)
return requiredRoles.some((role) => user?.roles?.includes(role))
//return true
}
}

35
src/main.ts Normal file
View File

@ -0,0 +1,35 @@
import { HttpException, HttpStatus } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
//Enable All CORS Requests : https://docs.nestjs.com/security/cors
app.enableCors({
origin: (origin, callback) => {
//origin URL이 허용된 경우 또는 orgin자체가 없는 경우(postman tool) 통과
if (process.env.CORS_ALLOW_ORIGINS.indexOf(origin) !== -1 || !origin) {
console.log('Allowed Origin URL: ' + origin)
callback(null, true)
} else {
callback(
new HttpException(
{
status: HttpStatus.FORBIDDEN,
error: origin + '에서는 접근이 허용되지 않습니다.'
},
HttpStatus.FORBIDDEN
)
)
}
},
methods: process.env.CORS_ALLOW_METHOD,
credentials: true
})
await app.listen(2000, function () {
console.log(
'[CORS-enabled->npm install -g webpack webpack-cli] web server listening on port 2000'
)
})
}
bootstrap()

31
src/prisma.service.ts Normal file
View File

@ -0,0 +1,31 @@
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
[x: string]: any
constructor() {
//SQL 로그를 출력하기위해 추가
super({
log: [
{ emit: 'event', level: 'query' },
{ emit: 'stdout', level: 'info' },
{ emit: 'stdout', level: 'warn' },
{ emit: 'stdout', level: 'error' }
],
errorFormat: 'colorless'
})
}
async onModuleInit() {
await this.$connect()
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async (event) => {
//SQL 로그를 출력하기위해 추가
console.log(event.name)
await app.close()
})
}
}

View File

@ -0,0 +1,9 @@
export class UserDTO {
email: string
password: string
name: string
role?: string | 'USER'
is_done?: boolean | false
updatedAt?: Date | null
createdAt?: Date
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'
import { UserController } from './user.controller'
describe('UserController', () => {
let controller: UserController
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController]
}).compile()
controller = module.get<UserController>(UserController)
})
it('should be defined', () => {
expect(controller).toBeDefined()
})
})

161
src/user/user.controller.ts Normal file
View File

@ -0,0 +1,161 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
UseGuards
} from '@nestjs/common'
import { User } from '@prisma/client'
import { UserDTO } from './dtos/user.dto'
import { UserService } from './user.service'
import { HasRoles } from 'src/auth/decorators/has-roles.decorator'
import { JwtAuthGuard } from 'src/auth/guards/jwt.authguard'
import { Role } from 'src/auth/guards/role.enum'
import { RolesGuard } from 'src/auth/guards/roles.guard'
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
async fetchAll(@Query() query): Promise<any> {
console.log(query)
//Field별 filter AND Sql용
let filterSql = {}
switch (query.filterField) {
case 'is_done':
if (query.filter) {
filterSql = {
AND: {
[query.filterField]: query.filter === 'true' ? true : false
}
}
}
break
case 'updatedAt':
case 'createdAt':
if (query.filterDateStart && query.filterDateEnd) {
filterSql = {
AND: {
[query.filterField]: {
gte: new Date(query.filterDateStart) as Date,
lte: new Date(query.filterDateEnd) as Date
}
}
}
}
break
}
console.log(filterSql)
//Field별 search OR Sql용
let searchSql = {}
if (query.search) {
const searchFieldSQLs = []
for (const index in query.searchFields) {
switch (query.searchFields[index]) {
case 'title':
case 'content':
searchFieldSQLs.push({
[query.searchFields[index]]: {
contains: query.search as string
}
})
break
case 'updatedAt':
case 'createdAt':
searchFieldSQLs.push({
[query.searchFields[index]]: {
gte: new Date(query.search) as Date
}
})
break
}
}
console.log(searchFieldSQLs)
searchSql = { OR: searchFieldSQLs }
}
console.log(searchSql)
//Field별 Sort Sql용
if (!query.sortBy) {
query.sortBy = 'id'
}
const orderBySql = {
[query.sortBy]: query.sortDesc === 'true' ? 'desc' : 'asc'
}
console.log(orderBySql)
//fetch SQL용
const page = query.page
? parseInt(query.page) - 1
: parseInt(process.env.DEFAULT_TABLE_PAGE)
const perPage = query.perPage
? parseInt(query.perPage)
: parseInt(process.env.DEFAULT_TABLE_PERPAGE)
const fetchSQL = {
skip: page * perPage,
take: perPage,
where: {
...filterSql,
...searchSql
},
orderBy: orderBySql
}
console.log(fetchSQL)
//전체 갯수 및 fetched Data
const total = await this.userService.count(fetchSQL)
const rows = await this.userService.fetchAll(fetchSQL)
const result = {
total: total,
perPage: perPage,
page: page + 1,
sortBy: query.sortBy,
sortDesc: query.sortDesc,
rows: rows
}
//console.log(result)
console.log('--------------------------------------------')
return result
}
@HasRoles(Role.USER)
@UseGuards(JwtAuthGuard, RolesGuard)
@Get(':id')
async fetchOne(@Param('id') id: number): Promise<User | undefined> {
return this.userService.fetchOne(id)
}
// @HasRoles(Role.USER)
// @UseGuards(JwtAuthGuard, RolesGuard)
//@UseGuards(JwtAuthGuard)
@Post()
async add(@Body() data: UserDTO): Promise<User> {
return await this.userService.add(data)
}
@HasRoles(Role.USER)
@UseGuards(JwtAuthGuard, RolesGuard)
@Put(':id')
async update(@Param('id') id: string, @Body() data: UserDTO): Promise<User> {
data.updatedAt = new Date()
return await this.userService.update({
where: { id: Number(id) },
data: data
})
}
@HasRoles(Role.USER)
@UseGuards(JwtAuthGuard, RolesGuard)
@Delete(':id')
async delete(@Param('id') id: string): Promise<User | undefined> {
return await this.userService.remove({ id: Number(id) })
}
}

11
src/user/user.module.ts Normal file
View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common'
import { PrismaService } from 'src/prisma.service'
import { UserService } from './user.service'
import { UserController } from './user.controller'
@Module({
controllers: [UserController],
providers: [UserService, PrismaService],
exports: [UserService]
})
export class UsersModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'
import { UserService } from './user.service'
describe('UserService', () => {
let service: UserService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserService]
}).compile()
service = module.get<UserService>(UserService)
})
it('should be defined', () => {
expect(service).toBeDefined()
})
})

75
src/user/user.service.ts Normal file
View File

@ -0,0 +1,75 @@
import { Injectable } from '@nestjs/common'
import { Prisma, User } from '@prisma/client'
import { PrismaService } from 'src/prisma.service'
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {
prisma.$on<any>('query', (event: Prisma.QueryEvent) => {
console.log('Query: ' + event.query)
console.log('Duration: ' + event.duration + 'ms')
})
}
//단일 조회 ByEmail
async fetchOneByEmail(email: string): Promise<User | undefined> {
return this.prisma.user.findUnique({ where: { email: email } })
}
//전체조회
async count(params: {
cursor?: Prisma.UserWhereUniqueInput
where?: Prisma.UserWhereInput
}): Promise<number> {
const { cursor, where } = params
return this.prisma.todo.count({
cursor,
where
})
}
//전체조회
async fetchAll(params: {
skip?: number
take?: number
cursor?: Prisma.UserWhereUniqueInput
where?: Prisma.UserWhereInput
orderBy?: Prisma.UserOrderByWithRelationInput
}): Promise<User[]> {
const { skip, take, cursor, where, orderBy } = params
return this.prisma.todo.findMany({
skip,
take,
cursor,
where,
orderBy
})
}
//단일 조회
async fetchOne(where: Prisma.UserWhereUniqueInput): Promise<User | null> {
return this.prisma.todo.findUnique({ where })
}
//단일 추가
async add(data: Prisma.UserCreateInput): Promise<User> {
return this.prisma.todo.create({ data })
}
//단일 수정
async update(params: {
where: Prisma.UserWhereUniqueInput
data: Prisma.UserUpdateInput
}): Promise<User | null> {
const { where, data } = params
return this.prisma.todo.update({
data,
where
})
}
//단일 삭제
async remove(where: Prisma.UserWhereUniqueInput): Promise<User | null> {
return this.prisma.todo.delete({ where })
}
}

24
test/app.e2e-spec.ts Normal file
View File

@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication } from '@nestjs/common'
import * as request from 'supertest'
import { AppModule } from './../src/app.module'
describe('AppController (e2e)', () => {
let app: INestApplication
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule]
}).compile()
app = moduleFixture.createNestApplication()
await app.init()
})
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!')
})
})

9
test/jest-e2e.json Normal file
View File

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

4
tsconfig.build.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

21
tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}