항해 99 77일차 12/04일
WIL
커스텀 에러 :
처음 작성된 코드는 내장된 Error의 인터페이스를 불러와서 상속을 받고 재선언 후 사용하고 있는데 너무 비효율 적이고 이미 내장된 코드를 한번 더 만든다는것 자체가 좋지 않은 코드였다.(이유에는 2022버전을 사용해서 내장된 Error인터페이스가 다른것을 보고있는 문제)
해결 : ts.config에서 자바 버전 2022를 삭제 한 후 Error를 extends해서 없는 status를 타입을 명시 해줬다.
export class ValidationErrors extends Error {
public status:number
constructor(message: string, status?: number) {
super(message);
this.status = status || 412;
this.name = 'ValidationError';
}
}
jwt any 타입 수정 : 기존 코드는 토큰 검증을 할 때 promise<any>를 사용해서 반환했다. any 사용 해서 반환을 하기 때문에
검증해서 받은 값에 userId가 any타입으로 정확한 타입을 알기 어려웠다.
//jwt.ts
validateRefreshToken: async (refreshToken: string):promise<any> => {
try {
const Payload = jwt.verify(refreshToken, process.env.JWT_KEY!)
return Payload
} catch (error) {
return false;
}
},
//authmiddleware.ts
const decodeRefreshToken = await jwt.validateRefreshToken(refreshToken);
//리프레쉬 토큰 만료시
if (decodeRefreshToken == false) throw new Unauthorized('RefreshToken이 일치하지 않거나 만료 되었습니다.');
const userId = decodeRefreshToken.userId;
1 . promise<string | jwt.Payload | boolean>타입으로 변경 검증 받은 값에 userId라는 속성이 없어서 타입에러 발생.
2. jwt.Payload를 상속받은 interface를 만들어서 사용했지만 인터페이스에 userId를 넣어줬지만 속성이 없어서 타입에러 발생
--해결--
3. 검증 하는 함수에 as { userId: number } 로 타입 단언해서 사용 타입추론으로는 userId의 속성을 찾지 못해서 단언해서 반환값을 타입을 선언해주었다.
validateRefreshToken: async (refreshToken: string) => {
try {
const Payload = jwt.verify(refreshToken, process.env.JWT_KEY!) as { userId: number }
return Payload
} catch (error) {
return false;
}
},
Joi Validator => class Validator 코드 수정:
기존에는 타입을 Joi Validator를 사용 했는데 이메일 검증 api와 로그인 회원가입할 때 api와 req값이 다르기 떄문에 코드의 중복이 발생했다.
import Joi from 'joi';
import { validate, validateOrReject ,IsEmail, IsString, Matches} from 'class-validator'
//회원가입 검사
export const signSchema = Joi.object({
email: Joi.string()
.pattern(/^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w+[.]?\w{2,3}/)
.message("이메일 형식이 틀렸습니다.")
.required(),
password: Joi.string()
.pattern(/^(?=.*[A-Z].*[a-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,20}$/)
.message("비밀번호 형식이 틀렸습니다!.")
.required(),
});
//이메일 중복검사
export const emailSchema = Joi.object({
email: Joi.string()
.pattern(/^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w+[.]?\w{2,3}/)
.message("이메일 형식이 틀렸습니다.")
.required()
})
데코레이터를 이용해서 편리하게 오브젝트의 프로퍼티를 검증 할 수 있고 { skipMissingProperties: true }라는 것을 이용해서 입력 받지 않은 값은 통과를 시켜서 중복코드를 줄일 수 있는 장점이 있고
타입스크립트에서도 많이 사용하는 class Validator로 사용 하기로 했다.
1.
//user.ts
import {validateBody} from '../../utils/validation';
import {createUserDto} from '../../utils/validation'
const router = Router();
//회원가입 중복체크
router.post('/signup/check',validateBody(createUserDto),User.signupcheck)
//회원가입
router.post('/signup', validateBody(createUserDto),User.signup);
//로그인
router.post('/login', validateBody(createUserDto),User.login);
//validation.ts
export class createUserDto{
@IsString()
@IsEmail()
@Matches(/^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w+[.]?\w{2,3}/,{
message : "이메일 형식이 틀렸습니다."
})
email?:string
@IsString()
@Matches(/^(?=.*[A-Z].*[a-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,20}$/,{
message : "비밀번호 형식이 틀렸습니다."
})
password?: string
}
export function validateBody(schema:{new() : object}){
return async function (req:Request,res:Response,next:NextFunction){
const target:object = plainToClass(schema, req.body)
try{
await validateOrReject(target,{ skipMissingProperties: true })
next()
}catch(err){
res.status(400).send(err)
next(err)
}
}
위의 코드는 처음 써본 코드인데
- 유저라우터에서 클래스와 함수를 불러와서 함수안에 createUserDto를 넣는다
-validateBody라는 함수가 동작하며 createUserDto class를 인스턴스화 시켜서 검증하는 방법이다.
솔직히 다른사람이 쓴 코드를 바로 써본거기 떄문에 이해하는게 너무 어려웠고 schema:{new() :object} 이부분을 이해하기가 너무 힘들었다.
2. 1번의 방법이 이해하기가 너무 어려웠기 때문에 다음과 같이 코드를 수정했다.
미들웨어를 만들어서 로그인이나 회원가입 이메일중복 검사 api에 도착하기전에 검증하는 부분을 거쳐서 검증 완료되면 next()로 이동하게 만들었다.
//^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w+[.]?\w{2,3} ex) youwa65@dddd.dd.fd
//^(?=.*[A-Z].*[a-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,20}$ 특수문자 대문자 소문사 숫자 하나이상 필요8-20사이
import { validateOrReject ,IsEmail, IsString, Matches} from 'class-validator'
import { plainToClass } from 'class-transformer';
import { NextFunction, Request, Response } from 'express';
export default async (req:Request,res:Response,next:NextFunction)=>{
class userDto{
@IsString()
@IsEmail()
@Matches(/^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w+[.]?\w{2,3}/,{
message : "이메일 형식이 틀렸습니다."
})
email?:string
@IsString()
@Matches(/^(?=.*[A-Z].*[a-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,20}$/,{
message : "비밀번호 형식이 틀렸습니다."
})
password?: string
@IsString()
@Matches(/^(?=.*[A-Z].*[a-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,20}$/,{
message : "비밀번호 형식이 틀렸습니다."
})
changePassword?: string
}
/*
plainToClass <= plain object를 class object로 변환(DTO)
validateOrReject <= 반환값이 프로미스이며 유효성 검증 실패시 에러발생
skipMissingProperties <= true일시 누락된건 스킵하고 검사
DTO <= 로직을 가지지 않는 순수한 데이터 객체
*/
const target = plainToClass(userDto, req.body)
try{
await validateOrReject(target,{ skipMissingProperties: true })
next()
}catch(err){
res.status(400).send(err)
console.log(err)
}
}
req.body로 {"email" : "123@naver.com"}을 입력한 값을 받으면
plainToClass라는 함수를 이용해서 userDto의 클래스 object로 변환 시킨다. userDto:{"email" : "123@naver.com"}
그후 validateOrReject 함수를 사용해서 변환 시킨 target을 검사한다 통과를 완료하면 next()로 넘어가고 그렇지 않으면 error를 뱉어 내는 코드다.