내일배움캠프 스파르타 코딩클럽
이어서 라우터와 에러 로그 인증인가 처리를 한 미들웨어를 작성해 보도록 하겠다.
< users.router.js >
import express from 'express';
import joi from 'joi';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { prisma } from '../utils/prisma/index.js';
import authMiddleware from '../middlewares/auth.middleware.js';
const router = express.Router();
const createdUsersSchema = joi.object({
email: joi.string().email().required(),
password: joi.string().min(6).required(),
pwCheck: joi.string().required(),
name: joi.string().required(),
});
// 회원가입 API
router.post('/sign-up', async (req, res, next) => {
try {
// body 에서 가져온 내용 유효성 검사
const validation = await createdUsersSchema.validateAsync(req.body);
const { email, password, pwCheck, name } = validation;
// 이미 사용되고 있는 email 검증
const isExistUser = await prisma.users.findFirst({
where: { email },
});
if (isExistUser) {
throw new Error('same email');
}
// password 확인
if (password !== pwCheck) {
throw new Error('not same password');
}
const part = 'APPLICANT';
const salt = 10;
const hashPw = await bcrypt.hash(password, salt);
// 데이터베이스에 저장
const user = await prisma.users.create({
data: {
email,
password: hashPw,
name,
part,
},
});
// 데이터 찾기
const data = await prisma.users.findFirst({
where: { email },
select: {
userId: true,
email: true,
name: true,
part: true,
createdAt: true,
updatedAt: true,
},
});
return res
.status(201)
.json({ message: '회원가입이 완료되었습니다.', data });
} catch (error) {
next(error);
}
});
const createdUsersSchema2 = joi.object({
email: joi.string().email().required(),
password: joi.string().min(6).required(),
});
// 로그인 API
router.post('/sign-in', async (req, res, next) => {
try {
const validation = await createdUsersSchema2.validateAsync(req.body);
const { email, password } = validation;
const user = await prisma.users.findFirst({
where: { email },
});
if (!user) {
throw new Error('can not');
}
if (!(await bcrypt.compare(password, user.password))) {
throw new Error('can not');
}
const accessToken = jwt.sign(
{ userId: user.userId },
process.env.ACCESS_TOKEN_SECRET_KEY,
{ expiresIn: '12h' }
);
res.cookie('authorization', `Bearer ${accessToken}`);
return res
.status(200)
.json({ message: 'Token이 정상적으로 발급되었습니다.' });
} catch (error) {
next(error);
}
});
// 내 정보 조회 API
router.get('/users', authMiddleware, async (req, res, next) => {
const { userId } = req.user;
const user = await prisma.users.findFirst({
where: { userId: +userId },
select: {
userId: true,
email: true,
name: true,
part: true,
createdAt: true,
updatedAt: true,
},
});
return res.status(200).json({ data: user });
});
export default router;
회원가입과 로그인 내정보 API를 작성해 주었다.
< resumes.router.js >
import express from 'express';
import joi from 'joi';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { prisma } from '../utils/prisma/index.js';
import authMiddleware from '../middlewares/auth.middleware.js';
const router = express.Router();
const createdUsersSchema = joi.object({
title: joi.string().required(),
introduce: joi.string().min(150).required(),
});
// 이력서 생성 API
router.post('/resumes', authMiddleware, async (req, res, next) => {
try {
const { userId } = req.user;
// body 에서 가져온 내용 유효성 검사
const validation = await createdUsersSchema.validateAsync(req.body);
const { title, introduce } = validation;
const state = 'APPlY';
// 데이터베이스에 저장
const user = await prisma.resumes.create({
data: {
UserId: userId,
title,
introduce,
state,
},
});
// 데이터 찾기
const data = await prisma.resumes.findFirst({
where: { UserId: userId },
select: {
resumeId: true,
UserId: true,
title: true,
introduce: true,
state: true,
createdAt: true,
updatedAt: true,
},
});
return res
.status(201)
.json({ message: '이력서 생성이 완료되었습니다.', data });
} catch (error) {
next(error);
}
});
// 이력서 목록 조회 API
router.get('/resumes', authMiddleware, async (req, res, next) => {
try {
const { userId } = req.user;
const sort = req.query.sort ? req.query.sort.toLowerCase() : 'desc';
const orderBy = sort === 'asc' ? 'asc' : 'desc';
if (orderBy !== 'asc' && orderBy !== 'desc') {
throw new Error('not asc or desc');
}
const resumes = await prisma.users.findMany({
where: { userId },
select: {
name: true,
Resumes: {
select: {
resumeId: true,
title: true,
introduce: true,
state: true,
createdAt: true,
updatedAt: true,
},
},
},
orderBy: { createdAt: orderBy },
});
return res.status(200).json({ data: resumes });
} catch (error) {
next(error);
}
});
// 이력서 상세 조회 API
router.get('/resumes/:resumeId', authMiddleware, async (req, res, next) => {
try {
const { userId } = req.user;
const { resumeId } = req.params;
if (!resumeId) {
throw new Error('undefind resumeId');
}
console.log(resumeId);
const resumes = await prisma.users.findFirst({
where: {
userId,
Resumes: {
some: { resumeId: +resumeId },
},
},
select: {
name: true,
Resumes: {
where: {
resumeId: +resumeId,
},
select: {
resumeId: true,
title: true,
introduce: true,
state: true,
createdAt: true,
updatedAt: true,
},
},
},
});
if (!resumes || resumes.Resumes.length === 0) {
throw new Error('undefind Resume');
}
return res.status(200).json({ data: resumes });
} catch (error) {
next(error);
}
});
const createdUsersSchema2 = joi.object({
title: joi.string(),
introduce: joi.string().min(150),
});
// 이력서 수정 API
router.patch('/resumes/:resumeId', authMiddleware, async (req, res, next) => {
try {
const { userId } = req.user;
const { resumeId } = req.params;
const validation = await createdUsersSchema2.validateAsync(req.body);
const { title, introduce } = validation;
if (!title && !introduce) {
throw new Error('some inner');
}
// 요청된 이력서가 존재하고 사용자가 소유한 이력서인지 확인
const resume = await prisma.resumes.findUnique({
where: {
resumeId: +resumeId,
UserId: userId,
},
});
// 이력서가 존재하지 않으면 에러 처리
if (!resume) {
throw new Error('undefind Resume');
}
// 이력서 업데이트
const updatedResume = await prisma.resumes.update({
where: {
resumeId: +resumeId,
UserId: userId,
},
data: {
title: title || undefined, // 제목이 제공되면 업데이트
introduce: introduce || undefined, // 자기소개가 제공되면 업데이트
},
select: {
resumeId: true,
UserId: true,
title: true,
introduce: true,
state: true,
createdAt: true,
updatedAt: true,
},
});
res.status(200).json({ data: updatedResume });
} catch (error) {
next(error);
}
});
// 이력서 삭제 API
router.delete('/resumes/:resumeId', authMiddleware, async (req, res, next) => {
try {
const { userId } = req.user;
const { resumeId } = req.params;
// 요청된 이력서가 존재하고 사용자가 소유한 이력서인지 확인
const resume = await prisma.resumes.findUnique({
where: {
resumeId: +resumeId,
UserId: userId,
},
});
// 이력서가 존재하지 않으면 에러 처리
if (!resume) {
throw new Error('undefind Resume');
}
// 데이터 삭제
await prisma.resumes.delete({
where: {
resumeId: +resumeId,
},
});
res.status(200).json({ deletedResumeId: +resumeId });
} catch (error) {
next(error);
}
});
export default router;
이력서를 RESTful 하게 CRUD 기능을 모두하는 이력서 기능을 위와같이 구현하였다.
< log.middleware.js >
import winston from 'winston';
// winston 을 통해 어떤 로그를 관리 할 지 설정하는 부분
const logger = winston.createLogger({
level: 'info', // 로그 레벨을 'info'로 설정합니다. 로그의 중요도를 나타냄
format: winston.format.json(), // 로그 포맷을 JSON 형식으로 설정합니다.
transports: [
new winston.transports.Console(), // 로그를 콘솔에 출력합니다.
],
});
// 미들웨어가 실행되는 부분
export default function (req, res, next) {
// 클라이언트의 요청이 시작된 시간을 기록합니다.
const start = new Date().getTime();
// 응답이 완료되면 로그를 기록합니다.
res.on('finish', () => {
const duration = new Date().getTime() - start;
logger.info(
`Method: ${req.method}, URL: ${req.url}, Status: ${res.statusCode}, Duration: ${duration}ms`
);
});
next();
}
winston 을 활용하여 위와같이 요청의 로그와 처리시간을 기록하게끔 하였다.
< auth.middleware.js >
import jwt from 'jsonwebtoken';
import { prisma } from '../utils/prisma/index.js';
export default async function (req, res, next) {
try {
// 1. 클라이언트로 부터 쿠키(Cookie) 전달받기
const { authorization } = req.cookies;
if (!authorization) {
throw new Error('인증 정보가 없습니다.');
}
// 2. 쿠키(Cookie)가 Bearer 토큰 형식인지 확인
const [tokenType, token] = authorization.split(' ');
if (tokenType !== 'Bearer') {
throw new Error('지원하지 않는 인증 방식입니다.');
}
const key = process.env.ACCESS_TOKEN_SECRET_KEY;
// 3. 서버에서 발급한 JWT가 맞는지 검증
const decodedToken = jwt.verify(token, key);
const userId = decodedToken.userId;
// 4. JWT의 `userId`를 이용해 사용자를 조회
const user = await prisma.users.findFirst({
where: { userId: +userId },
});
if (!user) {
res.clearCookie('authorization');
// 쿠키를 받았는데 없다면 정상적이지 않은 쿠키이기 때문에 삭제를 진행해야함
throw new Error('인증 정보와 일치하는 사용자가 없습니다.');
}
// 5. `req.user` 에 조회된 사용자 정보를 할당합니다.
req.user = user;
// 6. 다음 미들웨어를 실행합니다.
next();
} catch (error) {
res.clearCookie('authorization'); // 특정 쿠키를 삭제시킨다.
switch (error.name) {
case 'TokenExpiredError': // 토큰이 만료되었을 때, 발생하는 에러
return res
.status(401)
.json({ errorMessage: '인증 정보가 만료되었습니다.' });
case 'JsonWebTokenError': // 토큰에 검증이 실패했을 때, 발생하는 에러
return res
.status(401)
.json({ errorMessage: '토큰이 유효하지 않습니다.' });
default: // 그 외 모든 예외적인 에러 처리
return res
.status(401)
.json({ errorMessage: error.message ?? '비정상적인 요청입니다.' });
// error.message ?? 의 ??연산자는 왼쪽 피연산자가 null or undefined 일때만 오른쪽 피연산자를 반환 한다.
}
}
}
인증을 위해 필요한 작업을 위와같이 쿠키를 활용하여 처리를 하고 process.env.ACCESS_TOKEN_SECRET_KEY ㄴ
는 .env 를 활용하여 잘감춰 주었다.
그리고 에러처리는 그즉시 인증에서 잘 실행될 수 있도록 따로 에러 미들웨어에서 처리하지 않았다.
< error-handler.middleware.js >
export default (err, req, res, next) => {
console.log('에러 처리 미들웨어가 실행되었습니다.');
console.error(err.message);
// joi 에러
if (err.isJoi) {
if (err.message === '"email" must be a valid email') {
return res
.status(400)
.json({ errorMessage: '이메일 형식이 올바르지 않습니다.' });
}
if (err.message.includes('is required')) {
const splitErr = err.message.split(' ')[0].replace(/"/g, '');
// .replace(/"/g, '') 를 사용하면 joi의 오류메세지 따옴표를 제거가능
return res
.status(400)
.json({ errorMessage: `${splitErr}을(를) 입력해주세요.` });
}
if (err.message.includes('characters long')) {
const splitErr = err.message.split(' ')[0].replace(/"/g, '');
const splitNum = err.message.split(' ')[6];
return res
.status(400)
.json({
errorMessage: `${splitErr}는 ${splitNum}자 이상이어야 합니다.`,
});
}
}
// throw 에러
if (err.message === 'same email') {
return res.status(400).json({ errorMessage: '이미 가입 된 사용자입니다.' });
}
if (err.message === 'not same password') {
return res
.status(400)
.json({ errorMessage: '입력 한 두 비밀번호가 일치하지 않습니다.' });
}
if (err.message === 'can not') {
return res
.status(401)
.json({ errorMessage: '인증 정보가 유효하지 않습니다.' });
}
if (err.message === 'not asc or desc') {
return res
.status(400)
.json({ errorMessage: 'query에 입력한 값이 asc/desc 가 아닙니다.' });
}
if (err.message === 'undefind resumeId') {
return res.status(400).json({ errorMessage: 'resumeId를 입력해주세요.' });
}
if (err.message === 'undefind Resume') {
return res
.status(401)
.json({ errorMessage: '이력서가 존재하지 않습니다.' });
}
if (err.message === 'some inner') {
return res
.status(400)
.json({ errorMessage: '수정 할 정보를 입력해 주세요' });
}
return res.status(500).json({
errorMessage: '예상치 못한 에러가 발생했습니다. 관리자에게 문의해 주세요.',
});
};
에러처리를 모두 담당하는 미들웨어를 작성해 주었다.
이렇게 해서 이번 개인과제가 잘 마무리 되었다. 선택부분도 있었지만 시간관계상 구현하기 힘들어서
다음에 꼭 구현을 해보도록 하겠다.
'TIL' 카테고리의 다른 글
Nodejs 뉴스피드 프로젝트 팀과제 (1) (0) | 2024.06.04 |
---|---|
Nodejs 뉴스피드 프로젝트 팀과제 시작 (0) | 2024.06.03 |
Nodejs 숙련 이력서 개인과제 완료(2) (0) | 2024.05.31 |
Nodejs 숙련 이력서 개인과제 완료(1) (0) | 2024.05.29 |
Nodejs 강의 숙련 게시판 프로젝트(8) 게시글 생성,조회 / 댓글 생성,조회 (0) | 2024.05.28 |