TIL
Node.js 웹서버 심화 주차 개인 과제 (2)
황민도
2024. 6. 17. 21:59
내일배움캠프 스파르타 코딩클럽
< src. app.js >
import express from 'express';
import { SERVER_PORT } from './constants/env.constant.js';
import { errorHandler } from './middlewares/error-handler.middleware.js';
import { HTTP_STATUS } from './constants/http-status.constant.js';
import { apiRouter } from './routers/index.js';
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/health-check', (req, res) => {
return res.status(HTTP_STATUS.OK).send(`I'm healthy.`);
});
app.use('/api', apiRouter);
app.use(errorHandler);
app.listen(SERVER_PORT, () => {
console.log(`서버가 ${SERVER_PORT}번 포트에서 실행 중입니다.`);
});
< src.utils. prisma.util.js >
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient({
// Prisma를 이용해 데이터베이스를 접근할 때, SQL을 출력해줍니다.
log: ['query', 'info', 'warn', 'error'],
// 에러 메시지를 평문이 아닌, 개발자가 읽기 쉬운 형태로 출력해줍니다.
errorFormat: 'pretty',
}); // PrismaClient 인스턴스를 생성합니다.
try {
await prisma.$connect();
console.log('DB 연결에 성공했습니다.');
} catch (error) {
console.error('DB 연결에 실패했습니다.', error);
}
< src.constants. auth.constant.js >
export const HASH_SALT_ROUNDS = 10;
export const MIN_PASSWORD_LENGTH = 6;
export const ACCESS_TOKEN_EXPIRES_IN = '12h';
< src.constants. env.constant.js >
import 'dotenv/config';
export const SERVER_PORT = process.env.SERVER_PORT;
export const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
< src.constants. http-status.constant.js >
export const HTTP_STATUS = {
OK: 200, // 호출에 성공했을 때
CREATED: 201, // 생성에 성공했을 때
BAD_REQUEST: 400, // 사용자가 잘못 했을 때 (예: 입력 값을 빠뜨렸을 때)
UNAUTHORIZED: 401, // 인증 실패 unauthenciated (예: 비밀번호가 틀렸을 때)
FORBIDDEN: 403, // 인가 실패 unauthorized (예: 접근 권한이 없을 때)
NOT_FOUND: 404, // 데이터가 없는 경우
CONFLICT: 409, // 충돌이 발생했을 때 (예: 이메일 중복)
INTERNAL_SERVER_ERROR: 500, // 예상치 못한 에러가 발생했을 때
};
< src.constants. message.constant.js >
import { MIN_PASSWORD_LENGTH } from './auth.constant.js';
import { MIN_RESUME_LENGTH } from './resume.constant.js';
export const MESSAGES = {
AUTH: {
COMMON: {
EMAIL: {
REQUIRED: '이메일을 입력해 주세요.',
INVALID_FORMAT: '이메일 형식이 올바르지 않습니다.',
DUPLICATED: '이미 가입 된 사용자입니다.',
},
PASSWORD: {
REQURIED: '비밀번호를 입력해 주세요.',
MIN_LENGTH: `비밀번호는 ${MIN_PASSWORD_LENGTH}자리 이상이어야 합니다.`,
},
PASSWORD_CONFIRM: {
REQURIED: '비밀번호 확인을 입력해 주세요.',
NOT_MACHTED_WITH_PASSWORD: '입력 한 두 비밀번호가 일치하지 않습니다.',
},
NAME: {
REQURIED: '이름을 입력해 주세요.',
},
UNAUTHORIZED: '인증 정보가 유효하지 않습니다.',
JWT: {
NO_TOKEN: '인증 정보가 없습니다.',
NOT_SUPPORTED_TYPE: '지원하지 않는 인증 방식입니다.',
EXPIRED: '인증 정보가 만료되었습니다.',
NO_USER: '인증 정보와 일치하는 사용자가 없습니다.',
INVALID: '인증 정보가 유효하지 않습니다.',
},
},
SIGN_UP: {
SUCCEED: '회원가입에 성공했습니다.',
},
SIGN_IN: {
SUCCEED: '로그인에 성공했습니다.',
},
},
USERS: {
READ_ME: {
SUCCEED: '내 정보 조회에 성공했습니다.',
},
},
RESUMES: {
COMMON: {
TITLE: {
REQUIRED: '제목을 입력해 주세요.',
},
CONTENT: {
REQUIRED: '자기소개를 입력해 주세요.',
MIN_LENGTH: `자기소개는 ${MIN_RESUME_LENGTH}자 이상 작성해야 합니다.`,
},
NOT_FOUND: '이력서가 존재하지 않습니다.',
},
CREATE: {
SUCCEED: '이력서 생성에 성공했습니다.',
},
READ_LIST: {
SUCCEED: '이력서 목록 조회에 성공했습니다.',
},
READ_DETAIL: {
SUCCEED: '이력서 상세 조회에 성공했습니다.',
},
UPDATE: {
SUCCEED: '이력서 수정에 성공했습니다.',
NO_BODY_DATA: '수정 할 정보를 입력해 주세요.',
},
DELETE: {
SUCCEED: '이력서 삭제에 성공했습니다.',
},
},
};
< src.constants. resume.constant.js >
export const MIN_RESUME_LENGTH = 150;
< src.middlewares.validators. sing-up-validator.middleware.js >
import Joi from 'joi';
import { MESSAGES } from '../../constants/message.constant.js';
import { MIN_PASSWORD_LENGTH } from '../../constants/auth.constant.js';
const schema = Joi.object({
email: Joi.string().email().required().messages({
'any.required': MESSAGES.AUTH.COMMON.EMAIL.REQUIRED,
'string.email': MESSAGES.AUTH.COMMON.EMAIL.INVALID_FORMAT,
}),
password: Joi.string().required().min(MIN_PASSWORD_LENGTH).messages({
'any.required': MESSAGES.AUTH.COMMON.PASSWORD.REQURIED,
'string.min': MESSAGES.AUTH.COMMON.PASSWORD.MIN_LENGTH,
}),
passwordConfirm: Joi.string().required().valid(Joi.ref('password')).messages({
'any.required': MESSAGES.AUTH.COMMON.PASSWORD_CONFIRM.REQURIED,
'any.only': MESSAGES.AUTH.COMMON.PASSWORD_CONFIRM.NOT_MACHTED_WITH_PASSWORD,
}),
name: Joi.string().required().messages({
'any.required': MESSAGES.AUTH.COMMON.NAME.REQURIED,
}),
});
export const signUpValidator = async (req, res, next) => {
try {
await schema.validateAsync(req.body);
next();
} catch (error) {
next(error);
}
};
< src.middlewares.validators. sing-in-validator.middleware.js >
import Joi from 'joi';
import { MESSAGES } from '../../constants/message.constant.js';
const schema = Joi.object({
email: Joi.string().email().required().messages({
'any.required': MESSAGES.AUTH.COMMON.EMAIL.REQUIRED,
'string.email': MESSAGES.AUTH.COMMON.EMAIL.INVALID_FORMAT,
}),
password: Joi.string().required().messages({
'any.required': MESSAGES.AUTH.COMMON.PASSWORD.REQURIED,
}),
});
export const signInValidator = async (req, res, next) => {
try {
await schema.validateAsync(req.body);
next();
} catch (error) {
next(error);
}
};
< src.middlewares.validators. create-resume-validator.middleware.js >
import Joi from 'joi';
import { MESSAGES } from '../../constants/message.constant.js';
import { MIN_RESUME_LENGTH } from '../../constants/resume.constant.js';
const schema = Joi.object({
title: Joi.string().required().messages({
'any.required': MESSAGES.RESUMES.COMMON.TITLE.REQUIRED,
}),
content: Joi.string().min(MIN_RESUME_LENGTH).required().messages({
'any.required': MESSAGES.RESUMES.COMMON.CONTENT.REQUIRED,
'string.min': MESSAGES.RESUMES.COMMON.CONTENT.MIN_LENGTH,
}),
});
export const createResumeValidator = async (req, res, next) => {
try {
await schema.validateAsync(req.body);
next();
} catch (error) {
next(error);
}
};
< src.middlewares.validators. updated-validator.middleware.js >
import Joi from 'joi';
import { MESSAGES } from '../../constants/message.constant.js';
import { MIN_RESUME_LENGTH } from '../../constants/resume.constant.js';
const schema = Joi.object({
title: Joi.string(),
content: Joi.string().min(MIN_RESUME_LENGTH).messages({
'string.min': MESSAGES.RESUMES.COMMON.CONTENT.MIN_LENGTH,
}),
})
.min(1)
.messages({
'object.min': MESSAGES.RESUMES.UPDATE.NO_BODY_DATA,
});
export const updateResumeValidator = async (req, res, next) => {
try {
await schema.validateAsync(req.body);
next();
} catch (error) {
next(error);
}
};
< src.middlewares. error-handler.middleware.js >
import { HTTP_STATUS } from '../constants/http-status.constant.js';
import { HttpError } from '../errors/http.error.js';
export const errorHandler = (err, req, res, next) => {
console.error(err);
// joi에서 발생한 에러 처리
if (err.name === 'ValidationError') {
return res.status(HTTP_STATUS.BAD_REQUEST).json({
status: HTTP_STATUS.BAD_REQUEST,
message: err.message,
});
}
// Http Error 처리
if (err.status && err.message) {
return res.status(err.status).json({
status: err.status,
message: err.message,
});
}
// 그 밖의 예상치 못한 에러 처리
return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
message: '예상치 못한 에러가 발생했습니다. 관리자에게 문의해 주세요.',
});
};
< src.middlewares. require-access-token.middleware.js >
import jwt from 'jsonwebtoken';
import { MESSAGES } from '../constants/message.constant.js';
import { ACCESS_TOKEN_SECRET } from '../constants/env.constant.js';
import { UsersRepository } from '../repositories/users.repository.js';
import { HttpError } from '../errors/http.error.js';
const usersRepository = new UsersRepository();
export const requireAccessToken = async (req, res, next) => {
try {
// 인증 정보 파싱
const authorization = req.headers.authorization;
// Authorization이 없는 경우
if (!authorization) {
throw new HttpError.Unauthorized(MESSAGES.AUTH.COMMON.JWT.NO_TOKEN);
}
// JWT 표준 인증 형태와 일치하지 않는 경우
const [type, accessToken] = authorization.split(' ');
if (type !== 'Bearer') {
throw new HttpError.Unauthorized(MESSAGES.AUTH.COMMON.JWT.NOT_SUPPORTED_TYPE);
}
// AccessToken이 없는 경우
if (!accessToken) {
throw new HttpError.Unauthorized(MESSAGES.AUTH.COMMON.JWT.NO_TOKEN);
}
let payload;
try {
payload = jwt.verify(accessToken, ACCESS_TOKEN_SECRET);
} catch (error) {
// AccessToken의 유효기한이 지난 경우
if (error.name === 'TokenExpiredError') {
throw new HttpError.Unauthorized(MESSAGES.AUTH.COMMON.JWT.EXPIRED);
}
// 그 밖의 AccessToken 검증에 실패한 경우
else {
throw new HttpError.Unauthorized(MESSAGES.AUTH.COMMON.JWT.INVALID);
}
}
// Payload에 담긴 사용자 ID와 일치하는 사용자가 없는 경우
const { id } = payload;
const user = await usersRepository.findUserBpw(id);
if (!user) {
throw new HttpError.Unauthorized(MESSAGES.AUTH.COMMON.JWT.NO_USER);
}
req.user = user;
next();
} catch (error) {
next(error);
}
};
내용이 길어져 다음 TIL 에서 이어가 보도록 하겠다.