블로그 이미지
윤영식
Full Stacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

Category

Recent Post

2021. 9. 27. 18:04 React/Architecture

Micro Service의 앞단 Gateway에서 모든 호출에 대한 인증/인가를 처리한다. 먼저 JWT기반 인증에 대한 설정을 한다. 

  • passwort jwt 설정

 

JWT 처리를 위한  패키지 설치

passport를 통해 JWT를 관리한다.

  • userId/password 기반은 passport-local을 사용
  • JWT 체크 passport-jwt 사용
  • 추가적으로 http security를 위해 express middleware인 helmet 적용
$> yarn add @nestjs/jwt @nestjs/passport passport passport-local passport-jwt helmet
$> yarn add -D @types/passport-local @types/passport-jwt @types/express

 

Passport Local 적용

  • libs/domain/src/lib/auth/strategies 폴더 생성
  • local.strategy.ts 파일 생성
// local.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException({
        error: 'Incorrect username and password'
      });
    }
    return user;
  }
}

 

Username/Password 체크하기

libs/domain/src/lib/auth/  폴더 생성하고, auth.service.ts 파일을 생성한다. 

  • validateUser: LocalStrategy에서 호출한다. username/password 로그인 유효성을 login 호출 이전에 체크한다.
  • login: validate user인 경우 사용자 정보를 통한 webtoken 생성
// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

import { User } from '../entities/user/user.model';
import { UserService } from '../entities/user/user.service';
import { bcryptCompare } from '../utilties/bcrypt.util';

@Injectable()
export class AuthService {
  constructor(
    private readonly userService: UserService,
    private readonly jwtService: JwtService,
  ) { }

  async validateUser(username: string, pass: string): Promise<any> {
    const user: any = await this.userService.findOne(username) || {};
    if (user && pass === user.password) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(loginUser: User): Promise<any> {
    const user = await this.userService.findOne(loginUser.username);
    if (user) {
      const payload = { username: user.username, sub: user.id, email: user.email, role: user.role };
      return {
        access_token: this.jwtService.sign(payload),
      };
    } else {
      throw new UnauthorizedException({
        error: 'There is no user'
      });
    }
  }
}

토큰 생성확인은 https://jwt.io/ 에서 할 수 있다. 

 

libs/domain/src/lib/auth/auth.module.ts 파일을 생성하고, config.json파일에 AUTH 프로퍼티를 추가한다. 

  • JwtModule을 등록한다. 
  • secret은 반드시 별도의 환경설정 파일에서 관리한다. 
  • AuthService도 등록한다.
  • User 정보를 read하기 위해 EntitiesModule도 imports 에 설정한다.
// apps/gateway/api/src/environments/config.json
{
  "HTTP_PORT": 8000,
  "AUTH": {
    "SECRET": "iot_secret_auth",
    "EXPIRED_ON": "1d"
  },
  ...
}

// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';

import { GatewayConfiguration, loadConfigJson } from '@rnm/shared';
import { EntitiesModule } from '../entities/entity.module';

import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';

const config: GatewayConfiguration = loadConfigJson();

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: config.AUTH?.SECRET,
      signOptions: { expiresIn: config.AUTH?.EXPIRED_ON },
    }),
    EntitiesModule
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule { }

 

Passport JWT 적용

로그인이 성공하면 jsonwebtoken 을 생성하고, 이후 request(요청)에 대해 JWT를 체크하는 환경설정을 한다.

  • libs/domain/src/lib/auth/strategies/jwt.strategy.ts 파일  생성
    • request header의 Bearer Token 체크 => Cookie 사용으로 변경 (master branch소스 참조)
    • 확인하는 secret 설정
  • AuthModule에 등록한다.
// jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Request } from 'express';

import { loadConfigJson } from '@rnm/shared';
const config: any = loadConfigJson();

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      // jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      // Cookie를 사용한다
      jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => {
        return request?.cookies?.LOGIN_TOKEN;
      }]),
      ignoreExpiration: false,
      secretOrKey: config?.AUTH?.SECRET
    });
  }

  async validate(user: any): Promise<any> {
    // return { id: payload.sub, username: payload.username };
    return user;
  }
}

 

Password 암호화

암호화 모듈 설치

