auto commit
This commit is contained in:
parent
ecee18ed9e
commit
5a6cfa01b8
21
fems-api/src/middleware/cors.middleware.js
Normal file
21
fems-api/src/middleware/cors.middleware.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// CORS 설정
|
||||||
|
// src/middleware/cors.middleware.js
|
||||||
|
const cors = require("cors");
|
||||||
|
const config = require("../config");
|
||||||
|
|
||||||
|
const corsOptions = {
|
||||||
|
origin: (origin, callback) => {
|
||||||
|
const whitelist = config.cors.whitelist;
|
||||||
|
if (whitelist.indexOf(origin) !== -1 || !origin) {
|
||||||
|
callback(null, true);
|
||||||
|
} else {
|
||||||
|
callback(new Error("Not allowed by CORS"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
|
allowedHeaders: ["Content-Type", "Authorization"],
|
||||||
|
credentials: true,
|
||||||
|
maxAge: 86400, // 24시간
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = cors(corsOptions);
|
69
fems-api/src/middleware/error.middleware.js
Normal file
69
fems-api/src/middleware/error.middleware.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// src/middleware/error.middleware.js
|
||||||
|
const { logger, auditLogger } = require("../config/logger");
|
||||||
|
|
||||||
|
const errorHandler = (err, req, res) => {
|
||||||
|
err.statusCode = err.statusCode || 500;
|
||||||
|
err.status = err.status || "error";
|
||||||
|
|
||||||
|
// 운영 환경과 개발 환경의 에러 응답 구분
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
logger.error(err.stack);
|
||||||
|
|
||||||
|
res.status(err.statusCode).json({
|
||||||
|
status: err.status,
|
||||||
|
message: err.message,
|
||||||
|
errors: err.errors,
|
||||||
|
stack: err.stack,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 운영 환경에서는 스택 트레이스 제외
|
||||||
|
logger.error(err.message, {
|
||||||
|
status: err.status,
|
||||||
|
statusCode: err.statusCode,
|
||||||
|
errors: err.errors,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 운영 환경에서는 일반적인 에러 메시지만 전송
|
||||||
|
if (err.isOperational) {
|
||||||
|
res.status(err.statusCode).json({
|
||||||
|
status: err.status,
|
||||||
|
message: err.message,
|
||||||
|
errors: err.errors,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 프로그래밍 에러의 경우 일반적인 에러 메시지 전송
|
||||||
|
res.status(500).json({
|
||||||
|
status: "error",
|
||||||
|
message: "Something went wrong!",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 감사 로그 미들웨어
|
||||||
|
const auditMiddleware = (req, res, next) => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// 응답이 완료된 후 로깅
|
||||||
|
res.on("finish", () => {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
auditLogger.info({
|
||||||
|
type: "API_ACCESS",
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
statusCode: res.statusCode,
|
||||||
|
duration,
|
||||||
|
userId: req.user?.id,
|
||||||
|
ip: req.ip,
|
||||||
|
userAgent: req.headers["user-agent"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
errorHandler,
|
||||||
|
auditMiddleware,
|
||||||
|
};
|
29
fems-api/src/middleware/permission.middleware.js
Normal file
29
fems-api/src/middleware/permission.middleware.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// src/middleware/permission.middleware.js
|
||||||
|
const PermissionService = require("../services/permission.service");
|
||||||
|
const permissionService = new PermissionService();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 권한 검사 미들웨어
|
||||||
|
*/
|
||||||
|
const checkPermission = (resource, action) => async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const hasPermission = await permissionService.checkPermission(
|
||||||
|
req.user.id,
|
||||||
|
resource,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
return res.status(403).json({
|
||||||
|
message: "Permission denied",
|
||||||
|
detail: `Required permission: ${resource}:${action}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = checkPermission;
|
24
fems-api/src/middleware/rateLimit.middleware.js
Normal file
24
fems-api/src/middleware/rateLimit.middleware.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// Rate Limiting 미들웨어
|
||||||
|
// src/middleware/rateLimit.middleware.js
|
||||||
|
const rateLimit = require("express-rate-limit");
|
||||||
|
const RedisStore = require("rate-limit-redis");
|
||||||
|
const redis = require("../config/redis");
|
||||||
|
|
||||||
|
const createRateLimiter = (options = {}) => {
|
||||||
|
return rateLimit({
|
||||||
|
store: new RedisStore({
|
||||||
|
client: redis,
|
||||||
|
prefix: "rate-limit:",
|
||||||
|
}),
|
||||||
|
windowMs: options.windowMs || 15 * 60 * 1000, // 기본 15분
|
||||||
|
max: options.max || 100, // 기본 100회
|
||||||
|
message: {
|
||||||
|
status: "error",
|
||||||
|
message: "Too many requests, please try again later.",
|
||||||
|
},
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = createRateLimiter;
|
35
fems-api/src/middleware/securityHeaders.middleware.js
Normal file
35
fems-api/src/middleware/securityHeaders.middleware.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// 보안 헤더 미들웨어
|
||||||
|
// src/middleware/securityHeaders.middleware.js
|
||||||
|
const helmet = require("helmet");
|
||||||
|
|
||||||
|
const securityHeaders = helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'"],
|
||||||
|
fontSrc: ["'self'"],
|
||||||
|
objectSrc: ["'none'"],
|
||||||
|
mediaSrc: ["'self'"],
|
||||||
|
frameSrc: ["'none'"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
crossOriginEmbedderPolicy: true,
|
||||||
|
crossOriginOpenerPolicy: true,
|
||||||
|
crossOriginResourcePolicy: true,
|
||||||
|
dnsPrefetchControl: true,
|
||||||
|
expectCt: true,
|
||||||
|
frameguard: true,
|
||||||
|
hidePoweredBy: true,
|
||||||
|
hsts: true,
|
||||||
|
ieNoOpen: true,
|
||||||
|
noSniff: true,
|
||||||
|
originAgentCluster: true,
|
||||||
|
permittedCrossDomainPolicies: true,
|
||||||
|
referrerPolicy: true,
|
||||||
|
xssFilter: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = securityHeaders;
|
124
fems-api/src/services/permission.service.js
Normal file
124
fems-api/src/services/permission.service.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
// src/services/permission.service.js
|
||||||
|
const { Role, UserRole, User } = require("../models");
|
||||||
|
const redis = require("../config/redis");
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
|
||||||
|
class PermissionService {
|
||||||
|
constructor() {
|
||||||
|
// 권한 캐시 TTL (24시간)
|
||||||
|
this.CACHE_TTL = 24 * 60 * 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자의 권한을 검사
|
||||||
|
*/
|
||||||
|
async checkPermission(userId, resource, action) {
|
||||||
|
try {
|
||||||
|
const permissions = await this.getUserPermissions(userId);
|
||||||
|
return this.evaluatePermission(permissions, resource, action);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Permission check failed", {
|
||||||
|
userId,
|
||||||
|
resource,
|
||||||
|
action,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자의 모든 권한 조회 (캐시 활용)
|
||||||
|
*/
|
||||||
|
async getUserPermissions(userId) {
|
||||||
|
const cacheKey = `permissions:${userId}`;
|
||||||
|
|
||||||
|
// 캐시 확인
|
||||||
|
let permissions = await redis.get(cacheKey);
|
||||||
|
if (permissions) {
|
||||||
|
return JSON.parse(permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DB에서 권한 조회
|
||||||
|
permissions = await this._loadPermissionsFromDB(userId);
|
||||||
|
|
||||||
|
// 캐시 저장
|
||||||
|
await redis.set(
|
||||||
|
cacheKey,
|
||||||
|
JSON.stringify(permissions),
|
||||||
|
"EX",
|
||||||
|
this.CACHE_TTL
|
||||||
|
);
|
||||||
|
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DB에서 사용자 권한 로드
|
||||||
|
*/
|
||||||
|
async _loadPermissionsFromDB(userId) {
|
||||||
|
const user = await User.findByPk(userId, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Role,
|
||||||
|
through: UserRole,
|
||||||
|
where: { isActive: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모든 역할의 권한을 병합
|
||||||
|
return user.Roles.reduce((acc, role) => {
|
||||||
|
return this._mergePermissions(acc, role.permissions);
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 권한 평가
|
||||||
|
*/
|
||||||
|
evaluatePermission(permissions, resource, action) {
|
||||||
|
// 슈퍼관리자 권한 체크
|
||||||
|
if (permissions.superAdmin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 리소스별 권한 체크
|
||||||
|
const resourcePermissions = permissions[resource];
|
||||||
|
if (!resourcePermissions) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 작업 권한 체크
|
||||||
|
return resourcePermissions.includes(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 권한 병합
|
||||||
|
*/
|
||||||
|
_mergePermissions(base, additional) {
|
||||||
|
const merged = { ...base };
|
||||||
|
|
||||||
|
Object.entries(additional).forEach(([resource, actions]) => {
|
||||||
|
if (!merged[resource]) {
|
||||||
|
merged[resource] = new Set();
|
||||||
|
}
|
||||||
|
actions.forEach((action) => merged[resource].add(action));
|
||||||
|
});
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 권한 캐시 무효화
|
||||||
|
*/
|
||||||
|
async invalidateUserPermissions(userId) {
|
||||||
|
const cacheKey = `permissions:${userId}`;
|
||||||
|
await redis.del(cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PermissionService;
|
149
fems-api/src/utils/cache.js
Normal file
149
fems-api/src/utils/cache.js
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
// src/utils/cache.js
|
||||||
|
const redis = require("../config/redis");
|
||||||
|
const logger = require("./logger");
|
||||||
|
const { User } = require("../models");
|
||||||
|
const { DatabaseUtils } = require("./database");
|
||||||
|
const { PermissionService } = require("../services/permission.service");
|
||||||
|
|
||||||
|
class CacheManager {
|
||||||
|
constructor(prefix = "app") {
|
||||||
|
this.prefix = prefix;
|
||||||
|
this.defaultTTL = 3600; // 1시간
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 캐시 키 생성
|
||||||
|
*/
|
||||||
|
_generateKey(key) {
|
||||||
|
return `${this.prefix}:${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 캐시 조회 또는 설정
|
||||||
|
*/
|
||||||
|
async getOrSet(key, callback, ttl = this.defaultTTL) {
|
||||||
|
const cacheKey = this._generateKey(key);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 캐시 확인
|
||||||
|
const cached = await redis.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
return JSON.parse(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐시가 없는 경우 콜백 실행
|
||||||
|
const data = await callback();
|
||||||
|
|
||||||
|
// 캐시 저장
|
||||||
|
await redis.set(cacheKey, JSON.stringify(data), "EX", ttl);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Cache operation failed", { key, error });
|
||||||
|
// 캐시 실패 시 콜백 직접 실행
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 캐시 무효화
|
||||||
|
*/
|
||||||
|
async invalidate(key) {
|
||||||
|
const cacheKey = this._generateKey(key);
|
||||||
|
await redis.del(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 패턴으로 캐시 무효화
|
||||||
|
*/
|
||||||
|
async invalidatePattern(pattern) {
|
||||||
|
const keys = await redis.keys(this._generateKey(pattern));
|
||||||
|
if (keys.length > 0) {
|
||||||
|
await redis.del(keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 해시 캐시 처리
|
||||||
|
*/
|
||||||
|
async hashGetOrSet(hashKey, field, callback, ttl = this.defaultTTL) {
|
||||||
|
const cacheKey = this._generateKey(hashKey);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 해시 필드 확인
|
||||||
|
const cached = await redis.hget(cacheKey, field);
|
||||||
|
if (cached) {
|
||||||
|
return JSON.parse(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐시가 없는 경우 콜백 실행
|
||||||
|
const data = await callback();
|
||||||
|
|
||||||
|
// 해시 필드 저장
|
||||||
|
await redis.hset(cacheKey, field, JSON.stringify(data));
|
||||||
|
await redis.expire(cacheKey, ttl);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Hash cache operation failed", { hashKey, field, error });
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사용 예시를 위한 서비스 구현
|
||||||
|
class UserCacheService {
|
||||||
|
constructor() {
|
||||||
|
this.cacheManager = new CacheManager("users");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 정보 조회 (캐시 적용)
|
||||||
|
*/
|
||||||
|
async getUserById(userId) {
|
||||||
|
return this.cacheManager.getOrSet(
|
||||||
|
`user:${userId}`,
|
||||||
|
async () => {
|
||||||
|
const user = await User.findByPk(userId);
|
||||||
|
return user ? user.toJSON() : null;
|
||||||
|
},
|
||||||
|
3600 // 1시간 캐시
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자의 권한 정보 조회 (해시 캐시 적용)
|
||||||
|
*/
|
||||||
|
async getUserPermissions(userId) {
|
||||||
|
return this.cacheManager.hashGetOrSet(
|
||||||
|
`permissions`,
|
||||||
|
userId,
|
||||||
|
async () => {
|
||||||
|
const permissions = await PermissionService.getUserPermissions(userId);
|
||||||
|
return permissions;
|
||||||
|
},
|
||||||
|
3600 // 1시간 캐시
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 정보 업데이트 후 캐시 무효화
|
||||||
|
*/
|
||||||
|
async updateUser(userId, userData) {
|
||||||
|
return DatabaseUtils.withTransaction(async (transaction) => {
|
||||||
|
const user = await User.findByPk(userId);
|
||||||
|
await user.update(userData, { transaction });
|
||||||
|
|
||||||
|
// 관련 캐시 무효화
|
||||||
|
await this.cacheManager.invalidate(`user:${userId}`);
|
||||||
|
await this.cacheManager.hashGetOrSet(`permissions`, userId);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CacheManager,
|
||||||
|
UserCacheService,
|
||||||
|
};
|
49
fems-api/src/utils/database.js
Normal file
49
fems-api/src/utils/database.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// src/utils/database.js
|
||||||
|
const { sequelize } = require("../models");
|
||||||
|
const logger = require("./logger");
|
||||||
|
|
||||||
|
class DatabaseUtils {
|
||||||
|
/**
|
||||||
|
* 트랜잭션 관리 유틸리티
|
||||||
|
* @param {Function} callback - 트랜잭션 내에서 실행할 콜백 함수
|
||||||
|
*/
|
||||||
|
static async withTransaction(callback) {
|
||||||
|
const transaction = await sequelize.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await callback(transaction);
|
||||||
|
await transaction.commit();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
logger.error("Transaction failed", { error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 벌크 연산 유틸리티
|
||||||
|
* @param {Array} items - 처리할 아이템 배열
|
||||||
|
* @param {Function} handler - 각 아이템을 처리할 핸들러 함수
|
||||||
|
* @param {Object} options - 설정 옵션
|
||||||
|
*/
|
||||||
|
static async bulkOperation(items, handler, options = {}) {
|
||||||
|
const batchSize = options.batchSize || 1000;
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i += batchSize) {
|
||||||
|
const batch = items.slice(i, i + batchSize);
|
||||||
|
|
||||||
|
await this.withTransaction(async (transaction) => {
|
||||||
|
const batchResults = await Promise.all(
|
||||||
|
batch.map((item) => handler(item, transaction))
|
||||||
|
);
|
||||||
|
results.push(...batchResults);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DatabaseUtils;
|
37
fems-api/src/utils/errors.js
Normal file
37
fems-api/src/utils/errors.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// src/utils/errors.js
|
||||||
|
class AppError extends Error {
|
||||||
|
constructor(message, statusCode = 500, errors = []) {
|
||||||
|
super(message);
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.errors = errors;
|
||||||
|
this.status = `${statusCode}`.startsWith("4") ? "fail" : "error";
|
||||||
|
this.isOperational = true;
|
||||||
|
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ValidationError extends AppError {
|
||||||
|
constructor(message, errors = []) {
|
||||||
|
super(message, 400, errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthenticationError extends AppError {
|
||||||
|
constructor(message) {
|
||||||
|
super(message, 401);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthorizationError extends AppError {
|
||||||
|
constructor(message) {
|
||||||
|
super(message, 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
AppError,
|
||||||
|
ValidationError,
|
||||||
|
AuthenticationError,
|
||||||
|
AuthorizationError,
|
||||||
|
};
|
59
fems-api/src/utils/logger.js
Normal file
59
fems-api/src/utils/logger.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// src/utils/logger.js
|
||||||
|
const winston = require("winston");
|
||||||
|
const { createLogger, format, transports } = winston;
|
||||||
|
const { combine, timestamp, printf, colorize } = format;
|
||||||
|
|
||||||
|
// 로그 포맷 정의
|
||||||
|
const logFormat = printf(({ level, message, timestamp, ...metadata }) => {
|
||||||
|
let msg = `${timestamp} [${level}] : ${message}`;
|
||||||
|
|
||||||
|
if (Object.keys(metadata).length > 0) {
|
||||||
|
msg += JSON.stringify(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 로거 생성
|
||||||
|
const logger = createLogger({
|
||||||
|
format: combine(timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), logFormat),
|
||||||
|
transports: [
|
||||||
|
// 콘솔 출력
|
||||||
|
new transports.Console({
|
||||||
|
format: combine(colorize(), logFormat),
|
||||||
|
}),
|
||||||
|
// 에러 로그 파일
|
||||||
|
new transports.File({
|
||||||
|
filename: "logs/error.log",
|
||||||
|
level: "error",
|
||||||
|
}),
|
||||||
|
// 전체 로그 파일
|
||||||
|
new transports.File({
|
||||||
|
filename: "logs/combined.log",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 감사 로그를 위한 별도 로거
|
||||||
|
const auditLogger = createLogger({
|
||||||
|
format: combine(
|
||||||
|
timestamp(),
|
||||||
|
printf(({ message, timestamp, ...metadata }) => {
|
||||||
|
return JSON.stringify({
|
||||||
|
timestamp,
|
||||||
|
...message,
|
||||||
|
...metadata,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
),
|
||||||
|
transports: [
|
||||||
|
new transports.File({
|
||||||
|
filename: "logs/audit.log",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
logger,
|
||||||
|
auditLogger,
|
||||||
|
};
|
125
fems-api/src/utils/security.js
Normal file
125
fems-api/src/utils/security.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
// src/utils/security.js
|
||||||
|
const crypto = require("crypto");
|
||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
const { AuthenticationError } = require("./errors");
|
||||||
|
const config = require("../config/config");
|
||||||
|
const redis = require("../config/redis");
|
||||||
|
// const logger = require("./logger");
|
||||||
|
|
||||||
|
class SecurityUtils {
|
||||||
|
/**
|
||||||
|
* JWT 토큰 생성
|
||||||
|
*/
|
||||||
|
static generateTokens(payload) {
|
||||||
|
const accessToken = jwt.sign(payload, config.jwt.secret, {
|
||||||
|
expiresIn: config.jwt.accessTokenExpiry,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshToken = jwt.sign(payload, config.jwt.refreshSecret, {
|
||||||
|
expiresIn: config.jwt.refreshTokenExpiry,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰 검증
|
||||||
|
*/
|
||||||
|
static async verifyToken(token, isRefreshToken = false) {
|
||||||
|
try {
|
||||||
|
// 토큰 블랙리스트 확인
|
||||||
|
const isBlacklisted = await this.checkTokenBlacklist(token);
|
||||||
|
if (isBlacklisted) {
|
||||||
|
throw new AuthenticationError("Token has been revoked");
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = isRefreshToken
|
||||||
|
? config.jwt.refreshSecret
|
||||||
|
: config.jwt.secret;
|
||||||
|
return jwt.verify(token, secret);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === "TokenExpiredError") {
|
||||||
|
throw new AuthenticationError("Token has expired");
|
||||||
|
}
|
||||||
|
throw new AuthenticationError("Invalid token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰 블랙리스트 관리
|
||||||
|
*/
|
||||||
|
static async addToBlacklist(token, expirationTime) {
|
||||||
|
const tokenHash = this.hashToken(token);
|
||||||
|
await redis.set(`blacklist:${tokenHash}`, "true", "EX", expirationTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async checkTokenBlacklist(token) {
|
||||||
|
const tokenHash = this.hashToken(token);
|
||||||
|
return await redis.exists(`blacklist:${tokenHash}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰 해시화
|
||||||
|
*/
|
||||||
|
static hashToken(token) {
|
||||||
|
return crypto.createHash("sha256").update(token).digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 키 생성 및 관리
|
||||||
|
*/
|
||||||
|
static generateApiKey() {
|
||||||
|
return crypto.randomBytes(32).toString("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IP 제한 검사
|
||||||
|
*/
|
||||||
|
static checkIpRestriction(allowedIps, currentIp) {
|
||||||
|
if (!allowedIps || allowedIps.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return allowedIps.some((ip) => {
|
||||||
|
if (ip.includes("*")) {
|
||||||
|
const pattern = ip.replace(/\*/g, "\\d+");
|
||||||
|
const regex = new RegExp(`^${pattern}$`);
|
||||||
|
return regex.test(currentIp);
|
||||||
|
}
|
||||||
|
return ip === currentIp;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데이터 암호화/복호화
|
||||||
|
*/
|
||||||
|
static encrypt(text) {
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
const cipher = crypto.createCipheriv(
|
||||||
|
"aes-256-cbc",
|
||||||
|
Buffer.from(config.encryption.key),
|
||||||
|
iv
|
||||||
|
);
|
||||||
|
let encrypted = cipher.update(text);
|
||||||
|
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||||
|
return `${iv.toString("hex")}:${encrypted.toString("hex")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static decrypt(text) {
|
||||||
|
const [ivHex, encryptedHex] = text.split(":");
|
||||||
|
const iv = Buffer.from(ivHex, "hex");
|
||||||
|
const encrypted = Buffer.from(encryptedHex, "hex");
|
||||||
|
const decipher = crypto.createDecipheriv(
|
||||||
|
"aes-256-cbc",
|
||||||
|
Buffer.from(config.encryption.key),
|
||||||
|
iv
|
||||||
|
);
|
||||||
|
let decrypted = decipher.update(encrypted);
|
||||||
|
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||||
|
return decrypted.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SecurityUtils;
|
@ -95,12 +95,12 @@ const MaintenancePage = () => {
|
|||||||
return statusMap[row.original.status] || row.original.status;
|
return statusMap[row.original.status] || row.original.status;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
accessorKey: "scheduledDate",
|
// accessorKey: "scheduledDate",
|
||||||
header: "예정일",
|
// header: "예정일",
|
||||||
cell: ({ row }) =>
|
// cell: ({ row }) =>
|
||||||
new Date(row.original.scheduledDate).toLocaleDateString(),
|
// new Date(row.original.scheduledDate).toLocaleDateString(),
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
accessorKey: "completionDate",
|
accessorKey: "completionDate",
|
||||||
header: "완료일",
|
header: "완료일",
|
||||||
|
Loading…
Reference in New Issue
Block a user