duckil_plm/fems-api/src/middleware/auth.middleware.js

209 lines
5.3 KiB
JavaScript
Raw Normal View History

2024-11-02 02:05:37 +09:00
// src/middleware/auth.middleware.js
2024-11-09 06:22:41 +09:00
const jwt = require("jsonwebtoken");
const { promisify } = require("util");
const config = require("../config/config");
const { User, Role, UserRole } = require("../models");
const { AuthenticationError, AuthorizationError } = require("../utils/errors");
const logger = require("../utils/logger");
const redis = require("../config/redis");
2024-11-02 02:05:37 +09:00
2024-11-09 06:22:41 +09:00
/**
* 토큰 검증 사용자 인증 미들웨어
*/
2024-11-02 02:05:37 +09:00
const authMiddleware = async (req, res, next) => {
try {
2024-11-09 06:22:41 +09:00
// 토큰 추출
const token = extractTokenFromHeader(req);
if (!token) {
throw new AuthenticationError("Authentication token is required");
2024-11-02 02:05:37 +09:00
}
2024-11-09 06:22:41 +09:00
// 토큰 검증
const decoded = await verifyToken(token);
2024-11-02 02:05:37 +09:00
2024-11-09 06:22:41 +09:00
// 세션 확인
await validateSession(decoded.id, token);
2024-11-02 02:05:37 +09:00
2024-11-09 06:22:41 +09:00
// 사용자 정보 조회 및 검증
const user = await getUserWithRoles(decoded.id);
validateUser(user);
2024-11-02 02:05:37 +09:00
2024-11-09 06:22:41 +09:00
// 마지막 활동 시간 업데이트
await updateLastActivity(user.id);
// 요청 객체에 사용자 정보 추가
2024-11-02 02:05:37 +09:00
req.user = user;
next();
} catch (error) {
2024-11-09 06:22:41 +09:00
handleAuthError(error, res);
2024-11-02 02:05:37 +09:00
}
};
2024-11-09 06:22:41 +09:00
/**
* 특정 권한 필요한 엔드포인트용 미들웨어
*/
const requireRole = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ message: "Authentication required" });
}
const hasRequiredRole = req.user.Roles.some(
(role) => roles.includes(role.name) && role.isActive
);
if (!hasRequiredRole) {
return res.status(403).json({
message: `Required role: ${roles.join(" or ")}`,
});
}
next();
};
};
/**
* 세부 권한 체크 미들웨어
*/
const checkPermission = (resource, action) => {
return async (req, res, next) => {
try {
if (!req.user) {
throw new AuthenticationError("Authentication required");
}
2024-11-02 02:05:37 +09:00
2024-11-09 06:22:41 +09:00
const hasPermission = await validatePermission(
req.user.id,
resource,
action
);
if (!hasPermission) {
throw new AuthorizationError(
`Permission denied: ${resource}:${action}`
);
}
next();
} catch (error) {
handleAuthError(error, res);
}
};
};
// 유틸리티 함수들
const extractTokenFromHeader = (req) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return null;
}
return authHeader.split(" ")[1];
};
const verifyToken = async (token) => {
try {
// 토큰 블랙리스트 체크
const isBlacklisted = await redis.get(`blacklist:${token}`);
if (isBlacklisted) {
throw new AuthenticationError("Token has been revoked");
}
return await promisify(jwt.verify)(token, config.jwt.secret);
} catch (error) {
if (error.name === "TokenExpiredError") {
throw new AuthenticationError("Token has expired");
}
throw new AuthenticationError("Invalid token");
}
};
const validateSession = async (userId, token) => {
const currentSession = await redis.get(`session:${userId}`);
if (currentSession && currentSession !== token) {
throw new AuthenticationError("Session has been invalidated");
}
};
const getUserWithRoles = async (userId) => {
return await User.findByPk(userId, {
attributes: { exclude: ["password"] },
include: [
{
model: Role,
through: UserRole,
attributes: ["id", "name", "permissions", "isActive"],
where: { isActive: true },
},
],
});
};
const validateUser = (user) => {
if (!user) {
throw new AuthenticationError("User not found");
}
if (!user.isActive) {
throw new AuthenticationError("User account is inactive");
}
if (user.Roles.length === 0) {
throw new AuthorizationError("User has no active roles");
}
};
const validatePermission = async (userId, resource, action) => {
// Redis에서 캐시된 권한 확인
const cacheKey = `permissions:${userId}`;
let permissions = await redis.get(cacheKey);
if (!permissions) {
// DB에서 권한 조회 및 캐싱
const user = await User.findByPk(userId, {
include: [
{
model: Role,
attributes: ["permissions"],
where: { isActive: true },
},
],
});
permissions = user.Roles.reduce((acc, role) => {
const rolePerms = role.permissions || {};
Object.keys(rolePerms).forEach((res) => {
acc[res] = acc[res] || [];
acc[res].push(...rolePerms[res]);
});
return acc;
}, {});
await redis.set(cacheKey, JSON.stringify(permissions), "EX", 3600); // 1시간 캐시
} else {
permissions = JSON.parse(permissions);
}
return permissions[resource]?.includes(action) || false;
};
const updateLastActivity = async (userId) => {
await redis.set(`lastActivity:${userId}`, Date.now());
};
const handleAuthError = (error, res) => {
logger.error("Authentication error", { error });
if (error instanceof AuthenticationError) {
return res.status(401).json({ message: error.message });
}
if (error instanceof AuthorizationError) {
return res.status(403).json({ message: error.message });
}
return res.status(500).json({ message: "Internal server error" });
};
module.exports = {
authMiddleware,
requireRole,
checkPermission,
validatePermission, // Export for testing
};