$> yarn add bcrypt
$> yarn add -D @types/bcrypt

암호화 유틸리티를 생성한다. libs/domain/src/lib/utilties/bcrypt.util.ts 파일 생성

  • 사용자 생성시 패스워드
  • 입력 패스워드를 DB의 암호화된 패스워드와 비교한다.
import * as bcrypt from 'bcrypt';

// 사용자의 패스워드 암호화
export const bcryptHash = (plainText: string, saltOrRounds = 10): Promise<string> => {
  return bcrypt.hash(plainText, saltOrRounds);
}

// 입력 패스워드와 DB 패스워드 비교
export const bcryptCompare = (plainText: string, hashedMessage: string): Promise<boolean> => {
  return bcrypt.compare(plainText, hashedMessage);
}

libs/domain/src/lib/auth/auth.service.ts 의 validateUser에서 암호화된 password를 체크토록 수정한다.

// auth.service.ts
import { bcryptCompare } from '../utilties/bcrypt.util';

@Injectable()
export class AuthService {
  constructor(
    private readonly userService: UserService,
    private readonly jwtService: JwtService,
  ) { }

  async validateUser(username: string, pass: string): Promise<any> {
    const user: any = await this.userService.findOne(username) || {};
    // 암호화된 패스워드를 입력 패스워드와 같은지 비교
    const isMatch = await bcryptCompare(pass, user.password);
    if (user && isMatch) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
  ...
}

 

 

로그인 하기

  • apps/gateway/api/src/app/auth/auth.controller.ts 파일을 신규 생성.
  • apps/gateway/api/src/app/app.module.ts 설정
    • "auth/login" API에 대해 static server에 exclude 설정
    • AuthModule imports에 설정
    • AuthController 등록
// auth.controller.ts
import { Body, Controller, Post, UseGuards } from '@nestjs/common';

import { AuthService, LocalAuthGuard, User } from '@rnm/domain';

@Controller()
export class AuthController {
  constructor(
    private readonly authService: AuthService
  ) { }

  @UseGuards(LocalAuthGuard)
  @Post('auth/login')
  async login(@Body() user: User): Promise<Response> {
    return this.authService.login(user);
  }
}

// app.module.ts
@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, 'public'),
      exclude: [
        '/auth/*',
        '/api/gateway*', '/api/dashboard*', '/api/configuration*', '/api/back-office*',
        '/dashboard*', '/configuration*', '/back-office*'
      ],
    }),
    // ORM
    TypeOrmModule.forRoot({
      ...ormConfigService.getTypeOrmConfig(),
      entities: getMetadataArgsStorage().tables.map(tbl => tbl.target)
    }),
    EntitiesModule,
    // MicroService
    DashboardModule,
    // Auth
    AuthModule
  ],
  controllers: [
    AppController,
    AuthController,
    UserController
  ],
  providers: [GatewayApiAppService]
})
export class AppModule { }

 

UseGuard에서 username/password는  LocalAuthGuard를 등록한다. 이를 위해 libs/domain/src/lib/auth/guards/local-auth.guard.ts 파일 생성한다.

import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';

import { JwtAuthGuard, User, UserService } from '@rnm/domain';

@Controller('api/gateway/user')
export class UserController {
  constructor(
    private readonly service: UserService
  ) { }

  @UseGuards(JwtAuthGuard)
  @Post()
  create(@Body() data: User): Promise<User> {
    return this.service.create(data);
  }

  @UseGuards(JwtAuthGuard)
  @Put(':id')
  updateOne(@Param('id') id: number, @Body() data: User): Promise<any> {
    return this.service.updateOne(id, data);
  }

  @UseGuards(JwtAuthGuard)
  @Get()
  findAll(): Promise<User[]> {
    return this.service.findAll();
  }

  @UseGuards(JwtAuthGuard)
  @Get(':username')
  findOne(@Param('username') username: string): Promise<User | undefined> {
    return this.service.findOne(username);
  }

  @UseGuards(JwtAuthGuard)
  @Delete(':id')
  deleteOne(@Param('id') id: string): Promise<any> {
    return this.service.deleteOne(id);
  }
}
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') { }

