최초커밋

This commit is contained in:
kjs
2025-08-21 09:41:46 +09:00
commit a0e5b57a24
2454 changed files with 1476904 additions and 0 deletions

View File

@@ -0,0 +1,254 @@
// 인증 미들웨어
// JWT 토큰 검증 및 사용자 정보 설정
import { Request, Response, NextFunction } from "express";
import { JwtUtils } from "../utils/jwtUtils";
import { AuthenticatedRequest, PersonBean } from "../types/auth";
import { logger } from "../utils/logger";
// Express Request 타입 확장
declare global {
namespace Express {
interface Request {
ip: string;
}
}
}
/**
* JWT 토큰 검증 미들웨어
* 기존 세션 방식과 동일한 효과를 제공
*/
export const authenticateToken = (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void => {
try {
// Authorization 헤더에서 토큰 추출
const authHeader = req.get("Authorization");
const token = authHeader && authHeader.split(" ")[1]; // Bearer TOKEN
if (!token) {
res.status(401).json({
success: false,
error: {
code: "TOKEN_MISSING",
details: "인증 토큰이 필요합니다.",
},
});
return;
}
// JWT 토큰 검증 및 사용자 정보 추출
const userInfo: PersonBean = JwtUtils.verifyToken(token);
// 요청 객체에 사용자 정보 설정 (기존 PersonBean과 동일)
req.user = userInfo;
// 로그 기록
logger.info(`인증 성공: ${userInfo.userId} (${req.ip})`);
next();
} catch (error) {
logger.error(
`인증 실패: ${error instanceof Error ? error.message : "Unknown error"} (${req.ip})`
);
res.status(401).json({
success: false,
error: {
code: "INVALID_TOKEN",
details:
error instanceof Error ? error.message : "토큰 검증에 실패했습니다.",
},
});
}
};
/**
* 선택적 인증 미들웨어 (토큰이 없어도 통과)
* 일부 API에서 사용 (예: 공개 정보 조회)
*/
export const optionalAuth = (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void => {
try {
const authHeader = req.get("Authorization");
const token = authHeader && authHeader.split(" ")[1];
if (token) {
const userInfo: PersonBean = JwtUtils.verifyToken(token);
req.user = userInfo;
logger.info(`선택적 인증 성공: ${userInfo.userId} (${req.ip})`);
} else {
logger.info(`선택적 인증: 토큰 없음 (${req.ip})`);
}
next();
} catch (error) {
// 토큰이 있지만 유효하지 않은 경우에도 통과 (선택적 인증)
logger.warn(
`선택적 인증 실패: ${error instanceof Error ? error.message : "Unknown error"} (${req.ip})`
);
next();
}
};
/**
* 관리자 권한 확인 미들웨어
*/
export const requireAdmin = (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void => {
if (!req.user) {
res.status(401).json({
success: false,
error: {
code: "AUTHENTICATION_REQUIRED",
details: "인증이 필요합니다.",
},
});
return;
}
// 기존 Java 로직과 동일: plm_admin 사용자만 관리자로 인식
if (req.user.userId === "plm_admin") {
next();
} else {
res.status(403).json({
success: false,
error: {
code: "ADMIN_REQUIRED",
details: "관리자 권한이 필요합니다.",
},
});
}
};
/**
* 특정 사용자 또는 관리자 권한 확인 미들웨어
*/
export const requireUserOrAdmin = (targetUserId: string) => {
return (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void => {
if (!req.user) {
res.status(401).json({
success: false,
error: {
code: "AUTHENTICATION_REQUIRED",
details: "인증이 필요합니다.",
},
});
return;
}
// 본인 또는 관리자인 경우 통과
if (req.user.userId === targetUserId || req.user.userId === "plm_admin") {
next();
} else {
res.status(403).json({
success: false,
error: {
code: "PERMISSION_DENIED",
details: "권한이 없습니다.",
},
});
}
};
};
/**
* 토큰 갱신 미들웨어
* 토큰이 곧 만료될 경우 자동으로 갱신
*/
export const refreshTokenIfNeeded = (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void => {
try {
const authHeader = req.get("Authorization");
const token = authHeader && authHeader.split(" ")[1];
if (token) {
// 토큰이 1시간 이내에 만료되는지 확인
const decoded = JwtUtils.decodeToken(token);
if (decoded && decoded.exp) {
const currentTime = Math.floor(Date.now() / 1000);
const timeUntilExpiry = decoded.exp - currentTime;
// 1시간(3600초) 이내에 만료되는 경우 갱신
if (timeUntilExpiry > 0 && timeUntilExpiry < 3600) {
const newToken = JwtUtils.refreshToken(token);
// 새로운 토큰을 응답 헤더에 포함
res.setHeader("X-New-Token", newToken);
logger.info(`토큰 갱신: ${decoded.userId} (${req.ip})`);
}
}
}
next();
} catch (error) {
// 토큰 갱신 실패해도 요청은 계속 진행
logger.warn(
`토큰 갱신 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
next();
}
};
/**
* 인증 상태 확인 미들웨어
* 토큰 유효성만 확인하고 사용자 정보는 설정하지 않음
*/
export const checkAuthStatus = (
req: Request,
res: Response,
next: NextFunction
): void => {
try {
const authHeader = req.get("Authorization");
const token = authHeader && authHeader.split(" ")[1];
if (!token) {
res.status(200).json({
success: true,
data: {
isAuthenticated: false,
},
});
return;
}
const validation = JwtUtils.validateToken(token);
res.status(200).json({
success: true,
data: {
isAuthenticated: validation.isValid,
error: validation.error,
},
});
} catch (error) {
logger.error(
`인증 상태 확인 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
res.status(200).json({
success: true,
data: {
isAuthenticated: false,
error: "인증 상태 확인 중 오류가 발생했습니다.",
},
});
}
};

View File

@@ -0,0 +1,84 @@
import { Request, Response, NextFunction } from "express";
import { logger } from "../utils/logger";
// 커스텀 에러 클래스
export class AppError extends Error {
public statusCode: number;
public isOperational: boolean;
constructor(message: string, statusCode: number = 500) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// 에러 핸들러 미들웨어
export const errorHandler = (
err: Error | AppError,
req: Request,
res: Response,
next: NextFunction
): void => {
let error = { ...err };
error.message = err.message;
// Prisma 에러 처리
if (err.name === "PrismaClientKnownRequestError") {
const message = "데이터베이스 요청 오류가 발생했습니다.";
error = new AppError(message, 400);
}
// Prisma 유효성 검증 에러
if (err.name === "PrismaClientValidationError") {
const message = "입력 데이터가 유효하지 않습니다.";
error = new AppError(message, 400);
}
// JWT 에러 처리
if (err.name === "JsonWebTokenError") {
const message = "유효하지 않은 토큰입니다.";
error = new AppError(message, 401);
}
if (err.name === "TokenExpiredError") {
const message = "토큰이 만료되었습니다.";
error = new AppError(message, 401);
}
// 기본 상태 코드 설정
const statusCode = (error as AppError).statusCode || 500;
const message = error.message || "서버 내부 오류가 발생했습니다.";
// 에러 로깅
logger.error({
message: error.message,
stack: error.stack,
url: req.url,
method: req.method,
ip: req.ip,
userAgent: req.get("User-Agent"),
});
// 응답 전송
res.status(statusCode).json({
success: false,
error: {
message: message,
...(process.env.NODE_ENV === "development" && { stack: error.stack }),
},
});
};
// 404 에러 핸들러
export const notFoundHandler = (req: Request, res: Response): void => {
res.status(404).json({
success: false,
error: {
message: "요청한 리소스를 찾을 수 없습니다.",
path: req.originalUrl,
},
});
};