// 인증 미들웨어 // JWT 토큰 검증 및 사용자 정보 설정 import { Request, Response, NextFunction } from "express"; import { JwtUtils } from "../utils/jwtUtils"; import { AuthenticatedRequest, PersonBean } from "../types/auth"; import { logger } from "../utils/logger"; // AuthenticatedRequest 타입을 다른 모듈에서 사용할 수 있도록 re-export export { AuthenticatedRequest } from "../types/auth"; // 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) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; logger.error(`인증 실패: ${errorMessage} (${req.ip})`); // 토큰 만료 에러인지 확인 const isTokenExpired = errorMessage.includes("만료"); res.status(401).json({ success: false, error: { code: isTokenExpired ? "TOKEN_EXPIRED" : "INVALID_TOKEN", details: errorMessage || "토큰 검증에 실패했습니다.", }, }); } }; /** * 선택적 인증 미들웨어 (토큰이 없어도 통과) * 일부 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: "인증 상태 확인 중 오류가 발생했습니다.", }, }); } };