- Integrated express-async-errors to automatically handle errors in async route handlers, enhancing the overall error management in the application. - Updated app.ts to include the express-async-errors import for global error handling. - Removed redundant logging statements in admin and user menu retrieval functions to streamline the code and improve readability. - Adjusted logging levels from info to debug for less critical logs, ensuring that important information is logged appropriately without cluttering the logs.
259 lines
6.5 KiB
TypeScript
259 lines
6.5 KiB
TypeScript
// 인증 미들웨어
|
|
// 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.debug(`선택적 인증 성공: ${userInfo.userId} (${req.ip})`);
|
|
} else {
|
|
logger.debug(`선택적 인증: 토큰 없음 (${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: "인증 상태 확인 중 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
};
|