내일배움캠프 스파르타 코딩클럽
< prisma.utils.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);
}
< tokens.js >
import jwt from 'jsonwebtoken';
import {
ACCESS_TOKEN_SECRET,
REFRESH_TOKEN_SECRET,
} from '../constants/env.constant.js';
import {
ACCESS_TOKEN_EXPIRES,
REFRESH_TOKEN_EXPIRES,
} from '../constants/auth.constant.js';
//엑세스 토큰 발급 함수
export function createAccessToken({ id, role }) {
try {
console.log(ACCESS_TOKEN_SECRET);
return jwt.sign({ id, role }, ACCESS_TOKEN_SECRET, {
expiresIn: ACCESS_TOKEN_EXPIRES,
});
} catch (error) {
console.log('에러메세지', error.message);
}
}
//리프레시 토큰 발급 함수
export function createRefreshToken({ id, role }) {
return jwt.sign({ id, role }, REFRESH_TOKEN_SECRET, {
expiresIn: REFRESH_TOKEN_EXPIRES,
});
}
< http.error.js >
import { HTTP_STATUS } from '../constants/http-status.constant.js';
class BadRequest {
constructor(message = BadRequest.name) {
this.message = message;
this.status = HTTP_STATUS.BAD_REQUEST;
}
}
class Unauthorized {
constructor(message = Unauthorized.name) {
this.message = message;
this.status = HTTP_STATUS.UNAUTHORIZED;
}
}
class Forbidden {
constructor(message = Forbidden.name) {
this.message = message;
this.status = HTTP_STATUS.FORBIDDEN;
}
}
class NotFound {
constructor(message = NotFound.name) {
this.message = message;
this.status = HTTP_STATUS.NOT_FOUND;
}
}
class Conflict {
constructor(message = Conflict.name) {
this.message = message;
this.status = HTTP_STATUS.CONFLICT;
}
}
class InternalServerError {
constructor(message = InternalServerError.name) {
this.message = message;
this.status = HTTP_STATUS.INTERNAL_SERVER_ERROR;
}
}
export const HttpError = {
BadRequest,
Unauthorized,
Forbidden,
NotFound,
Conflict,
InternalServerError,
};
< auth.constant.js >
export const HASH_SALT_ROUNDS = 10;
export const ACCESS_TOKEN_EXPIRES = '1h';
export const REFRESH_TOKEN_EXPIRES = '7d';
export const MIN_PASSWORD_LENGTH = 8;
export const MAX_PASSWORD_LENGTH = 12;
< env.constant.js >
export const SERVER_PORT = process.env.SERVER_PORT;
export const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
export const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET;
export const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
export const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
export const AWS_REGION = process.env.AWS_REGION;
export const AWS_S3_BUCKET_NAME = process.env.AWS_S3_BUCKET_NAME;
< 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, // 예상치 못한 에러가 발생했을 때
};
< message.constant.js >
import { MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH } from './auth.constant.js';
export const MESSAGES = {
AUTH: {
COMMON: {
EMAIL: {
REQUIRED: '이메일을 입력해 주세요.',
INVALID_FORMAT: '이메일 형태가 올바르지 않습니다.',
DUPLICATED: '이미 가입된 사용자입니다.',
},
PASSWORD: {
REQUIRED: '비밀번호를 입력해 주세요.',
LENGTH: `비밀번호를 ${MIN_PASSWORD_LENGTH}자리 이상, ${MAX_PASSWORD_LENGTH}자리 이하로 설정해주세요.`,
NO_GAP: '비밀번호에는 공백이 포함될 수 없습니다.',
NO_STRING: '비밀번호는 문자열로 입력해야합니다.',
},
REPEAT_PASSWORD: {
REQUIRED: '비밀번호 확인을 입력해 주세요.',
NOT_MATCHED: '비밀번호가 일치하지 않습니다.',
},
NAME: {
REQUIRED: '이름을 입력해주세요.',
NO_STRING: '이름은 문자열로 입력해야합니다.',
},
INTRODUCE: {
NO_STRING: '자기소개는 문자열로 입력해야합니다.',
},
PROFILE_IMAGE: {
NO_STRING: '프로필 URL을 문자열로 입력해주세요.',
},
},
JWT: {
NO_TOKEN: '인증 정보가 없습니다.',
NOT_SUPPORTED_TYPE: '지원하지 않는 인증 방식입니다.',
EXPIRED: '인증 정보가 만료되었습니다.',
NO_USER: '인증 정보와 일치하는 사용자가 없습니다.',
INVALID: '인증 정보가 유효하지 않습니다.',
},
SIGN_UP: {
SUCCEED: '회원가입에 성공했습니다.',
},
SIGN_IN: {
SUCCEED: '로그인에 성공했습니다.',
UNAUTHORIZED: '인증에 실패했습니다',
TOKEN: '토근 재발급에 성공했습니다.',
},
SIGN_OUT: {
SUCCEED: '로그아웃에 성공했습니다.',
},
},
};
< error-handler.middleware.js >
import { HTTP_STATUS } from '../constants/http-status.constant.js';
const errorHandler = (err, req, res, next) => {
console.log(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,
});
}
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message });
};
export default errorHandler;
< log.middleware.js >
import winston from '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();
// 응답이 완료되면 로그를 기록합니다.(finish)
res.on('finish', () => {
const duration = new Date().getTime() - start;
logger.info(
`Method: ${req.method}, URL: ${req.url}, Status: ${res.statusCode}, Duration: ${duration}ms`
);
});
next();
}
< require-access-token.middleware.js >
import { ACCESS_TOKEN_SECRET } from '../constants/env.constant.js';
import { MESSAGES } from '../constants/message.constant.js';
import jwt from 'jsonwebtoken';
import { HttpError } from '../errors/http.error.js';
import { UsersRepository } from '../repositories/users.repository.js';
import { PetsitterRepository } from '../repositories/petsitters.repository.js';
import { prisma } from '../utils/prisma.utils.js';
const usersRepository = new UsersRepository(prisma);
const petsittersRepository = new PetsitterRepository(prisma);
export const requireAccessToken = async (req, res, next) => {
try {
const authorization = req.headers.authorization;
if (!authorization) {
throw new HttpError.Unauthorized(MESSAGES.AUTH.JWT.NO_TOKEN);
}
const [type, accessToken] = authorization.split(' ');
if (type !== 'Bearer') {
throw new HttpError.Unauthorized(MESSAGES.AUTH.JWT.NOT_SUPPORTED_TYPE);
}
if (!accessToken) {
throw new HttpError.Unauthorized(MESSAGES.AUTH.JWT.NO_TOKEN);
}
let payload;
try {
payload = jwt.verify(accessToken, ACCESS_TOKEN_SECRET);
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new HttpError.Unauthorized(MESSAGES.AUTH.JWT.EXPIRED);
} else {
throw new HttpError.Unauthorized(MESSAGES.AUTH.JWT.INVALID);
}
}
const { id } = payload;
const { role } = payload;
const user =
role === 'user'
? await usersRepository.findOneId(id)
: await petsittersRepository.findPetsitterById({ id });
if (!user) {
throw new HttpError.Unauthorized(MESSAGES.AUTH.JWT.NO_USER);
}
if (role === 'user') {
const { refreshToken } = await usersRepository.findOneRefreshTokenId(id);
if (refreshToken === null) {
throw new HttpError.Unauthorized(MESSAGES.AUTH.JWT.NO_TOKEN);
}
}
req.user = { ...user, role };
next();
} catch (error) {
next(error);
}
};
< require-refresh-token.middleware.js >
import { REFRESH_TOKEN_SECRET } from '../constants/env.constant.js';
import { MESSAGES } from '../constants/message.constant.js';
import jwt from 'jsonwebtoken';
import { HttpError } from '../errors/http.error.js';
import { UsersRepository } from '../repositories/users.repository.js';
import { PetsitterRepository } from '../repositories/petsitters.repository.js';
import bcrypt from 'bcrypt';
import { prisma } from '../utils/prisma.utils.js';
const usersRepository = new UsersRepository(prisma);
const petsittersRepository = new PetsitterRepository(prisma);
export const requireRefreshToken = async (req, res, next) => {
try {
const authorization = req.headers.authorization;
if (!authorization) {
throw new HttpError.Unauthorized(MESSAGES.AUTH.JWT.NO_TOKEN);
}
const [type, refreshToken] = authorization.split(' ');
if (type !== 'Bearer') {
throw new HttpError.Unauthorized(MESSAGES.AUTH.JWT.NOT_SUPPORTED_TYPE);
}
if (!refreshToken) {
throw new HttpError.Unauthorized(MESSAGES.AUTH.JWT.NO_TOKEN);
}
let payload;
try {
payload = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET);
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new HttpError.Unauthorized(MESSAGES.AUTH.JWT.EXPIRED);
} else {
throw new HttpError.Unauthorized(MESSAGES.AUTH.JWT.INVALID);
}
}
const { id } = payload;
const { role } = payload;
const existedRefreshToken = await usersRepository.findOneRefreshTokenId(id);
if (!existedRefreshToken) {
throw new HttpError.Unauthorized(MESSAGES.AUTH.JWT.NO_USER);
}
const isValidRefreshToken = bcrypt.compareSync(
refreshToken,
existedRefreshToken.refreshToken
);
if (!isValidRefreshToken) {
throw new HttpError.Unauthorized(MESSAGES.AUTH.JWT.NO_USER);
}
const user =
role === 'user'
? await usersRepository.findOneId(id)
: await petsittersRepository.findPetsitterById({ id });
if (!user) {
throw new HttpError.Unauthorized(MESSAGES.AUTH.JWT.NO_USER);
}
req.user = { ...user, role };
next();
} catch (error) {
next(error);
}
};
< require-roles.middleware.js >
import { HTTP_STATUS } from '../constants/http-status.constant.js';
export const requireRoles = (roles) => {
return (req, res, next) => {
try {
const user = req.user;
const hasPermission = user && roles.includes(user.role);
if (!hasPermission) {
res
.status(HTTP_STATUS.FORBIDDEN)
.json({ message: '접근권한이 없습니다.' });
}
next();
} catch (error) {
next(error);
}
};
};
< upload-image.js >
import { S3Client } from '@aws-sdk/client-s3';
import multer from 'multer';
import multerS3 from 'multer-s3';
import path from 'path';
import {
AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY,
AWS_REGION,
AWS_S3_BUCKET_NAME,
} from '../constants/env.constant.js';
// S3 클라이언트 설정
const s3 = new S3Client({
region: AWS_REGION,
credentials: {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
},
});
// 파일 필터 설정 (이미지 파일만 허용)
const fileFilter = (req, file, cb) => {
const fileTypes = /jpeg|jpg|png/;
const extname = fileTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = fileTypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
} else {
cb(new Error('이미지 파일만 업로드할 수 있습니다.'));
}
};
// multer 설정 (S3 버킷에 저장)
const upload = multer({
storage: multerS3({
s3: s3,
bucket: AWS_S3_BUCKET_NAME,
acl: 'public-read', // 파일 접근 권한 설정
key: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(
null,
file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)
);
},
}),
limits: { fileSize: 1024 * 1024 * 5 }, // 파일 크기 제한 (5MB)
fileFilter: fileFilter,
}).single('profileImage'); // 파일 필드 이름 명시
export { upload };
'TIL' 카테고리의 다른 글
Node.js 백오피스 팀 프로젝트 (4) (0) | 2024.06.24 |
---|---|
Node.js 백오피스 팀 프로젝트 (3) (0) | 2024.06.24 |
Node.js 백오피스 팀 프로젝트 (1) (0) | 2024.06.19 |
Node.js 백오피스 팀 프로젝트 시작 (0) | 2024.06.17 |
Node.js 웹서버 심화 주차 개인 과제 (3) (0) | 2024.06.17 |