User 생성하는 apps/gateway/api/src/app/user/user.controller.ts 에도 @UseGuards 를 JWT 토큰 체크하는 Guard로 등록한다. 이를 위하여 libs/domain/src/lib/auth/guards/jwt-auth.guard.ts 파일을 생성한다. 

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: any, user: any, info: any, context: any, status?: any) {
    // You can throw an exception based on either "info" or "err" arguments
    if (err || !user) {
      throw err || new UnauthorizedException();
    }
    return user;
  }
}

그리고 apps/gateway/api/src/app/user/user.controller.ts 에 @UseGuards를 "JwtAuthGuard"로 등록한다. 

import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';

import { JwtAuthGuard, User, UserService } from '@rnm/domain';

@Controller('api/gateway/user')
export class UserController {
  constructor(
    private readonly service: UserService
  ) { }

  @UseGuards(JwtAuthGuard)
  @Post()
  create(@Body() data: User): Promise<User> {
    return this.service.create(data);
  }

  @UseGuards(JwtAuthGuard)
  @Put(':id')
  updateOne(@Param('id') id: number, @Body() data: User): Promise<any> {
    return this.service.updateOne(id, data);
  }

  @UseGuards(JwtAuthGuard)
  @Get()
  findAll(): Promise<User[]> {
    return this.service.findAll();
  }

  @UseGuards(JwtAuthGuard)
  @Get(':username')
  findOne(@Param('username') username: string): Promise<User | undefined> {
    return this.service.findOne(username);
  }

  @UseGuards(JwtAuthGuard)
  @Delete(':id')
  deleteOne(@Param('id') id: string): Promise<any> {
    return this.service.deleteOne(id);
  }
}

Postman으로 테스트 한다. 

소스: https://github.com/ysyun/rnm-stack/releases/tag/ms-6

 

Release ms-6 · ysyun/rnm-stack

[ms-6] add typeorm and jwt for auth

github.com

주의: 소스가 계속 업데이트되고 있기에 master branch를 참조해도 된다.

 

 

<참조>

- NestJS에 passport 기반 JWT 적용하기 

https://docs.nestjs.kr/security/authentication

 

네스트JS 한국어 매뉴얼 사이트

네스트JS 한국, 네스트JS Korea 한국어 매뉴얼

docs.nestjs.kr

- passport local 환경설정

http://www.passportjs.org/packages/passport-local/

 

passport-local

Local username and password authentication strategy for Passport.

www.passportjs.org

- passport-jwt 환경설정

https://www.passportjs.org/packages/passport-jwt/

 

passport-jwt

Passport authentication strategy using JSON Web Tokens

www.passportjs.org

- password 암호화

https://wanago.io/2020/05/25/api-nestjs-authenticating-users-bcrypt-passport-jwt-cookies/

 

API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies

1. API with NestJS #1. Controllers, routing and the module structure2. API with NestJS #2. Setting up a PostgreSQL database with TypeORM3. API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies4. API with NestJS #4. Error handling

wanago.io

posted by 윤영식
2021. 9. 24. 19:48 React/Architecture

MS-4/5/6 글을 통해 Gateway의 공통 기능 구현을 위한 설정을 적용한다.

  • Login 할때 사용자 정보는 ORM 기반으로 처리한다.
  • 인증을 JWT 기반으로 처리한다.
  • Login 화면을 React 기반 구현한다.

 

NestJS에 Prisma ORM 설정

gateway-api의 공통 기능은 다음과 같고, JWT처리를 위한 사용자 정보 조회를 위해 Prisma ORM을 적용한다. Micro Service도 Prisma를 사용할 것이다.

  • api gateway: TCP 통신으로 micro service의 API를 연결한다. 
  • http server: gateway의 공통 기능중 Login을 서비스한다.
  • reverse proxy: Login이 성공하면 dashboard micro service로 이동한다. (configuration, back-office)
  • auth server: JWT 기반으로 token을 발행하고, 요청에 대한 모든 Authorization(인가, 권한)을 체크한다.

 

Step-1) 설치 및 환경 설정

auth server 기능에서 사용자 정보 데이터처리를 위해 ORM으로 Prisma를 사용을 위해 패키지를 (v3.1.1) 설치한다.

$> yarn add -D prisma
$> yarn add @prisma/client
$> yarn add -D ts-node

전체 애플리케이션을 위한 초기 prisma 환경을 생성한다.

