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
|
|
|
|
};
|