From 40dea070272d7aabdcc3c09468ce48792f50b176 Mon Sep 17 00:00:00 2001 From: namjiyun Date: Sat, 19 Nov 2022 15:55:33 +0900 Subject: [PATCH] =?UTF-8?q?6=EC=B0=A8=20=EC=84=B8=EB=AF=B8=EB=82=98=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EA=B3=BC=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar4/package.json | 10 ++- seminar4/prisma/schema.prisma | 9 +- seminar4/src/constants/index.ts | 3 + seminar4/src/constants/response.ts | 16 ++++ seminar4/src/constants/responseMessage.ts | 33 +++++++ seminar4/src/constants/statusCode.ts | 15 ++++ seminar4/src/constants/tokenType.ts | 4 + seminar4/src/controller/userController.ts | 67 +++++++++++++- seminar4/src/interfaces/UserCreateDTO.ts | 6 ++ seminar4/src/interfaces/UserSignInDTO.ts | 4 + seminar4/src/middlewares/auth.ts | 30 +++++++ seminar4/src/middlewares/index.ts | 1 + seminar4/src/modules/jwtHandler.ts | 37 ++++++++ seminar4/src/router/userRouter.ts | 7 +- seminar4/src/service/userService.ts | 54 ++++++++++-- seminar4/yarn.lock | 103 +++++++++++++++++++++- 16 files changed, 378 insertions(+), 21 deletions(-) create mode 100644 seminar4/src/constants/index.ts create mode 100644 seminar4/src/constants/response.ts create mode 100644 seminar4/src/constants/responseMessage.ts create mode 100644 seminar4/src/constants/statusCode.ts create mode 100644 seminar4/src/constants/tokenType.ts create mode 100644 seminar4/src/interfaces/UserCreateDTO.ts create mode 100644 seminar4/src/interfaces/UserSignInDTO.ts create mode 100644 seminar4/src/middlewares/auth.ts create mode 100644 seminar4/src/middlewares/index.ts create mode 100644 seminar4/src/modules/jwtHandler.ts diff --git a/seminar4/package.json b/seminar4/package.json index c08566f..7ebef16 100644 --- a/seminar4/package.json +++ b/seminar4/package.json @@ -5,17 +5,25 @@ "license": "MIT", "scripts": { "dev": "nodemon", - "build": "tsc && node dist" + "build": "tsc && node dist", + "db:pull": "npx prisma db pull", + "db:push": "npx prisma db push", + "generate": "npx prisma generate" }, "devDependencies": { + "@types/bcryptjs": "^2.4.2", "@types/express": "^4.17.14", + "@types/jsonwebtoken": "^8.5.9", "@types/node": "^18.11.9", "nodemon": "^2.0.20" }, "dependencies": { "@prisma/client": "^4.5.0", "@types/express-validator": "^3.0.0", + "bcryptjs": "^2.4.3", "express": "^4.18.2", + "express-validator": "^6.14.2", + "jsonwebtoken": "^8.5.1", "prisma": "^4.5.0" } } diff --git a/seminar4/prisma/schema.prisma b/seminar4/prisma/schema.prisma index cbbb347..fa39e92 100644 --- a/seminar4/prisma/schema.prisma +++ b/seminar4/prisma/schema.prisma @@ -8,8 +8,9 @@ datasource db { } model User { - id Int @id(map: "user_pk") @unique(map: "user_id_uindex") @default(autoincrement()) - userName String + id Int @id(map: "user_pk") @unique(map: "user_id_uindex") @default(autoincrement()) + userName String @db.VarChar(100) age Int? - email String? @db.VarChar(400) -} + email String @db.VarChar(400) + password String @db.VarChar(400) +} \ No newline at end of file diff --git a/seminar4/src/constants/index.ts b/seminar4/src/constants/index.ts new file mode 100644 index 0000000..320475c --- /dev/null +++ b/seminar4/src/constants/index.ts @@ -0,0 +1,3 @@ +export { default as rm } from './responseMessage'; +export { default as sc } from './statusCode'; +export { default as tokenType } from './tokenType'; diff --git a/seminar4/src/constants/response.ts b/seminar4/src/constants/response.ts new file mode 100644 index 0000000..1441aab --- /dev/null +++ b/seminar4/src/constants/response.ts @@ -0,0 +1,16 @@ +export const success = (status: number, message: string, data?: any) => { + return { + status, + success: true, + message, + data, + }; +}; + +export const fail = (status: number, message: string) => { + return { + status, + success: false, + message, + }; +}; diff --git a/seminar4/src/constants/responseMessage.ts b/seminar4/src/constants/responseMessage.ts new file mode 100644 index 0000000..a0d8858 --- /dev/null +++ b/seminar4/src/constants/responseMessage.ts @@ -0,0 +1,33 @@ +export default { + NULL_VALUE: '필요한 값이 없습니다.', + OUT_OF_VALUE: '파라미터 값이 잘못되었습니다.', + NOT_FOUND: '잘못된 경로입니다.', + BAD_REQUEST: '잘못된 요청입니다.', + + // 회원가입 및 로그인 + SIGNUP_SUCCESS: '회원 가입 성공', + SIGNUP_FAIL: '회원 가입 실패', + SIGNIN_SUCCESS: '로그인 성공', + SIGNIN_FAIL: '로그인 실패', + ALREADY_NICKNAME: '이미 사용중인 닉네임입니다.', + + // 유저 + READ_USER_SUCCESS: '유저 조회 성공', + READ_ALL_USERS_SUCCESS: '모든 유저 조회 성공', + UPDATE_USER_SUCCESS: '유저 수정 성공', + DELETE_USER_SUCCESS: '유저 탈퇴 성공', + DELETE_USER_FAIL: '유저 탈퇴 실패', + NO_USER: '탈퇴했거나 가입하지 않은 유저입니다.', + + // 토큰 + CREATE_TOKEN_SUCCESS: '토큰 재발급 성공', + EXPIRED_TOKEN: '토큰이 만료되었습니다.', + EXPIRED_ALL_TOKEN: '모든 토큰이 만료되었습니다.', + INVALID_TOKEN: '유효하지 않은 토큰입니다.', + VALID_TOKEN: '유효한 토큰입니다.', + EMPTY_TOKEN: '토큰 값이 없습니다.', + + // 서버 내 오류 + INTERNAL_SERVER_ERROR: '서버 내 오류', + INVALID_PASSWORD: '잘못된 비밀번호입니다.', +}; diff --git a/seminar4/src/constants/statusCode.ts b/seminar4/src/constants/statusCode.ts new file mode 100644 index 0000000..c9f04ce --- /dev/null +++ b/seminar4/src/constants/statusCode.ts @@ -0,0 +1,15 @@ +export default { + OK: 200, // 목록, 상세, 수정 성공 + CREATED: 201, // POST나 PUT으로 데이터 등록할 경우 사용 + // 어떠한 생성 작업을 요청받아 생성 작업을 성공 + ACCEPTED: 202, // 요청은 받았지만, 아직 동작을 수행하지 않은 상태로 요청이 적절함을 의미한다. + NO_CONTENT: 204, // 요청이 성공은 했지만 응답할 콘텐츠가 없을 경우를 뜻한다. + BAD_REQUEST: 400, // 클라이언트가 올바르지 못한 요청을 보내고 있음을 의미 + UNAUTHORIZED: 401, // 로그인을 하지 않아 권한이 없음. 권한 인증 요구 + FORBIDDEN: 403, // 요청이 서버에 의해 거부 되었음을 의미. 금지된 페이지 + NOT_FOUND: 404, // 요청한 URL을 찾을 수 없음을 의미 + CONFLICT: 409, // 클라이언트 요청에 대해 서버에서 충돌 요소가 발생 할수 있음을 의미 + INTERNAL_SERVER_ERROR: 500, // 서버에 오류가 발생하여 응답 할 수 없음을 의미 + SERVICE_UNAVAILABLE: 503, // 현재 서버가 유지보수 등의 이유로 일시적인 사용 불가함을 의미 + DB_ERROR: 600, +}; diff --git a/seminar4/src/constants/tokenType.ts b/seminar4/src/constants/tokenType.ts new file mode 100644 index 0000000..23cde90 --- /dev/null +++ b/seminar4/src/constants/tokenType.ts @@ -0,0 +1,4 @@ +export default { + TOKEN_EXPIRED: -3, + TOKEN_INVALID: -2, +}; diff --git a/seminar4/src/controller/userController.ts b/seminar4/src/controller/userController.ts index 269d528..0648064 100644 --- a/seminar4/src/controller/userController.ts +++ b/seminar4/src/controller/userController.ts @@ -1,5 +1,10 @@ import { Request, Response } from 'express'; +import { validationResult } from 'express-validator'; +import { rm, sc } from '../constants'; +import { fail, success } from '../constants/response'; import { UserCreateDTO } from '../DTO/userDTO'; +import { UserSignInDTO } from '../interfaces/UserSignInDTO'; +import jwtHandler from '../modules/jwtHandler'; import { userService } from '../service'; const getUserById = async (req: Request, res: Response) => { @@ -13,14 +18,37 @@ const getUserById = async (req: Request, res: Response) => { return res.status(200).json({ status: 200, message: '유저 조회 성공', data }); }; +// const createUser = async (req: Request, res: Response) => { +// const userCreateDTO: UserCreateDTO = req.body; +// const data = await userService.createUser(userCreateDTO); + +// if (!data) { +// return res.status(404).json({ status: 404, message: 'NOT_FOUND' }); +// } +// return res.status(200).json({ status: 200, message: '유저 생성 성공', data }); +// }; + const createUser = async (req: Request, res: Response) => { - const userCreateDTO: UserCreateDTO = req.body; - const data = await userService.createUser(userCreateDTO); + //? validation의 결과를 바탕으로 분기 처리 + const error = validationResult(req); + if (!error.isEmpty()) { + return res.status(sc.BAD_REQUEST).send(fail(sc.BAD_REQUEST, rm.BAD_REQUEST)); + } + //? 기존 비구조화 할당 방식 -> DTO의 형태 + const userCreateDto: UserCreateDTO = req.body; + const data = await userService.createUser(userCreateDto); if (!data) { - return res.status(404).json({ status: 404, message: 'NOT_FOUND' }); + return res.status(sc.BAD_REQUEST).send(fail(sc.BAD_REQUEST, rm.SIGNUP_FAIL)); } - return res.status(200).json({ status: 200, message: '유저 생성 성공', data }); + const accessToken = jwtHandler.sign(data.id); + + const result = { + id: data.id, + name: data.userName, + accessToken, + }; + return res.status(sc.CREATED).send(success(sc.CREATED, rm.SIGNUP_SUCCESS, data)); }; const getAllUser = async (req: Request, res: Response) => { @@ -54,12 +82,43 @@ const deleteUser = async (req: Request, res: Response) => { } return res.status(200).json({ status: 200, message: '유저 삭제 성공', data }); }; + +const signInUser = async (req: Request, res: Response) => { + const error = validationResult(req); + if (!error.isEmpty()) { + return res.status(sc.BAD_REQUEST).send(fail(sc.BAD_REQUEST, rm.BAD_REQUEST)); + } + + const userSignInDto: UserSignInDTO = req.body; + + try { + const userId = await userService.signIn(userSignInDto); + + if (!userId) return res.status(sc.NOT_FOUND).send(fail(sc.NOT_FOUND, rm.NOT_FOUND)); + else if (userId === sc.UNAUTHORIZED) return res.status(sc.UNAUTHORIZED).send(fail(sc.UNAUTHORIZED, rm.INVALID_PASSWORD)); + + const accessToken = jwtHandler.sign(userId); + + const result = { + id: userId, + accessToken, + }; + + res.status(sc.OK).send(success(sc.OK, rm.SIGNIN_SUCCESS, result)); + } catch (e) { + console.log(error); + //? 서버 내부에서 오류 발생 + res.status(sc.INTERNAL_SERVER_ERROR).send(fail(sc.INTERNAL_SERVER_ERROR, rm.INTERNAL_SERVER_ERROR)); + } +}; + const userController = { getUserById, createUser, getAllUser, updateUser, deleteUser, + signInUser, }; export default userController; diff --git a/seminar4/src/interfaces/UserCreateDTO.ts b/seminar4/src/interfaces/UserCreateDTO.ts new file mode 100644 index 0000000..17d77c9 --- /dev/null +++ b/seminar4/src/interfaces/UserCreateDTO.ts @@ -0,0 +1,6 @@ +export interface UserCreateDTO { + name: string; + age: number; + email: string; + password: string; +} diff --git a/seminar4/src/interfaces/UserSignInDTO.ts b/seminar4/src/interfaces/UserSignInDTO.ts new file mode 100644 index 0000000..47fde5a --- /dev/null +++ b/seminar4/src/interfaces/UserSignInDTO.ts @@ -0,0 +1,4 @@ +export interface UserSignInDTO { + email: string; + password: string; +} diff --git a/seminar4/src/middlewares/auth.ts b/seminar4/src/middlewares/auth.ts new file mode 100644 index 0000000..642a093 --- /dev/null +++ b/seminar4/src/middlewares/auth.ts @@ -0,0 +1,30 @@ +import { NextFunction, Request, Response } from 'express'; +import { JwtPayload } from 'jsonwebtoken'; +import { rm, sc } from '../constants'; +import { fail } from '../constants/response'; +import tokenType from '../constants/tokenType'; +import jwtHandler from '../modules/jwtHandler'; + +export default async (req: Request, res: Response, next: NextFunction) => { + const token = req.headers.authorization?.split(' ').reverse()[0]; //? Bearer ~~ 에서 토큰만 파싱 + if (!token) return res.status(sc.UNAUTHORIZED).send(fail(sc.UNAUTHORIZED, rm.EMPTY_TOKEN)); + + try { + const decoded = jwtHandler.verify(token); //? jwtHandler에서 만들어둔 verify로 토큰 검사 + + //? 토큰 에러 분기 처리 + if (decoded === tokenType.TOKEN_EXPIRED) return res.status(sc.UNAUTHORIZED).send(fail(sc.UNAUTHORIZED, rm.EXPIRED_TOKEN)); + if (decoded === tokenType.TOKEN_INVALID) return res.status(sc.UNAUTHORIZED).send(fail(sc.UNAUTHORIZED, rm.INVALID_TOKEN)); + + //? decode한 후 담겨있는 userId를 꺼내옴 + const userId: number = (decoded as JwtPayload).userId; + if (!userId) return res.status(sc.UNAUTHORIZED).send(fail(sc.UNAUTHORIZED, rm.INVALID_TOKEN)); + + //? 얻어낸 userId 를 Request Body 내 userId 필드에 담고, 다음 미들웨어로 넘김( next() ) + req.body.userId = userId; + next(); + } catch (error) { + console.log(error); + res.status(sc.INTERNAL_SERVER_ERROR).send(fail(sc.INTERNAL_SERVER_ERROR, rm.INTERNAL_SERVER_ERROR)); + } +}; diff --git a/seminar4/src/middlewares/index.ts b/seminar4/src/middlewares/index.ts new file mode 100644 index 0000000..a2c97a3 --- /dev/null +++ b/seminar4/src/middlewares/index.ts @@ -0,0 +1 @@ +export { default as auth } from './auth'; diff --git a/seminar4/src/modules/jwtHandler.ts b/seminar4/src/modules/jwtHandler.ts new file mode 100644 index 0000000..dce5338 --- /dev/null +++ b/seminar4/src/modules/jwtHandler.ts @@ -0,0 +1,37 @@ +// src/modules/jwtHandler.ts +import jwt from 'jsonwebtoken'; +import { tokenType } from '../constants'; + +//* 받아온 userId를 담는 access token 생성 +const sign = (userId: number) => { + const payload = { + userId, + }; + + const accessToken = jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn: '2h' }); + return accessToken; +}; + +//* token 검사! +const verify = (token: string) => { + let decoded: string | jwt.JwtPayload; + + try { + decoded = jwt.verify(token, process.env.JWT_SECRET as string); + } catch (error: any) { + if (error.message === 'jwt expired') { + return tokenType.TOKEN_EXPIRED; + } else if (error.message === 'invalid token') { + return tokenType.TOKEN_INVALID; + } else { + return tokenType.TOKEN_INVALID; + } + } + + return decoded; +}; + +export default { + sign, + verify, +}; diff --git a/seminar4/src/router/userRouter.ts b/seminar4/src/router/userRouter.ts index 290ab76..7ea1591 100644 --- a/seminar4/src/router/userRouter.ts +++ b/seminar4/src/router/userRouter.ts @@ -1,14 +1,15 @@ import { Router } from 'express'; import { userController } from '../controller'; import { body } from 'express-validator'; +import { auth } from '../middlewares'; const { validatorErrorChecker } = require('../middlewares/validator'); const router: Router = Router(); -router.get('/:userId', userController.getUserById); +router.get('/:userId', auth, userController.getUserById); -router.post('/', body('name').notEmpty(), validatorErrorChecker, userController.createUser); +router.post('/', [body('name').notEmpty(), body('email').notEmpty(), body('password').isLength({ min: 6 })], userController.createUser); router.get('/', userController.getAllUser); router.patch('/:userId', body('name').notEmpty(), validatorErrorChecker, userController.updateUser); router.delete('/:userId', userController.deleteUser); - +router.post('/signin', [body('email').notEmpty(), body('email').isEmail(), body('password').notEmpty(), body('password').isLength({ min: 6 })], userController.signInUser); export default router; diff --git a/seminar4/src/service/userService.ts b/seminar4/src/service/userService.ts index 38dd29e..dba58e6 100644 --- a/seminar4/src/service/userService.ts +++ b/seminar4/src/service/userService.ts @@ -1,5 +1,8 @@ import { UserCreateDTO } from '../DTO/userDTO'; import { PrismaClient } from '@prisma/client'; +import bcrypt from 'bcryptjs'; +import { sc } from '../constants'; +import { UserSignInDTO } from '../interfaces/UserSignInDTO'; const prisma = new PrismaClient(); //* userId로 유저 조회 @@ -13,15 +16,53 @@ const getUserById = async (userId: number) => { return user; }; -const createUser = async (userCreateDTO: UserCreateDTO) => { - const user = await prisma.user.create({ +const signIn = async (userSignInDto: UserSignInDTO) => { + try { + const user = await prisma.user.findFirst({ + where: { + email: userSignInDto.email, + }, + }); + if (!user) return null; + + //? bcrypt가 DB에 저장된 기존 password와 넘겨 받은 password를 대조하고, + //? match false시 401을 리턴 + const isMatch = await bcrypt.compare(userSignInDto.password, user.password); + if (!isMatch) return sc.UNAUTHORIZED; + + return user.id; + } catch (error) { + console.log(error); + throw error; + } +}; + +// const createUser = async (userCreateDTO: UserCreateDTO) => { +// const user = await prisma.user.create({ +// data: { +// userName: userCreateDTO.name, +// age: userCreateDTO.age, +// email: userCreateDTO.email, +// }, +// }); +// return user; +// }; + +const createUser = async (userCreateDto: UserCreateDTO) => { + //? 넘겨받은 password를 bcrypt의 도움을 받아 암호화 + const salt = await bcrypt.genSalt(10); //^ 매우 작은 임의의 랜덤 텍스트 salt + const password = await bcrypt.hash(userCreateDto.password, salt); //^ 위에서 랜덤을 생성한 salt를 이용해 암호화 + + const data = await prisma.user.create({ data: { - userName: userCreateDTO.name, - age: userCreateDTO.age, - email: userCreateDTO.email, + userName: userCreateDto?.name, + age: userCreateDto?.age, + email: userCreateDto.email, + password, }, }); - return user; + + return data; }; const getAllUser = async () => { @@ -52,6 +93,7 @@ const userService = { getAllUser, updateUser, deleteUser, + signIn, }; export default userService; diff --git a/seminar4/yarn.lock b/seminar4/yarn.lock index 3bcabd5..286b505 100644 --- a/seminar4/yarn.lock +++ b/seminar4/yarn.lock @@ -19,6 +19,11 @@ resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.5.0.tgz#82df347a893a5ae2a67707d44772ba181f4b9328" integrity sha512-4t9ir2SbQQr/wMCNU4YpHWp5hU14J2m3wHUZnGJPpmBF8YtkisxyVyQsKd1e6FyLTaGq8LOLhm6VLYHKqKNm+g== +"@types/bcryptjs@^2.4.2": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.2.tgz#e3530eac9dd136bfdfb0e43df2c4c5ce1f77dfae" + integrity sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ== + "@types/body-parser@*": version "1.19.2" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" @@ -60,6 +65,13 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/jsonwebtoken@^8.5.9": + version "8.5.9" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz#2c064ecb0b3128d837d2764aa0b117b0ff6e4586" + integrity sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg== + dependencies: + "@types/node" "*" + "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" @@ -119,6 +131,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +bcryptjs@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" + integrity sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -157,6 +174,11 @@ braces@~3.0.2: dependencies: fill-range "^7.0.1" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -236,6 +258,13 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -256,7 +285,7 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== -express-validator@*: +express-validator@*, express-validator@^6.14.2: version "6.14.2" resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-6.14.2.tgz#6147893f7bec0e14162c3a88b3653121afc4678f" integrity sha512-8XfAUrQ6Y7dIIuy9KcUPCfG/uCbvREctrxf5EeeME+ulanJ4iiW71lWmm9r4YcKKYOCBMan0WpVg7FtHu4Z4Wg== @@ -431,6 +460,74 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +jsonwebtoken@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -594,7 +691,7 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -safe-buffer@5.2.1: +safe-buffer@5.2.1, safe-buffer@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -604,7 +701,7 @@ safe-buffer@5.2.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -semver@^5.7.1: +semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==