$> npx prisma init

✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver (Preview) or mongodb (Preview).
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started

자동 생성된 .env 파일에 설정을 추가한다. 

# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server (Preview) and MongoDB (Preview).
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
#DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

# POSTGRES
POSTGRES_USER=iot
POSTGRES_PASSWORD=1
POSTGRES_DB=rnm-stack

# Nest run locally
DB_HOST=localhost
# Nest run in docker, change host to database container name
# DB_HOST=postgres
DB_PORT=5432
DB_SCHEMA=public

# Prisma database connection
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:${DB_PORT}/${POSTGRES_DB}?schema=${DB_SCHEMA}&sslmode=prefer

VS Code의 extension을 설치한다. 

VSCode의 prisma extension

 

Step-2) schema.prisma 설정

VSCode extension이 설치되면 schema.prisma의 내용이 다음과 같이 highlighting된다. 

schema.prisma 파일 안에 Prisma 방식의 스키마를 정의한다.

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
  // previewFeatures = []
}

// generator dbml {
//   provider = "prisma-dbml-generator"
// }

model User {
  id        Int   @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  email     String   @unique
  password  String
  firstname String?
  lastname  String?
  posts     Post[]
  role      Role
}

model Post {
  id        Int   @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  published Boolean
  title     String
  content   String?
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  Int?
}

enum Role {
  ADMIN
  MANAGER
  USER
}

schema.prisma를 통해 Migrate SQL과 Prisma Client 파일을 자동 생성한다. Prisma Client 파일은 구현 코드에서 사용된다. (참조)

 

 

Step-3) schema 설정을 통해 sql 생성하기 

명령을 수행하면 prisma/migrations sql이 자동 실행된다. 생성된 sql을 통해 table schema를 업데이트한다. 변경점이 있으면 날짜별로 update 할 수 있는 table schema가 자동으로 생성된다. 

$> npx prisma migrate dev --create-only --name=iot

또한 DB Schema에 _prisma_migrations 테이블이 자동생성된다. 

rnm-stack 의 public에 migrations 테이블 자동 생성

테이블을 자동 생성하고 싶다. prisma db push 명령을 사용한다. (참조)

$> npx prisma db push

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "rnm-stack", schema "public" at "localhost:5432"
🚀  Your database is now in sync with your schema. Done in 77ms
✔ Generated Prisma Client (3.1.1) to ./node_modules/@prisma/client in 61ms

Post, User 테이블 자동 생성

 

Step-4) Prisma Studio 사용하기

prisma는 내장 웹기반 studio를 제공한다. 테이블을 선택하여 조작할 수 있다.

$> npx prisma studio

 

 

NestJS 에서 PrismaClient  사용하기

Step-1) PrismaClient 생성

schema에 생성되었으면 다음으로 코드에서 Prisma 접근을 위해 PrismaClient를 생성해야 한다. (참조)

  • "npx prisma migrate dev" 명령으로 수행할 경우는 "npx prisma generate"이 필요없다. 
  • "npx prisma migrate dev --create-only" 일 경우만 수행한다.
  • schema.prisma 변경시 마다 다시 실행해 주어야 한다. (참조)

$> npx prisma generate
✔ Generated Prisma Client (3.1.1) to ./node_modules/@prisma/client in 181ms
You can now start using Prisma Client in your code. Reference: https://pris.ly/d/client
```
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
```

prisma client는 기본적으로 node_modules/.prisma/client 폴더 밑에 생성된다.

node_modules/.prisma/client 폴더

 

 

Step-2) schema.prisma가 변경이 발생할 경우

이제 "PrismaClient"를 import해서 사용할 수 있는 상태가 되었다. 만일 테이블 변경이 발생한다면 아래와 같이 수행한다. 

  • schema.prisma 파일 내역 수정
  • "npx prisma migrate" 명령 실행
  • migrations 폴더에 있는 migration sql을 database에 적용한다. 

참조: https://www.prisma.io/blog/prisma-migrate-ga-b5eno5g08d0b

 

 

Step-3) NestJS 서비스 생성

사용자 정보는 공통이므로 libs/domain 에 생성한다. (참조)

  • libs/shared/src/lib밑으로 prisma 폴더를 생성하고, prisma-client.service.ts파일을 생성한다. 
  • index.ts에 export를 추가한다. 

