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;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "scheduledDate",
|
||||
header: "예정일",
|
||||
cell: ({ row }) =>
|
||||
new Date(row.original.scheduledDate).toLocaleDateString(),
|
||||
},
|
||||
// {
|
||||
// accessorKey: "scheduledDate",
|
||||
// header: "예정일",
|
||||
// cell: ({ row }) =>
|
||||
// new Date(row.original.scheduledDate).toLocaleDateString(),
|
||||
// },
|
||||
{
|
||||
accessorKey: "completionDate",
|
||||
header: "완료일",
|
||||
|
Loading…
Reference in New Issue
Block a user