우버이츠클론코딩 #6
Token 만들기
-
jwt 직접 만들기
-> npm i jsonwebtoken
-> npm i @types/jsonwebtoken –only-dev
-
app.module.ts
- token 을 user한테 지정시, 사용자는 자기 token에 뭐 들어있는지 볼 수 있음
-> token에는 중요한 개인 정보 넣지 않기(ID정도)
- 목적: 사용자에게 약간의 json 주기(우리가 json 지정해줘야 함)
-> SCRET_KEY 추가
@Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: process.env.NODE_ENV == "dev" ? ".env.dev" : ".env.test", ignoreEnvFile: process.env.NODE_ENV ==="prod", //production환경일땐 ConfigModule이 환경변수 파일 validationSchema: Joi.object({ NODE_ENV: Joi.string() .valid('dev', 'prod') .required(), // 환경변수 유효성 검사 DB_HOST: Joi.string().required(), DB_PORT: Joi.string().required(), DB_USERNAME: Joi.string().required(), DB_PASSWORD: Joi.string().required(), DB_NAME: Joi.string().required(), SECRET_KEY: Joi.string().required(), //token 지정을 위해 사용하는 privateKey }),
-
.env.dev
-> SECRET_KEY는 secret key generater에서 복붙
DB_HOST=localhost DB_PORT=5432 DB_USERNAME=postgres DB_PASSWORD=1234 DB_NAME=nuber-eats SECRET_KEY=7a3MHXMhcOMOtud3rYkmOrrr7Iua31II
-
users.module.ts
import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; import { UsersResolver } from './users.resolver'; import { UsersService } from './users.service'; @Module({ imports: [TypeOrmModule.forFeature([User]), ConfigService], //service는 repository 필요로 하기 떄문에 // ConfigService는 token 생성할 때 import해주려고 users.module안에 configService 추가 providers:[UsersResolver, UsersService], }) export class UsersModule {}
-
users.service.ts
-> token 생성
-> jsonwebtoken import하기
import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { createAccountInput } from "./dtos/create-account.dto"; import { LoginInput } from "./dtos/login.dto"; import { User } from "./entities/user.entity"; import * as jwt from 'jsonwebtoken'; import { ConfigService } from "@nestjs/config"; @Injectable() export class UsersService{ constructor( @InjectRepository(User) private readonly users: Repository <User>,//User entity의 InjectRepository 불러오기 & type이 repository이고 repository type은 user enitity private readonly config: ConfigService // token 파트에서 ConfigService import해서 사용하려고 ){ console.log(this.config.get("SECRET_KEY")) } async createAccount({email, password, role}: createAccountInput ): Promise <{ok: boolean, error? :string }>{ //check new user(that email does not exist) try { const exists = await this.users.findOne({email}) //findOne = 주어진 condition(환경)과 일치하는 첫 번째 entity 찾기 if(exists){ //make error return {ok: false, error: 'There is a uwer with that email already'}; //boolean =false, error ="there~" } await this.users.save(this.users.create({email, password, role})) //없다면 새로운 계정 create & save return {ok: true}; } catch(e){ return {ok: false, error: "Couldn't create account"}; } // create user & hash the password } async login({ email, password }: LoginInput): Promise<{ok: boolean; error?:string, token?: string}> { //make a JWT and give it to the user try{ // find the user with the email const user = await this.users.findOne({ email }); if(!user){ //user가 존재하지 않는다면 return { ok:false, error: 'User not found', } } //check if the password is correct //비밀번호를 hash 한후 데이터베이스에 있는 hash된 비번과 같은지 확인 const passwordCorrect = await user.checkPassword(password); //여기의 user와 위의 const user와는 다름.. 전자는 entity if (!passwordCorrect){ return{ ok:false, error:"Wrong password" } } const token =jwt.sign({id:user.id},this.config.get('SECRET_KEY'))//지정(sign)하기 & sign()안에는 무엇을 넣어 주고 싶은지(여기선 user ID) return{ ok:true, token: 'llalaalalala', } }catch(error){ return{ ok: false, error, } } } }
-> json web token 목적: 우리만이 유효한 인증을 할 수 있게 하는것(정보의 진위여부가 중요)
-
token module 만들기
-
module 종류
-
‘static module’
ex) UsersModule
JwtModule
-> 어떤 설정도 안돼있음
-
‘dynamic module’
ex) GraphQLModule
-> 설정이 적용되어 있는 module
=> dynamic module은 결과적으로 static module이 됨
dynamic module 만들기 -> 옵션설정 -> 리턴 값으로 설정한 옵션들이 존재하는 정적인 모듈
-
-
nest g mo jwt
-
module안에 .forRoot 구현
-
jwt.module.ts
import { DynamicModule, Module } from '@nestjs/common'; @Module({}) export class JwtModule { static forRoot(): DynamicModule{ //DynamicModule은 module을 반환해주는 module return { module:JwtModule, //module이 service를 export할 수 있도록 } } }
-
-
module이 service를 export 할 수 있도록 JwtSevice 만들기
-> nest g s jwt
-
jwt.module.ts
import { DynamicModule, Module } from '@nestjs/common'; import { JwtService } from './jwt.service'; @Module({}) export class JwtModule { static forRoot(): DynamicModule{ //DynamicModule은 module을 반환해주는 module return { module:JwtModule, exports: [JwtService], providers: [JwtService], } } }
-
users.module.ts
-> JwtService를 import
import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { JwtService } from 'src/jwt/jwt.service'; import { User } from './entities/user.entity'; import { UsersResolver } from './users.resolver'; import { UsersService } from './users.service'; @Module({ imports: [TypeOrmModule.forFeature([User]), ConfigService, JwtService], //service는 repository 필요로 하기 떄문에 // ConfigService는 token 생성할 때 import해주려고 users.module안에 configService 추가 providers:[UsersResolver, UsersService], }) export class UsersModule {}
-
user.service.ts
export class UsersService{ constructor( @InjectRepository(User) private readonly users: Repository <User>,//User entity의 InjectRepository 불러오기 & type이 repository이고 repository type은 user enitity private readonly config: ConfigService, // token 파트에서 ConfigService import해서 사용하려고 private readonly jwtService: JwtService //nestjs는 클래스 타입만 보고 import 알아서 찾아줌 ){ console.log(this.config.get("SECRET_KEY")) }
-
jwt.module.ts
import { DynamicModule, Global, Module } from '@nestjs/common'; import { JwtService } from './jwt.service'; @Module({}) @Global() export class JwtModule { static forRoot(): DynamicModule{ //DynamicModule은 module을 반환해주는 module return { module:JwtModule, exports: [JwtService], providers: [JwtService], }; } }
※Global로 설정된 module은 imports에 넣어줄 필요 없음
-
2.3 Global Module & non-Global Module
module에 config 옵션 추가하기
-
jwt 안에 interfaces 폴더 생성 & jwt-module-options.interface.ts
export interface JwtModuleOptions{ privateKey:string; }//원하는 옵션 추가
-
jwt.module.ts
-> option으로 JwtModuleOptions타입 추가
import { DynamicModule, Global, Module } from '@nestjs/common'; import { JwtModuleOptions } from './interfaces/jwt-module-options.interface'; import { JwtService } from './jwt.service'; @Module({}) @Global() export class JwtModule { static forRoot(options: JwtModuleOptions): DynamicModule{ //DynamicModule은 module을 반환해주는 module return { module:JwtModule, exports: [JwtService], providers: [JwtService], }; } }
-
app.module.ts
-> JwtModule에 옵션 넘겨주기
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { GraphQLModule } from '@nestjs/graphql'; import {TypeOrmModule} from "@nestjs/typeorm"; import * as Joi from 'joi'; //타입스크립트나 NestJS로 되어있지 않을때 패키지 import import { Restaurant } from './restaurants/entities/restaurant.entity'; import { RestaurantsModule } from './restaurants/restaurants.module'; import { UsersModule } from './users/users.module'; import { CommonModule } from './common/common.module'; import { User } from './users/entities/user.entity'; import { JwtModule } from './jwt/jwt.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: process.env.NODE_ENV === "dev" ? ".env.dev" : ".env.test", ignoreEnvFile: process.env.NODE_ENV ==="prod", //production환경일땐 ConfigModule이 환경변수 파일 validationSchema: Joi.object({ NODE_ENV: Joi.string() .valid('dev', 'prod') .required(), // 환경변수 유효성 검사 DB_HOST: Joi.string().required(), DB_PORT: Joi.string().required(), DB_USERNAME: Joi.string().required(), DB_PASSWORD: Joi.string().required(), DB_NAME: Joi.string().required(), PRIVATE_KEY: Joi.string().required(), //token 지정을 위해 사용하는 privateKey }), }), TypeOrmModule.forRoot({ type: 'postgres', host: process.env.DB_HOST, port: +process.env.DB_PORT, username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, //localhost인 경우엔 안써도 됨 database: process.env.DB_NAME, synchronize: process.env.NODE_ENV ==="prod", //production이 아니면 true로 logging: process.env.NODE_ENV ==="prod", //DB에 돌아가는 모든 로그 확인 entities:[User], }), GraphQLModule.forRoot({ //dynamic module로 설정이 존재 autoSchemaFile: true, //root module 설정 }), UsersModule, CommonModule, JwtModule.forRoot({ privateKey: process.env.PRIVATE_KEY, }), //JwtModule처럼 static module은 어떠한 설정도 적용되어 있지않음 ], controllers: [], providers: [], }) export class AppModule {}
-
Jwt.module.ts
-> option을 JwtService로 내보내기 => providers 옵션 사용!
import { DynamicModule, Global, Module } from '@nestjs/common'; import { JwtModuleOptions } from './interfaces/jwt-module-options.interface'; import { JwtService } from './jwt.service'; @Module({}) @Global() export class JwtModule { static forRoot(options: JwtModuleOptions): DynamicModule{ //DynamicModule은 module을 반환해주는 module return { module:JwtModule, providers: [{ provide: "BANANAS", //class 대신 value로 대체 useValue: options, //BANANAS라는 이름의 provider로, value가 option }, JwtService, ], exports: [JwtService], //원하는 class를 provide할 수 있게 해줌 // provide:JwtService, useClass:JwtService 함축한것 }; } }
-
jwt.service.ts
=> module에서 무언가를 service로 inject할 수 있음
import { Global, Inject, Injectable } from '@nestjs/common'; import { JwtModuleOptions } from './interfaces/jwt-module-options.interface'; @Injectable() export class JwtService { constructor( @Inject('BANANAS') private readonly options: JwtModuleOptions){} hello(){ console.log('hello') } }
providers안에 필요한거 넣고 inject해서 요청
-
jwt에 jwt.constants.ts 생성 & jwt.interface생성
-
jwt.constants.ts
export const CONFIG_OPTIONS = "CONFIG_OPTIONS";
-
jwt.service.ts
-> BANANAS 변경
import { Global, Inject, Injectable } from '@nestjs/common'; import { CONFIG_OPTIONS } from './jwt.constants'; import { JwtModuleOptions } from './jwt.interfaces'; @Injectable() export class JwtService { constructor( @Inject(CONFIG_OPTIONS) private readonly options: JwtModuleOptions){ console.log(options); } hello(){ console.log('hello') } }
-
jwt.module.ts
import { DynamicModule, Global, Module } from '@nestjs/common'; import { CONFIG_OPTIONS } from './jwt.constants'; import { JwtModuleOptions } from './jwt.interfaces'; import { JwtService } from './jwt.service'; @Module({}) @Global() export class JwtModule { static forRoot(options: JwtModuleOptions): DynamicModule{ //DynamicModule은 module을 반환해주는 module return { module:JwtModule, providers: [{ provide: CONFIG_OPTIONS, //class 대신 value로 대체 useValue: options, //BANANAS라는 이름의 provider로, value가 option }, JwtService, ], exports: [JwtService], //원하는 class를 provide할 수 있게 해줌 // provide:JwtService, useClass:JwtService 함축한것 }; } }
2.4
-
jwt.service.ts
-> sign할 때 JwtService 사용하도록
import { Global, Inject, Injectable } from '@nestjs/common'; import * as jwt from "jsonwebtoken"; import { CONFIG_OPTIONS } from './jwt.constants'; import { JwtModuleOptions } from './jwt.interfaces'; @Injectable() export class JwtService { constructor( @Inject(CONFIG_OPTIONS) private readonly options: JwtModuleOptions){ } sign(userId:number): string{ //user ID 만 암호화해주기 return jwt.sign({id:userId}, this.options.privateKey); } }
-
user.service.ts
import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { CreateAccountInput } from "./dtos/create-account.dto"; import { LoginInput } from "./dtos/login.dto"; import { User } from "./entities/user.entity"; import * as jwt from 'jsonwebtoken'; import { ConfigService } from "@nestjs/config"; import { JwtService } from "src/jwt/jwt.service"; @Injectable() export class UsersService{ constructor( @InjectRepository(User) private readonly users: Repository <User>,//User entity의 InjectRepository 불러오기 & type이 repository이고 repository type은 user enitity private readonly jwtService: JwtService, //nestjs는 클래스 타입만 보고 import 알아서 찾아줌 ){} async createAccount({ email, password, role, }: CreateAccountInput): Promise <{ok: boolean, error? :string }>{ //check new user(that email does not exist) try { const exists = await this.users.findOne({email}) //findOne = 주어진 condition(환경)과 일치하는 첫 번째 entity 찾기 if(exists){ //make error return {ok: false, error: 'There is a uwer with that email already'}; //boolean =false, error ="there~" } await this.users.save(this.users.create({email, password, role})); //없다면 새로운 계정 create & save return {ok: true}; } catch(e){ return {ok: false, error: "Couldn't create account"}; } // create user & hash the password } async login({ email, password }: LoginInput): Promise<{ok: boolean; error?:string, token?: string}> { //make a JWT and give it to the user try{ // find the user with the email const user = await this.users.findOne({ email }); if(!user){ //user가 존재하지 않는다면 return { ok:false, error: 'User not found', } } //check if the password is correct //비밀번호를 hash 한후 데이터베이스에 있는 hash된 비번과 같은지 확인 const passwordCorrect = await user.checkPassword(password); //여기의 user와 위의 const user와는 다름.. 전자는 entity if (!passwordCorrect){ return{ ok:false, error:"Wrong password", }; } const token = this.jwtService.sign(user.id);//이 module을 내 백엔드만으로 특정함 return{ ok:true, token, } }catch(error){ return{ ok: false, error, }; } } }
=> 이건 내 백엔드에서만 특정// 같이 하는거면 공유가능한 jwtModule만들기
2.5 authentication
-
token 정보가 누구인지
-
users.resolver
import { Resolver, Query, Mutation, Args} from "@nestjs/graphql"; import { CreateAccountInput, CreateAccountOutput } from "./dtos/create-account.dto"; import { LoginInput, LoginOutput} from "./dtos/login.dto"; import { User } from "./entities/user.entity"; import { UsersService } from "./users.service"; @Resolver(of => User) export class UsersResolver { constructor( private readonly usersService: UsersService ){} @Query(returns => Boolean)//graphQL 루트 만들기 hi(){ return true; } @Mutation(returns =>CreateAccountOutput) async createAccount( @Args("input") createAccountInput: CreateAccountInput, ): Promise <CreateAccountOutput>{ //createAccountInput이라는 input type만듦 try{ return this.usersService.createAccount(createAccountInput); } catch(error) { //에러 발생시 return{ error, ok: false, } } } @Mutation(returns => LoginOutput) async login(@Args('input') loginInput: LoginInput ): Promise<LoginOutput>{//input Arguments 필요 try { return this.usersService.login(loginInput) //loginInput 저장 } catch(error){ return{ ok: false, error, }; } } @Query(returns => User) me(){ //middleware 구현 }//로그인한 사람이 누구인지 반환 }
=> token은 HTTP headers를 활용하여 받기
-
jwt.middleware.ts
import { Injectable, NestMiddleware } from "@nestjs/common"; import { NextFunction, Request, Response } from "express"; import { UsersService } from "src/users/users.service"; import { JwtService } from "./jwt.service"; @Injectable() export class JwtMiddleware implements NestMiddleware{ constructor( private readonly jwtService: JwtService, private readonly userService: UsersService ){} //injectable일때만 inject할 수 있음 async use(req:Request, res: Response, next:NextFunction){ if("x-jwt" in req.headers){ const token =(req.headers["x-jwt"]); const decoded = this.jwtService.verify(token.toString()); if(typeof decoded ==="object" && decoded.hasOwnProperty('id')){ try{ const user = await this.userService.findById(decoded['id']); req['user'] = user; // HTTP request } catch(e){ } } } next(); } } //NestMiddleware로 부터 상속 -> interface처럼 행동
-> forRoutes()통해서 /graphql 경로에 method가 POST인 경우에만 적용(apply)
-
app.module.ts
-> 모든 routes에 적용
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { GraphQLModule } from '@nestjs/graphql'; import {TypeOrmModule} from "@nestjs/typeorm"; import * as Joi from 'joi'; //타입스크립트나 NestJS로 되어있지 않을때 패키지 import import { Restaurant } from './restaurants/entities/restaurant.entity'; import { RestaurantsModule } from './restaurants/restaurants.module'; import { UsersModule } from './users/users.module'; import { CommonModule } from './common/common.module'; import { User } from './users/entities/user.entity'; import { JwtModule } from './jwt/jwt.module'; import { JwtMiddleware } from './jwt/jwt.middleware'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: process.env.NODE_ENV === "dev" ? ".env.dev" : ".env.test", ignoreEnvFile: process.env.NODE_ENV ==="prod", //production환경일땐 ConfigModule이 환경변수 파일 validationSchema: Joi.object({ NODE_ENV: Joi.string() .valid('dev', 'prod') .required(), // 환경변수 유효성 검사 DB_HOST: Joi.string().required(), DB_PORT: Joi.string().required(), DB_USERNAME: Joi.string().required(), DB_PASSWORD: Joi.string().required(), DB_NAME: Joi.string().required(), PRIVATE_KEY: Joi.string().required(), //token 지정을 위해 사용하는 privateKey }), }), TypeOrmModule.forRoot({ type: 'postgres', host: process.env.DB_HOST, port: +process.env.DB_PORT, username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, //localhost인 경우엔 안써도 됨 database: process.env.DB_NAME, synchronize: process.env.NODE_ENV ==="prod", //production이 아니면 true로 logging: process.env.NODE_ENV ==="prod", //DB에 돌아가는 모든 로그 확인 entities:[User], }), GraphQLModule.forRoot({ //dynamic module로 설정이 존재 autoSchemaFile: true, //root module 설정 }), UsersModule, CommonModule, JwtModule.forRoot({ privateKey: process.env.PRIVATE_KEY, }), //JwtModule처럼 static module은 어떠한 설정도 적용되어 있지않음 ], controllers: [], providers: [], }) export class AppModule implements NestModule{ configure(consumer:MiddlewareConsumer){ consumer.apply(JwtMiddleware).forRoutes({ //jwtMiddleware에 넘겨주기 path:"*", method:RequestMethod.ALL, }); } }
-
app.module.ts
-> 특정경로 제외
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { GraphQLModule } from '@nestjs/graphql'; import {TypeOrmModule} from "@nestjs/typeorm"; import * as Joi from 'joi'; //타입스크립트나 NestJS로 되어있지 않을때 패키지 import import { Restaurant } from './restaurants/entities/restaurant.entity'; import { RestaurantsModule } from './restaurants/restaurants.module'; import { UsersModule } from './users/users.module'; import { CommonModule } from './common/common.module'; import { User } from './users/entities/user.entity'; import { JwtModule } from './jwt/jwt.module'; import { JwtMiddleware } from './jwt/jwt.middleware'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: process.env.NODE_ENV === "dev" ? ".env.dev" : ".env.test", ignoreEnvFile: process.env.NODE_ENV ==="prod", //production환경일땐 ConfigModule이 환경변수 파일 validationSchema: Joi.object({ NODE_ENV: Joi.string() .valid('dev', 'prod') .required(), // 환경변수 유효성 검사 DB_HOST: Joi.string().required(), DB_PORT: Joi.string().required(), DB_USERNAME: Joi.string().required(), DB_PASSWORD: Joi.string().required(), DB_NAME: Joi.string().required(), PRIVATE_KEY: Joi.string().required(), //token 지정을 위해 사용하는 privateKey }), }), TypeOrmModule.forRoot({ type: 'postgres', host: process.env.DB_HOST, port: +process.env.DB_PORT, username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, //localhost인 경우엔 안써도 됨 database: process.env.DB_NAME, synchronize: process.env.NODE_ENV ==="prod", //production이 아니면 true로 logging: process.env.NODE_ENV ==="prod", //DB에 돌아가는 모든 로그 확인 entities:[User], }), GraphQLModule.forRoot({ //dynamic module로 설정이 존재 autoSchemaFile: true, //root module 설정 }), UsersModule, CommonModule, JwtModule.forRoot({ privateKey: process.env.PRIVATE_KEY, }), //JwtModule처럼 static module은 어떠한 설정도 적용되어 있지않음 ], controllers: [], providers: [], }) export class AppModule implements NestModule{ configure(consumer:MiddlewareConsumer){ consumer.apply(JwtMiddleware).exclude)({ path:"/api", method:RequestMethod.ALL, // /api를 제외하고 적용 }) } }
-
jwt.middleware
import { NestMiddleware } from "@nestjs/common"; import { NextFunction, Request, Response } from "express"; export function jwtMiddleware(req:Request, res:Response, next:NextFunction){ console.log(req.headers); next(); }
-> main.ts에 구현
-
main.ts
import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { jwtMiddleware } from './jwt/jwt.middleware'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes( new ValidationPipe() ); app.use(jwtMiddleware); await app.listen(3000); } bootstrap();
2.6
-
jwt.middleware.ts
-> users repository 가져올거라서 다시 class로
import { NestMiddleware } from "@nestjs/common"; import { NextFunction, Request, Response } from "express"; export class JwtMiddleware implements NestMiddleware{ use(req:Request, res: Response, next:NextFunction){ console.log(req.headers); next(); } } //NestMiddleware로 부터 상속 -> interface처럼 행동
-
main.ts
import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { JwtMiddleware} from './jwt/jwt.middleware'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes( new ValidationPipe() ); app.use(JwtMiddleware); await app.listen(3000); } bootstrap();
=> 이러면 오류남! (app.use에는 function만!)
-
app.module.ts
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { GraphQLModule } from '@nestjs/graphql'; import {TypeOrmModule} from "@nestjs/typeorm"; import * as Joi from 'joi'; //타입스크립트나 NestJS로 되어있지 않을때 패키지 import import { UsersModule } from './users/users.module'; import { CommonModule } from './common/common.module'; import { User } from './users/entities/user.entity'; import { JwtModule } from './jwt/jwt.module'; import { JwtMiddleware } from './jwt/jwt.middleware'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: process.env.NODE_ENV === "dev" ? ".env.dev" : ".env.test", ignoreEnvFile: process.env.NODE_ENV ==="prod", //production환경일땐 ConfigModule이 환경변수 파일 validationSchema: Joi.object({ NODE_ENV: Joi.string() .valid('dev', 'prod') .required(), // 환경변수 유효성 검사 DB_HOST: Joi.string().required(), DB_PORT: Joi.string().required(), DB_USERNAME: Joi.string().required(), DB_PASSWORD: Joi.string().required(), DB_NAME: Joi.string().required(), PRIVATE_KEY: Joi.string().required(), //token 지정을 위해 사용하는 privateKey }), }), TypeOrmModule.forRoot({ type: 'postgres', host: process.env.DB_HOST, port: +process.env.DB_PORT, username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, //localhost인 경우엔 안써도 됨 database: process.env.DB_NAME, synchronize: process.env.NODE_ENV ==="prod", //production이 아니면 true로 logging: process.env.NODE_ENV ==="prod", //DB에 돌아가는 모든 로그 확인 entities:[User], }), GraphQLModule.forRoot({ //dynamic module로 설정이 존재 autoSchemaFile: true, //root module 설정 }), UsersModule, CommonModule, JwtModule.forRoot({ privateKey: process.env.PRIVATE_KEY, }), //JwtModule처럼 static module은 어떠한 설정도 적용되어 있지않음 ], controllers: [], providers: [], }) export class AppModule implements NestModule{ configure(consumer: MiddlewareConsumer){ consumer .apply(JwtMiddleware) .forRoutes({path:"/graphql", method: RequestMethod.ALL}); } }
-
main.ts
import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes( new ValidationPipe() ); await app.listen(3000); } bootstrap();
-
jwt.middleware.ts
import { NestMiddleware } from "@nestjs/common"; import { NextFunction, Request, Response } from "express"; export class JwtMiddleware implements NestMiddleware{ use(req:Request, res: Response, next:NextFunction){ if("x-jwt" in req.headers){ console.log(req.headers["x-jwt"]); } next(); } } //NestMiddleware로 부터 상속 -> interface처럼 행동
=> token 추출
-
jwt.middleware.ts
-> verify로 올바른 토큰인지 확인 & 암호해독
import { Injectable, NestMiddleware } from "@nestjs/common"; import { NextFunction, Request, Response } from "express"; import { JwtService } from "./jwt.service"; @Injectable() export class JwtMiddleware implements NestMiddleware{ constructor(private readonly jwtService: JwtService){} //injectable일때만 inject할 수 있음 use(req:Request, res: Response, next:NextFunction){ if("x-jwt" in req.headers){ const token =(req.headers["x-jwt"]); const decoded = this.jwtService.verify(token.toString()); if(typeof decoded ==="object" && decoded.hasOwnProperty('id')){ console.log(decoded['id']); } } next(); } } //NestMiddleware로 부터 상속 -> interface처럼 행동
=> id 출력
-
users.service.ts
import { Injectable, Query, RequestMethod } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { CreateAccountInput } from "./dtos/create-account.dto"; import { LoginInput } from "./dtos/login.dto"; import { User } from "./entities/user.entity"; import * as jwt from 'jsonwebtoken'; import { ConfigService } from "@nestjs/config"; import { JwtService } from "src/jwt/jwt.service"; @Injectable() export class UsersService{ constructor( @InjectRepository(User) private readonly users: Repository <User>,//User entity의 InjectRepository 불러오기 & type이 repository이고 repository type은 user enitity private readonly jwtService: JwtService, //nestjs는 클래스 타입만 보고 import 알아서 찾아줌 ){} async createAccount({ email, password, role, }: CreateAccountInput): Promise <{ok: boolean, error? :string }>{ //check new user(that email does not exist) try { const exists = await this.users.findOne({email}) //findOne = 주어진 condition(환경)과 일치하는 첫 번째 entity 찾기 if(exists){ //make error return {ok: false, error: 'There is a uwer with that email already'}; //boolean =false, error ="there~" } await this.users.save(this.users.create({email, password, role})); //없다면 새로운 계정 create & save return {ok: true}; } catch(e){ return {ok: false, error: "Couldn't create account"}; } // create user & hash the password } async login({ email, password }: LoginInput): Promise<{ok: boolean; error?:string, token?: string}> { //make a JWT and give it to the user try{ // find the user with the email const user = await this.users.findOne({ email }); if(!user){ //user가 존재하지 않는다면 return { ok:false, error: 'User not found', } } //check if the password is correct //비밀번호를 hash 한후 데이터베이스에 있는 hash된 비번과 같은지 확인 const passwordCorrect = await user.checkPassword(password); //여기의 user와 위의 const user와는 다름.. 전자는 entity if (!passwordCorrect){ return{ ok:false, error:"Wrong password", }; } const token = this.jwtService.sign(user.id);//이 module을 내 백엔드만으로 특정함 return{ ok:true, token, } }catch(error){ return{ ok: false, error, }; } } async findById(id:number): Promise<User>{ return this.users.findOne({id}); } }
-
jwtmiddleware.ts
import { Injectable, NestMiddleware } from "@nestjs/common"; import { NextFunction, Request, Response } from "express"; import { UsersService } from "src/users/users.service"; import { JwtService } from "./jwt.service"; @Injectable() export class JwtMiddleware implements NestMiddleware{ constructor( private readonly jwtService: JwtService, private readonly userService: UsersService ){} //injectable일때만 inject할 수 있음 use(req:Request, res: Response, next:NextFunction){ if("x-jwt" in req.headers){ const token =(req.headers["x-jwt"]); const decoded = this.jwtService.verify(token.toString()); if(typeof decoded ==="object" && decoded.hasOwnProperty('id')){ console.log(decoded['id']); } } next(); } } //NestMiddleware로 부터 상속 -> interface처럼 행동
-
user.module.ts
-> UsersService를 export 해주기
import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { JwtService } from 'src/jwt/jwt.service'; import { User } from './entities/user.entity'; import { UsersResolver } from './users.resolver'; import { UsersService } from './users.service'; @Module({ imports: [TypeOrmModule.forFeature([User])], //service는 repository 필요로 하기 떄문에 // ConfigService는 token 생성할 때 import해주려고 users.module안에 configService 추가 providers:[UsersResolver, UsersService], exports:[UsersService] }) export class UsersModule {}
-
jwt.middleware.ts
import { Injectable, NestMiddleware } from "@nestjs/common"; import { NextFunction, Request, Response } from "express"; import { UsersService } from "src/users/users.service"; import { JwtService } from "./jwt.service"; @Injectable() export class JwtMiddleware implements NestMiddleware{ constructor( private readonly jwtService: JwtService, private readonly userService: UsersService ){} //injectable일때만 inject할 수 있음 async use(req:Request, res: Response, next:NextFunction){ if("x-jwt" in req.headers){ const token =(req.headers["x-jwt"]); const decoded = this.jwtService.verify(token.toString()); if(typeof decoded ==="object" && decoded.hasOwnProperty('id')){ try{ const user = await this.userService.findById(decoded['id']); req['user'] = user; } catch(e){ } } } next(); } } //NestMiddleware로 부터 상속 -> interface처럼 행동
-> user 찾기
2.7
-> graphql로 request 공유하기 => HTTP request를 graphql resolver에 전달
jwt.middleware의 ruser request는 모든 resolver에서 공유 가능
-
app.module.ts
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { GraphQLModule } from '@nestjs/graphql'; import {TypeOrmModule} from "@nestjs/typeorm"; import * as Joi from 'joi'; //타입스크립트나 NestJS로 되어있지 않을때 패키지 import import { UsersModule } from './users/users.module'; import { CommonModule } from './common/common.module'; import { User } from './users/entities/user.entity'; import { JwtModule } from './jwt/jwt.module'; import { JwtMiddleware } from './jwt/jwt.middleware'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: process.env.NODE_ENV === "dev" ? ".env.dev" : ".env.test", ignoreEnvFile: process.env.NODE_ENV ==="prod", //production환경일땐 ConfigModule이 환경변수 파일 validationSchema: Joi.object({ NODE_ENV: Joi.string() .valid('dev', 'prod') .required(), // 환경변수 유효성 검사 DB_HOST: Joi.string().required(), DB_PORT: Joi.string().required(), DB_USERNAME: Joi.string().required(), DB_PASSWORD: Joi.string().required(), DB_NAME: Joi.string().required(), PRIVATE_KEY: Joi.string().required(), //token 지정을 위해 사용하는 privateKey }), }), TypeOrmModule.forRoot({ type: 'postgres', host: process.env.DB_HOST, port: +process.env.DB_PORT, username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, //localhost인 경우엔 안써도 됨 database: process.env.DB_NAME, synchronize: process.env.NODE_ENV ==="prod", //production이 아니면 true로 logging: process.env.NODE_ENV ==="prod", //DB에 돌아가는 모든 로그 확인 entities:[User], }), GraphQLModule.forRoot({ //dynamic module로 설정이 존재 autoSchemaFile: true, //root module 설정 context: ({req}) => ({user: req["user"]}), //graphql resolver의 context를 통해 공유 }), UsersModule, CommonModule, JwtModule.forRoot({ privateKey: process.env.PRIVATE_KEY, }), //JwtModule처럼 static module은 어떠한 설정도 적용되어 있지않음 ], controllers: [], providers: [], }) export class AppModule implements NestModule{ configure(consumer: MiddlewareConsumer){ consumer .apply(JwtMiddleware) .forRoutes({path:"/graphql", method: RequestMethod.ALL}); } }
-> graphql resolver의 context를 통해 공유
-
app.module.ts
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { GraphQLModule } from '@nestjs/graphql'; import {TypeOrmModule} from "@nestjs/typeorm"; import * as Joi from 'joi'; //타입스크립트나 NestJS로 되어있지 않을때 패키지 import import { UsersModule } from './users/users.module'; import { CommonModule } from './common/common.module'; import { User } from './users/entities/user.entity'; import { JwtModule } from './jwt/jwt.module'; import { JwtMiddleware } from './jwt/jwt.middleware'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: process.env.NODE_ENV === "dev" ? ".env.dev" : ".env.test", ignoreEnvFile: process.env.NODE_ENV ==="prod", //production환경일땐 ConfigModule이 환경변수 파일 validationSchema: Joi.object({ NODE_ENV: Joi.string() .valid('dev', 'prod') .required(), // 환경변수 유효성 검사 DB_HOST: Joi.string().required(), DB_PORT: Joi.string().required(), DB_USERNAME: Joi.string().required(), DB_PASSWORD: Joi.string().required(), DB_NAME: Joi.string().required(), PRIVATE_KEY: Joi.string().required(), //token 지정을 위해 사용하는 privateKey }), }), TypeOrmModule.forRoot({ type: 'postgres', host: process.env.DB_HOST, port: +process.env.DB_PORT, username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, //localhost인 경우엔 안써도 됨 database: process.env.DB_NAME, synchronize: process.env.NODE_ENV ==="prod", //production이 아니면 true로 logging: process.env.NODE_ENV ==="prod", //DB에 돌아가는 모든 로그 확인 entities:[User], }), GraphQLModule.forRoot({ //dynamic module로 설정이 존재 autoSchemaFile: true, //root module 설정 context: ({req}) => ({user: req["user"]}), //graphql resolver의 context를 통해 공유 }), UsersModule, CommonModule, JwtModule.forRoot({ privateKey: process.env.PRIVATE_KEY, }), //JwtModule처럼 static module은 어떠한 설정도 적용되어 있지않음 ], controllers: [], providers: [], }) export class AppModule implements NestModule{ configure(consumer: MiddlewareConsumer){ consumer .apply(JwtMiddleware) .forRoutes({path:"/graphql", method: RequestMethod.ALL}); } }
=> 터미널에 request정보
- token 보내기
- token이 request로 보내짐
- JwtMiddleware가 받음 -> JwtMiddleware가 token 찾고 request user에 넣어줌
- request가 GraphQLModule의 context로 들어감 (context는 매 request마다 호출)(-> context함수 호출시 HTTP request property 주어짐)
-
users.resolver.ts
@Query(returns => User) me(@Context() context){ if(!context.user){ return; //에러 반환 } else{ return context.user; } }//middleware 구현 //로그인한 사람이 누구인지 반환 }
2.8
-> guard로 request 멈추기
-
auth.guard.ts
-> guard로 request를 다음단계로 진행할 지 결정
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; @Injectable() export class AuthGuard implements CanActivate{ canActivate(context: ExecutionContext){ console.log(context); return false; //request 막기 } }// CanActivate는 true를 return 하면 request 진행
-
users.resolver.ts
@Query(returns => User) @UseGuards(AuthGuard) me() {}//middleware 구현 //로그인한 사람이 누구인지 반환
-
auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; import { GqlExecutionContext } from "@nestjs/graphql"; @Injectable() export class AuthGuard implements CanActivate{ canActivate(context: ExecutionContext){ const gqlContext = GqlExecutionContext.create(context).getContext(); const user = gqlContext['user']; if(!user){ return false; } return true; //request 막기 } }// CanActivate는 true를 return 하면 request 진행
=>guard로 endpoint 보호
- authentication: 누가 자원을 요청하는 지 확인(permission 확인)
2.9
-> login 안됐으면 request 멈추기
-
auth.decorator
-> 누구인지 확인하는 decorator 만들기
import { createParamDecorator, ExecutionContext } from "@nestjs/common" import { GqlExecutionContext } from "@nestjs/graphql"; export const AuthUser = createParamDecorator( (data:unknown, context: ExecutionContext) => { const gqlContext = GqlExecutionContext.create(context).getContext(); const user = gqlContext['user']; return user; } )
-
users.resolver.ts
@Query(returns => User) @UseGuards(AuthGuard) me(@AuthUser() authUser: User){ return authUser; }//middleware 구현 //로그인한 사람이 누구인지 반환 }
2.10
authentication 작동 순서
- header에 token 보내기
- header는 http기술 쓰기 때문에 middleware 만듦
- middleware는 header에 jwtService.verify() 사용
- id찾으면 userService로 해당 id를 가진 user 찾기
- user찾으면 request object에 붙여서 보냄
- request object가 graphql context안으로 들어감
- guard 가 graphql context를 찾음
- user 유무에 따라 bool값 반환
- guard에 의해 request가 authorize되면 resolver에 decorater 필요
- decorator는 graphql context에서 찾은 user를 찾아 return
2.11
-
users.resolver.ts
-> user의 profile을 볼 수 있는 query 추가
@UseGuards(AuthGuard) @Query(returns => User) user(id){ //user id가져오기 return id; }
-
user-profile.dto.ts
import { ArgsType, Field } from "@nestjs/graphql"; @ArgsType() export class UserProfileInput{ @Field(type=>Number) userId: number; }
-
user-profile.dto.ts
-> 나중에 MutationOutput을 CoreOutput으로 변경하기
import { ArgsType, Field, ObjectType } from "@nestjs/graphql"; import { MutationOutput } from "src/common/dtos/output.dto"; import { User } from "../entities/user.entity"; @ArgsType() export class UserProfileInput{ @Field(type=>Number) userId: number; } @ObjectType() export class UserProfileOutput extends MutationOutput{ @Field(type => User, {nullable: true}) user?: User; //users.resolver.ts에서 }
-
users.resolver.ts
@UseGuards(AuthGuard) @Query(returns => UserProfileOutput) async userProfile(@Args()userProfileInput: UserProfileInput): Promise<UserProfileOutput>{ //user id가져오기 try { const user = await this.usersService.findById(userProfileInput.userId) if (!user){ throw Error(); //user 찾지 못하면 error로 } return{ ok:true, user, }; }catch(e){ return{ error:"User Not Found", ok: false, } } ; }
2.12
-
edit-profile.ts
-> Mutation의 dto만들기
import { InputType, ObjectType, PartialType, PickType } from "@nestjs/graphql"; import { CoreOutput } from "src/common/dtos/output.dto"; import { User } from "../entities/user.entity"; @ObjectType() export class EditProfileOutput extends CoreOutput {} @InputType() export class EditProfileInput extends PartialType( PickType(User, ["email","password"]),//user에서 email과 password가지고 class 생성 & PartialType으로 optional하게 ){}
-
users.service.ts
async editProfile(userId: number, {email, password}: EditProfileInput){ return this.users.update(userId, {email, password}) //db에 entity 존재 유무 체크 안함 }
-
users.resolver.ts
@UseGuards(AuthGuard) @Mutation(returns=> EditProfileOutput) async editProfile(@AuthUser() authUser: User, @Args('input') editProfileInput: EditProfileInput, ): Promise<EditProfileOutput>{ try{ await this.usersService.editProfile(authUser.id, editProfileInput); }catch(error) { return { ok: false, error } } }
-> 이것처럼 하면 아이디나 비번 둘 중 하나만 고치면 에러가남..
-
user.service.ts
async editProfile(userId: number, editProfileInput: EditProfileInput){ return this.users.update(userId, {...editProfileInput}) //db에 entity 존재 유무 체크 안함 }
-> ediprofileInput으로 하나만 전송해도 되도록
-
users.resolver.ts
@UseGuards(AuthGuard) @Mutation(returns=> EditProfileOutput) async editProfile(@AuthUser() authUser: User, @Args('input') editProfileInput: EditProfileInput, ): Promise<EditProfileOutput>{ try{ await this.usersService.editProfile(authUser.id, editProfileInput); return { ok:true, } }catch(error) { return { ok: false, error } } }
=> 근데 패스워드는 edit하면 해쉬가 안됨
-
user.entity.ts
-> BeforUpdate decorator 사용
@BeforeInsert()//DB에 저장하기 전에 password hash해주기 @BeforeUpdate()//password 저장하기 전에 hash하도록 async hashPassword(): Promise<void> { try{ this.password = await bcrypt.hash(this.password, 10); //hash round는 10으로 } catch(e){ console.log(e); throw new InternalServerErrorException(); } }
-
user.service.ts
-> update는 dv에 query만 보내지 entity를 update하진 않음 -> BeforeUpdate못함
=> save쓰기
async editProfile(userId: number, {email, password}: EditProfileInput){ const user = await this.users.findOne(userId); if(email){ user.email = email } if(password){ user.password = password } return this.users.save(user) //db에 entity 존재 유무 체크 안함
-
delete도 만들자