// prisma-client.service.ts 
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'

@Injectable()
export class PrismaClientService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect()
  }

  async onModuleDestroy() {
    await this.$disconnect()
  }
}

// index.ts
export * from './lib/configuration/config.model';
export * from './lib/configuration/config.service';
export * from './lib/prisma/prisma-client.service';

apps/gateway/api/src/app/app.module.ts에 PrismaClientService를 추가한다.

// /apps/gateway/api/src/app/app.module.ts
import { Module } from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';

import { GatewayApiAppService } from '@rnm/domain';
import { PrismaClientService } from '@rnm/shared'; <== 요기

import { AppController } from './app.controller';
import { DashboardModule } from './dashboard/dashboard.module';

@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, 'public'),
      exclude: [
        '/api/gateway*', '/api/dashboard*',
        '/dashboard*', '/configuration*', '/back-office*'
      ],
    }),
    DashboardModule
  ],
  controllers: [AppController],
  providers: [GatewayApiAppService, PrismaClientService] <== 요기
})
export class AppModule { }

gateway/api/src/app/app.controller.ts에 테스트 코드로 POST로 user를 생성하는 코드를 작성한다. 

// apps/gateway/api/src/app/app.controller.ts
import { Body, Controller, Get, Post } from '@nestjs/common';

import { GatewayApiAppService } from '@rnm/domain';
import { PrismaClientService } from '@rnm/shared';

import { Role, User as UserModel } from '@prisma/client';

@Controller('api/gateway')
export class AppController {
  constructor(
    private readonly appService: GatewayApiAppService,
    private readonly dbService: PrismaClientService
  ) { }

  @Get()
  getData() {
    return this.appService.getData();
  }

  @Post('user')
  async createUser(@Body() userData: {
    email: string,
    password: string,
    firstname: string,
    lastname; string,
    role: Role
  }): Promise<UserModel> {
    const { email, password, firstname, lastname, role } = userData;
    return this.dbService.user.create({
      data: {
        email,
        password,
        firstname,
        lastname,
        role: !role ? Role.USER : role
      }
    });
  }
}

VSCode에서 Debug창에서 Gateway를 실행하고, 디버깅을 한다.

28, 29줄에 breakpoint 

 

Postman을 실행하여 User 를 생성해 본다.

  • 새로운 Request를 만들고 POST를 선택
  • Body에 JSON 타입을 선택
  • request 값을 넣고, http://localhost:8000/api/gateway/user 호출

DB툴로 User insert가 되었는지 확인 또는 prisma studio에서 확인

test@test.com 사용자가 insert 성공!

 

소스: https://github.com/ysyun/rnm-stack/releases/tag/ms-4

 

<참조>

- Primsa on NestJS 기반 개발

https://www.prisma.io/nestjs

 

NestJS Database & Prisma | Type-safe ORM for SQL Databases

Prisma is a next-generation ORM for Node.js & TypeScript. It's the easiest way to build NestJS apps with MySQL, PostgreSQL & SQL Server databases.

www.prisma.io

- Prisma Migrate 순서

https://www.prisma.io/blog/prisma-migrate-ga-b5eno5g08d0b

 

Prisma Migrate is Production Ready - Hassle-Free Database Migrations

Prisma Migrate is ready for use in production - Database schema migration tool with declarative data modeling and auto-generated, customizable SQL migrations

www.prisma.io

- Primsa, JWT on NestJS StartKit

https://github.com/fivethree-team/nestjs-prisma-starter

 

GitHub - fivethree-team/nestjs-prisma-starter: Starter template for NestJS 😻 includes GraphQL with Prisma Client, Passport-JW

Starter template for NestJS 😻 includes GraphQL with Prisma Client, Passport-JWT authentication, Swagger Api and Docker - GitHub - fivethree-team/nestjs-prisma-starter: Starter template for NestJS 😻...

github.com

- Prisma의 다양한 DB 예제

https://github.com/prisma/prisma-examples

 

GitHub - prisma/prisma-examples: 🚀 Ready-to-run Prisma example projects

🚀 Ready-to-run Prisma example projects. Contribute to prisma/prisma-examples development by creating an account on GitHub.

github.com

posted by 윤영식
prev 1 next