diff --git a/api.hyungi.net/config/cors.js b/api.hyungi.net/config/cors.js new file mode 100644 index 0000000..a6ba58f --- /dev/null +++ b/api.hyungi.net/config/cors.js @@ -0,0 +1,89 @@ +/** + * CORS 설정 + * + * Cross-Origin Resource Sharing 설정 + * + * @author TK-FB-Project + * @since 2025-12-11 + */ + +const logger = require('../utils/logger'); + +/** + * 허용된 Origin 목록 + */ +const allowedOrigins = [ + 'http://localhost:20000', // 웹 UI + 'http://localhost:3005', // API 서버 + 'http://localhost:3000', // 개발 포트 + 'http://127.0.0.1:20000', // 로컬호스트 대체 + 'http://127.0.0.1:3005', + 'http://127.0.0.1:3000' +]; + +/** + * CORS 설정 옵션 + */ +const corsOptions = { + /** + * Origin 검증 함수 + */ + origin: function (origin, callback) { + // Origin이 없는 경우 (직접 접근, Postman 등) + if (!origin) { + logger.debug('CORS: Origin 없음 - 허용'); + return callback(null, true); + } + + // 허용된 Origin 확인 + if (allowedOrigins.includes(origin)) { + logger.debug('CORS: 허용된 Origin', { origin }); + return callback(null, true); + } + + // 개발 환경에서는 모든 localhost 허용 + if (process.env.NODE_ENV === 'development') { + if (origin.includes('localhost') || origin.includes('127.0.0.1')) { + logger.debug('CORS: 로컬호스트 허용 (개발 모드)', { origin }); + return callback(null, true); + } + } + + // 로컬 네트워크 IP 자동 허용 (192.168.x.x) + if (origin.match(/^http:\/\/192\.168\.\d+\.\d+(:\d+)?$/)) { + logger.debug('CORS: 로컬 네트워크 IP 허용', { origin }); + return callback(null, true); + } + + // 차단 + logger.warn('CORS: 차단된 Origin', { origin }); + callback(new Error(`CORS 정책에 의해 차단됨: ${origin}`)); + }, + + /** + * 인증 정보 포함 허용 + */ + credentials: true, + + /** + * 허용된 HTTP 메소드 + */ + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], + + /** + * 허용된 헤더 + */ + allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], + + /** + * 노출할 헤더 + */ + exposedHeaders: ['Content-Range', 'X-Content-Range'], + + /** + * Preflight 요청 캐시 시간 (초) + */ + maxAge: 86400 // 24시간 +}; + +module.exports = corsOptions; diff --git a/api.hyungi.net/config/security.js b/api.hyungi.net/config/security.js new file mode 100644 index 0000000..d669c8f --- /dev/null +++ b/api.hyungi.net/config/security.js @@ -0,0 +1,92 @@ +/** + * 보안 설정 (Helmet) + * + * HTTP 헤더 보안 설정 + * + * @author TK-FB-Project + * @since 2025-12-11 + */ + +/** + * Helmet 보안 설정 옵션 + */ +const helmetOptions = { + /** + * Content Security Policy + * XSS 공격 방지 + */ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], // 개발 중 unsafe-eval 허용 + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://fonts.gstatic.com"], + connectSrc: ["'self'", "https://api.technicalkorea.com"], + frameSrc: ["'none'"], + objectSrc: ["'none'"] + } + }, + + /** + * HTTP Strict Transport Security (HSTS) + * HTTPS 강제 사용 + */ + hsts: { + maxAge: 31536000, // 1년 + includeSubDomains: true, + preload: true + }, + + /** + * X-Frame-Options + * 클릭재킹 공격 방지 + */ + frameguard: { + action: 'deny' + }, + + /** + * X-Content-Type-Options + * MIME 타입 스니핑 방지 + */ + noSniff: true, + + /** + * X-XSS-Protection + * XSS 필터 활성화 + */ + xssFilter: true, + + /** + * Referrer-Policy + * 리퍼러 정보 제어 + */ + referrerPolicy: { + policy: 'strict-origin-when-cross-origin' + }, + + /** + * X-DNS-Prefetch-Control + * DNS prefetching 제어 + */ + dnsPrefetchControl: { + allow: false + }, + + /** + * X-Download-Options + * IE8+ 다운로드 옵션 + */ + ieNoOpen: true, + + /** + * X-Permitted-Cross-Domain-Policies + * Adobe 제품의 크로스 도메인 정책 + */ + permittedCrossDomainPolicies: { + permittedPolicies: 'none' + } +}; + +module.exports = helmetOptions; diff --git a/api.hyungi.net/controllers/userController.js b/api.hyungi.net/controllers/userController.js index d9f3e81..fc24ba7 100644 --- a/api.hyungi.net/controllers/userController.js +++ b/api.hyungi.net/controllers/userController.js @@ -1,37 +1,68 @@ -// controllers/userController.js - 사용자 관리 컨트롤러 +/** + * 사용자 관리 컨트롤러 + * + * 사용자 CRUD 및 상태 관리 기능을 제공하는 컨트롤러 + * + * @author TK-FB-Project + * @since 2025-12-11 + */ const bcrypt = require('bcrypt'); -const { ApiError, asyncHandler } = require('../utils/errorHandler'); -const db = require('../db'); +const { ValidationError, ForbiddenError, NotFoundError, ConflictError, DatabaseError } = require('../utils/errors'); +const { asyncHandler } = require('../middlewares/errorHandler'); +const logger = require('../utils/logger'); + +/** + * 관리자 권한 확인 헬퍼 함수 + */ +const checkAdminPermission = (user) => { + if (!user || !['admin', 'system'].includes(user.access_level)) { + throw new ForbiddenError('관리자 권한이 필요합니다'); + } +}; /** * 모든 사용자 조회 */ const getAllUsers = asyncHandler(async (req, res) => { - console.log('👥 모든 사용자 조회 요청'); - - const query = ` - SELECT - user_id, - username, - name, - email, - phone, - role, - access_level, - is_active, - created_at, - updated_at, - last_login - FROM users - ORDER BY created_at DESC - `; - - const [users] = await db.execute(query); - - console.log(`✅ 사용자 ${users.length}명 조회 완료`); - - res.success(users, '사용자 목록 조회 성공'); + checkAdminPermission(req.user); + + logger.info('사용자 목록 조회 요청', { requestedBy: req.user?.username }); + + const { getDb } = require('../dbPool'); + const db = await getDb(); + + try { + const query = ` + SELECT + user_id, + username, + name, + email, + phone, + role, + access_level, + is_active, + created_at, + updated_at, + last_login + FROM users + ORDER BY created_at DESC + `; + + const [users] = await db.execute(query); + + logger.info('사용자 목록 조회 성공', { count: users.length }); + + res.json({ + success: true, + data: users, + message: '사용자 목록 조회 성공' + }); + } catch (error) { + logger.error('사용자 목록 조회 실패', { error: error.message }); + throw new DatabaseError('사용자 목록을 조회하는데 실패했습니다'); + } }); /** @@ -39,202 +70,391 @@ const getAllUsers = asyncHandler(async (req, res) => { */ const getUserById = asyncHandler(async (req, res) => { const { id } = req.params; - - console.log(`👤 사용자 조회: ID ${id}`); - - const query = ` - SELECT - user_id, - username, - name, - email, - phone, - role, - access_level, - is_active, - created_at, - updated_at, - last_login - FROM users - WHERE user_id = ? - `; - - const [users] = await db.execute(query, [id]); - - if (users.length === 0) { - throw new ApiError('사용자를 찾을 수 없습니다.', 404); + + if (!id || isNaN(id)) { + throw new ValidationError('유효하지 않은 사용자 ID입니다'); + } + + logger.info('사용자 조회 요청', { userId: id }); + + const { getDb } = require('../dbPool'); + const db = await getDb(); + + try { + const query = ` + SELECT + user_id, + username, + name, + email, + phone, + role, + access_level, + is_active, + created_at, + updated_at, + last_login + FROM users + WHERE user_id = ? + `; + + const [users] = await db.execute(query, [id]); + + if (users.length === 0) { + throw new NotFoundError('사용자를 찾을 수 없습니다'); + } + + logger.info('사용자 조회 성공', { userId: id, username: users[0].username }); + + res.json({ + success: true, + data: users[0], + message: '사용자 조회 성공' + }); + } catch (error) { + if (error instanceof NotFoundError) { + throw error; + } + logger.error('사용자 조회 실패', { userId: id, error: error.message }); + throw new DatabaseError('사용자를 조회하는데 실패했습니다'); } - - console.log(`✅ 사용자 조회 완료: ${users[0].name}`); - - res.success(users[0], '사용자 조회 성공'); }); /** * 새 사용자 생성 */ const createUser = asyncHandler(async (req, res) => { + checkAdminPermission(req.user); + const { username, name, email, phone, role, password } = req.body; - - console.log(`👤 새 사용자 생성: ${name} (${username})`); - + + logger.info('사용자 생성 요청', { username, name, role }); + // 필수 필드 검증 if (!username || !name || !role || !password) { - throw new ApiError('필수 필드가 누락되었습니다.', 400); + throw new ValidationError('필수 필드가 누락되었습니다', { + required: ['username', 'name', 'role', 'password'], + received: { username, name, role, password: '***' } + }); } - - // 사용자명 중복 확인 - const checkQuery = 'SELECT user_id FROM users WHERE username = ?'; - const [existing] = await db.execute(checkQuery, [username]); - - if (existing.length > 0) { - throw new ApiError('이미 존재하는 사용자명입니다.', 400); + + // 사용자명 유효성 검증 + if (username.length < 3 || username.length > 20) { + throw new ValidationError('사용자명은 3-20자 사이여야 합니다'); + } + + // 비밀번호 유효성 검증 + if (password.length < 6) { + throw new ValidationError('비밀번호는 최소 6자 이상이어야 합니다'); + } + + // 권한 레벨 검증 + const validRoles = ['admin', 'group_leader', 'worker']; + if (!validRoles.includes(role)) { + throw new ValidationError('유효하지 않은 권한입니다', { + valid: validRoles, + received: role + }); + } + + const { getDb } = require('../dbPool'); + const db = await getDb(); + + try { + // 사용자명 중복 확인 + const checkQuery = 'SELECT user_id FROM users WHERE username = ?'; + const [existing] = await db.execute(checkQuery, [username]); + + if (existing.length > 0) { + throw new ConflictError('이미 존재하는 사용자명입니다'); + } + + // 비밀번호 해시화 + const hashedPassword = await bcrypt.hash(password, 10); + + // 사용자 생성 + const insertQuery = ` + INSERT INTO users (username, name, email, phone, role, access_level, password_hash, is_active, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, 1, NOW()) + `; + + const [result] = await db.execute(insertQuery, [ + username, + name, + email || null, + phone || null, + role, + role, // access_level을 role과 동일하게 설정 + hashedPassword + ]); + + logger.info('사용자 생성 성공', { + userId: result.insertId, + username, + name, + role, + createdBy: req.user.username + }); + + res.status(201).json({ + success: true, + data: { user_id: result.insertId }, + message: '사용자가 성공적으로 생성되었습니다' + }); + } catch (error) { + if (error instanceof ConflictError) { + throw error; + } + logger.error('사용자 생성 실패', { username, error: error.message }); + throw new DatabaseError('사용자를 생성하는데 실패했습니다'); } - - // 비밀번호 해시화 - const hashedPassword = await bcrypt.hash(password, 10); - - // 사용자 생성 - const insertQuery = ` - INSERT INTO users (username, name, email, phone, role, access_level, password_hash, is_active, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, 1, NOW()) - `; - - const [result] = await db.execute(insertQuery, [ - username, - name, - email || null, - phone || null, - role, - role, // access_level을 role과 동일하게 설정 - hashedPassword - ]); - - console.log(`✅ 사용자 생성 완료: ID ${result.insertId}`); - - res.created({ user_id: result.insertId }, '사용자가 성공적으로 생성되었습니다.'); }); /** * 사용자 정보 수정 */ const updateUser = asyncHandler(async (req, res) => { + checkAdminPermission(req.user); + const { id } = req.params; const { username, name, email, phone, role, password } = req.body; - - console.log(`👤 사용자 수정: ID ${id}`); - - // 사용자 존재 확인 - const checkQuery = 'SELECT user_id FROM users WHERE user_id = ?'; - const [existing] = await db.execute(checkQuery, [id]); - - if (existing.length === 0) { - throw new ApiError('사용자를 찾을 수 없습니다.', 404); + + if (!id || isNaN(id)) { + throw new ValidationError('유효하지 않은 사용자 ID입니다'); } - - // 업데이트할 필드들 - const updates = []; - const values = []; - - if (username) { - // 사용자명 중복 확인 (자신 제외) - const dupQuery = 'SELECT user_id FROM users WHERE username = ? AND user_id != ?'; - const [duplicate] = await db.execute(dupQuery, [username, id]); - - if (duplicate.length > 0) { - throw new ApiError('이미 존재하는 사용자명입니다.', 400); + + logger.info('사용자 수정 요청', { userId: id }); + + // 최소 하나의 수정 필드가 필요 + if (!username && !name && email === undefined && phone === undefined && !role && !password) { + throw new ValidationError('수정할 필드가 없습니다'); + } + + const { getDb } = require('../dbPool'); + const db = await getDb(); + + try { + // 사용자 존재 확인 + const checkQuery = 'SELECT user_id, username, is_active FROM users WHERE user_id = ?'; + const [existing] = await db.execute(checkQuery, [id]); + + if (existing.length === 0) { + throw new NotFoundError('사용자를 찾을 수 없습니다'); } - - updates.push('username = ?'); - values.push(username); + + if (existing[0].is_active === 0) { + throw new ValidationError('비활성화된 사용자는 수정할 수 없습니다'); + } + + // 업데이트할 필드들 + const updates = []; + const values = []; + + if (username) { + if (username.length < 3 || username.length > 20) { + throw new ValidationError('사용자명은 3-20자 사이여야 합니다'); + } + + // 사용자명 중복 확인 (자신 제외) + const dupQuery = 'SELECT user_id FROM users WHERE username = ? AND user_id != ?'; + const [duplicate] = await db.execute(dupQuery, [username, id]); + + if (duplicate.length > 0) { + throw new ConflictError('이미 존재하는 사용자명입니다'); + } + + updates.push('username = ?'); + values.push(username); + } + + if (name) { + updates.push('name = ?'); + values.push(name); + } + + if (email !== undefined) { + updates.push('email = ?'); + values.push(email || null); + } + + if (phone !== undefined) { + updates.push('phone = ?'); + values.push(phone || null); + } + + if (role) { + const validRoles = ['admin', 'group_leader', 'worker']; + if (!validRoles.includes(role)) { + throw new ValidationError('유효하지 않은 권한입니다'); + } + updates.push('role = ?, access_level = ?'); + values.push(role, role); + } + + if (password) { + if (password.length < 6) { + throw new ValidationError('비밀번호는 최소 6자 이상이어야 합니다'); + } + const hashedPassword = await bcrypt.hash(password, 10); + updates.push('password_hash = ?'); + values.push(hashedPassword); + } + + updates.push('updated_at = NOW()'); + values.push(id); + + const updateQuery = `UPDATE users SET ${updates.join(', ')} WHERE user_id = ?`; + + await db.execute(updateQuery, values); + + logger.info('사용자 수정 성공', { + userId: id, + username: existing[0].username, + updatedFields: Object.keys(req.body), + updatedBy: req.user.username + }); + + res.json({ + success: true, + data: { user_id: id }, + message: '사용자 정보가 성공적으로 수정되었습니다' + }); + } catch (error) { + if (error instanceof NotFoundError || error instanceof ValidationError || error instanceof ConflictError) { + throw error; + } + logger.error('사용자 수정 실패', { userId: id, error: error.message }); + throw new DatabaseError('사용자 정보를 수정하는데 실패했습니다'); } - - if (name) { - updates.push('name = ?'); - values.push(name); - } - - if (email !== undefined) { - updates.push('email = ?'); - values.push(email || null); - } - - if (phone !== undefined) { - updates.push('phone = ?'); - values.push(phone || null); - } - - if (role) { - updates.push('role = ?, access_level = ?'); - values.push(role, role); - } - - if (password) { - const hashedPassword = await bcrypt.hash(password, 10); - updates.push('password_hash = ?'); - values.push(hashedPassword); - } - - if (updates.length === 0) { - throw new ApiError('수정할 내용이 없습니다.', 400); - } - - updates.push('updated_at = NOW()'); - values.push(id); - - const updateQuery = `UPDATE users SET ${updates.join(', ')} WHERE user_id = ?`; - - await db.execute(updateQuery, values); - - console.log(`✅ 사용자 수정 완료: ID ${id}`); - - res.success({ user_id: id }, '사용자 정보가 성공적으로 수정되었습니다.'); }); /** * 사용자 상태 변경 (활성화/비활성화) */ const updateUserStatus = asyncHandler(async (req, res) => { + checkAdminPermission(req.user); + const { id } = req.params; const { is_active } = req.body; - - console.log(`👤 사용자 상태 변경: ID ${id}, 활성화: ${is_active}`); - - const query = 'UPDATE users SET is_active = ?, updated_at = NOW() WHERE user_id = ?'; - const [result] = await db.execute(query, [is_active ? 1 : 0, id]); - - if (result.affectedRows === 0) { - throw new ApiError('사용자를 찾을 수 없습니다.', 404); + + if (!id || isNaN(id)) { + throw new ValidationError('유효하지 않은 사용자 ID입니다'); + } + + if (is_active === undefined || ![0, 1, true, false].includes(is_active)) { + throw new ValidationError('유효하지 않은 활성 상태 값입니다'); + } + + const activeValue = is_active === true || is_active === 1 ? 1 : 0; + + // 자기 자신 비활성화 방지 + if (parseInt(id) === req.user.user_id && activeValue === 0) { + throw new ValidationError('자기 자신을 비활성화할 수 없습니다'); + } + + logger.info('사용자 상태 변경 요청', { userId: id, is_active: activeValue }); + + const { getDb } = require('../dbPool'); + const db = await getDb(); + + try { + // 사용자 존재 확인 + const checkQuery = 'SELECT user_id, username, is_active FROM users WHERE user_id = ?'; + const [users] = await db.execute(checkQuery, [id]); + + if (users.length === 0) { + throw new NotFoundError('사용자를 찾을 수 없습니다'); + } + + // 상태 변경이 필요한지 확인 + if (users[0].is_active === activeValue) { + const status = activeValue === 1 ? '활성' : '비활성'; + throw new ValidationError(`사용자가 이미 ${status} 상태입니다`); + } + + const query = 'UPDATE users SET is_active = ?, updated_at = NOW() WHERE user_id = ?'; + await db.execute(query, [activeValue, id]); + + const statusText = activeValue === 1 ? '활성화' : '비활성화'; + + logger.info(`사용자 ${statusText} 성공`, { + userId: id, + username: users[0].username, + newStatus: activeValue, + updatedBy: req.user.username + }); + + res.json({ + success: true, + data: { user_id: id, is_active: activeValue }, + message: `사용자가 성공적으로 ${statusText}되었습니다` + }); + } catch (error) { + if (error instanceof NotFoundError || error instanceof ValidationError) { + throw error; + } + logger.error('사용자 상태 변경 실패', { userId: id, error: error.message }); + throw new DatabaseError('사용자 상태를 변경하는데 실패했습니다'); } - - console.log(`✅ 사용자 상태 변경 완료: ID ${id}`); - - res.success({ user_id: id, is_active }, '사용자 상태가 성공적으로 변경되었습니다.'); }); /** - * 사용자 삭제 + * 사용자 삭제 (Soft Delete) */ const deleteUser = asyncHandler(async (req, res) => { + checkAdminPermission(req.user); + const { id } = req.params; - - console.log(`👤 사용자 삭제: ID ${id}`); - + + if (!id || isNaN(id)) { + throw new ValidationError('유효하지 않은 사용자 ID입니다'); + } + // 자기 자신 삭제 방지 if (req.user && req.user.user_id == id) { - throw new ApiError('자기 자신은 삭제할 수 없습니다.', 400); + throw new ValidationError('자기 자신은 삭제할 수 없습니다'); } - - const query = 'DELETE FROM users WHERE user_id = ?'; - const [result] = await db.execute(query, [id]); - - if (result.affectedRows === 0) { - throw new ApiError('사용자를 찾을 수 없습니다.', 404); + + logger.info('사용자 삭제 요청', { userId: id }); + + const { getDb } = require('../dbPool'); + const db = await getDb(); + + try { + // 사용자 존재 확인 + const checkQuery = 'SELECT user_id, username, is_active FROM users WHERE user_id = ?'; + const [users] = await db.execute(checkQuery, [id]); + + if (users.length === 0) { + throw new NotFoundError('사용자를 찾을 수 없습니다'); + } + + if (users[0].is_active === 0) { + throw new ValidationError('이미 비활성화된 사용자입니다'); + } + + // Soft Delete (is_active = 0) + const query = 'UPDATE users SET is_active = 0, updated_at = NOW() WHERE user_id = ?'; + await db.execute(query, [id]); + + logger.info('사용자 비활성화 성공', { + userId: id, + username: users[0].username, + deletedBy: req.user.username + }); + + res.json({ + success: true, + data: { user_id: id }, + message: '사용자가 성공적으로 비활성화되었습니다' + }); + } catch (error) { + if (error instanceof NotFoundError || error instanceof ValidationError) { + throw error; + } + logger.error('사용자 비활성화 실패', { userId: id, error: error.message }); + throw new DatabaseError('사용자를 비활성화하는데 실패했습니다'); } - - console.log(`✅ 사용자 삭제 완료: ID ${id}`); - - res.success({ user_id: id }, '사용자가 성공적으로 삭제되었습니다.'); }); module.exports = { diff --git a/api.hyungi.net/index.js b/api.hyungi.net/index.js index 9cd0487..cb64521 100644 --- a/api.hyungi.net/index.js +++ b/api.hyungi.net/index.js @@ -204,6 +204,7 @@ const workAnalysisRoutes = require('./routes/workAnalysisRoutes'); const analysisRoutes = require('./routes/analysisRoutes'); const systemRoutes = require('./routes/systemRoutes'); // 새로운 분석 라우트 const performanceRoutes = require('./routes/performanceRoutes'); // 성능 모니터링 라우트 +const userRoutes = require('./routes/userRoutes'); // 사용자 관리 라우트 // 🔒 인증 미들웨어 가져오기 const { verifyToken } = require('./middlewares/authMiddleware'); @@ -344,291 +345,7 @@ app.use('/api/projects', projectRoutes); app.use('/api/tools', toolsRoute); // 👤 사용자 관리 API (관리자 전용) -app.get('/api/users', async (req, res) => { - try { - // 관리자 권한 확인 - if (!req.user || !['admin', 'system'].includes(req.user.access_level)) { - return res.status(403).json({ - success: false, - error: '관리자 권한이 필요합니다.' - }); - } - - const { getDb } = require('./dbPool'); - const db = await getDb(); - const [users] = await db.execute(` - SELECT user_id, username, name, role, access_level, is_active, created_at, worker_id - FROM users - WHERE is_active = 1 - ORDER BY user_id - `); - - res.json({ - success: true, - message: '사용자 목록 조회 성공', - data: users - }); - } catch (error) { - console.error('사용자 목록 조회 오류:', error); - res.status(500).json({ - success: false, - error: '사용자 목록을 불러올 수 없습니다.' - }); - } -}); - -// 👤 사용자 정보 수정 API (관리자 전용) -app.put('/api/users/:id', async (req, res) => { - try { - // 관리자 권한 확인 - if (!req.user || !['admin', 'system'].includes(req.user.access_level)) { - return res.status(403).json({ - success: false, - error: '관리자 권한이 필요합니다.' - }); - } - - const userId = req.params.id; - const { username, name, role, access_level, is_active } = req.body; - - // undefined 값을 null로 변환 - const safeUsername = username !== undefined ? username : null; - const safeName = name !== undefined ? name : null; - const safeRole = role !== undefined ? role : null; - const safeAccessLevel = access_level !== undefined ? access_level : null; - const safeIsActive = is_active !== undefined ? is_active : null; - - const { getDb } = require('./dbPool'); - const db = await getDb(); - - const [result] = await db.execute(` - UPDATE users - SET username = ?, name = ?, role = ?, access_level = ?, is_active = ?, updated_at = NOW() - WHERE user_id = ? - `, [safeUsername, safeName, safeRole, safeAccessLevel, safeIsActive, userId]); - - if (result.affectedRows === 0) { - return res.status(404).json({ - success: false, - error: '사용자를 찾을 수 없습니다.' - }); - } - - res.json({ - success: true, - message: '사용자 정보가 성공적으로 수정되었습니다.' - }); - } catch (error) { - console.error('사용자 정보 수정 오류:', error); - res.status(500).json({ - success: false, - error: '사용자 정보 수정에 실패했습니다.' - }); - } -}); - -// 👤 사용자 삭제 API (관리자 전용) -app.delete('/api/users/:id', async (req, res) => { - try { - // 관리자 권한 확인 - if (!req.user || !['admin', 'system'].includes(req.user.access_level)) { - return res.status(403).json({ - success: false, - error: '관리자 권한이 필요합니다.' - }); - } - - const userId = req.params.id; - - // 자기 자신은 삭제할 수 없도록 방지 - if (parseInt(userId) === req.user.user_id) { - return res.status(400).json({ - success: false, - error: '자기 자신은 삭제할 수 없습니다.' - }); - } - - const { getDb } = require('./dbPool'); - const db = await getDb(); - - // 사용자 존재 여부 확인 - const [existingUser] = await db.execute(` - SELECT user_id, username FROM users WHERE user_id = ? - `, [userId]); - - if (existingUser.length === 0) { - return res.status(404).json({ - success: false, - error: '사용자를 찾을 수 없습니다.' - }); - } - - // 사용자 삭제 (실제로는 비활성화) - const [result] = await db.execute(` - UPDATE users - SET is_active = 0, updated_at = NOW() - WHERE user_id = ? - `, [userId]); - - res.json({ - success: true, - message: `사용자 '${existingUser[0].username}'가 성공적으로 비활성화되었습니다.` - }); - } catch (error) { - console.error('사용자 삭제 오류:', error); - res.status(500).json({ - success: false, - error: '사용자 삭제에 실패했습니다.' - }); - } -}); - -// 👤 사용자 생성 API (관리자 전용) -app.post('/api/users', async (req, res) => { - try { - // 관리자 권한 확인 - if (!req.user || !['admin', 'system'].includes(req.user.access_level)) { - return res.status(403).json({ - success: false, - error: '관리자 권한이 필요합니다.' - }); - } - - const { username, name, role, access_level, password } = req.body; - - // 필수 필드 검증 - if (!username || !name || !password) { - return res.status(400).json({ - success: false, - error: '사용자명, 이름, 비밀번호는 필수 입력 항목입니다.' - }); - } - - const { getDb } = require('./dbPool'); - const db = await getDb(); - - // 중복 사용자명 확인 - const [existingUser] = await db.execute(` - SELECT user_id FROM users WHERE username = ? - `, [username]); - - if (existingUser.length > 0) { - return res.status(400).json({ - success: false, - error: '이미 존재하는 사용자명입니다.' - }); - } - - // 비밀번호 해시화 - const bcrypt = require('bcryptjs'); - const hashedPassword = await bcrypt.hash(password, 10); - - // undefined 값을 null로 변환 및 role에 따른 access_level 자동 설정 - const safeRole = role !== undefined ? role : null; - - // role에 따라 access_level 자동 설정 - let safeAccessLevel; - if (access_level !== undefined) { - safeAccessLevel = access_level; - } else if (safeRole === 'admin') { - safeAccessLevel = 'admin'; - } else if (safeRole === 'leader' || safeRole === 'group_leader') { - safeAccessLevel = 'group_leader'; - } else { - safeAccessLevel = 'worker'; - } - - // 사용자 생성 - const [result] = await db.execute(` - INSERT INTO users (username, name, password, role, access_level, is_active, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, 1, NOW(), NOW()) - `, [username, name, hashedPassword, safeRole, safeAccessLevel]); - - res.status(201).json({ - success: true, - message: `사용자 '${username}'가 성공적으로 생성되었습니다.`, - data: { - user_id: result.insertId, - username: username, - name: name, - role: safeRole, - access_level: safeAccessLevel, - is_active: true - } - }); - } catch (error) { - console.error('사용자 생성 오류:', error); - res.status(500).json({ - success: false, - error: '사용자 생성에 실패했습니다.' - }); - } -}); - -// 👤 사용자 상태 변경 API (관리자 전용) -app.put('/api/users/:id/status', async (req, res) => { - try { - // 관리자 권한 확인 - if (!req.user || !['admin', 'system'].includes(req.user.access_level)) { - return res.status(403).json({ - success: false, - error: '관리자 권한이 필요합니다.' - }); - } - - const userId = req.params.id; - const { is_active } = req.body; - - // 자기 자신의 상태는 변경할 수 없도록 방지 - if (parseInt(userId) === req.user.user_id) { - return res.status(400).json({ - success: false, - error: '자기 자신의 상태는 변경할 수 없습니다.' - }); - } - - const { getDb } = require('./dbPool'); - const db = await getDb(); - - // 사용자 존재 여부 확인 - const [existingUser] = await db.execute(` - SELECT user_id, username, is_active FROM users WHERE user_id = ? - `, [userId]); - - if (existingUser.length === 0) { - return res.status(404).json({ - success: false, - error: '사용자를 찾을 수 없습니다.' - }); - } - - // 상태 변경 - const newStatus = is_active !== undefined ? is_active : !existingUser[0].is_active; - const [result] = await db.execute(` - UPDATE users - SET is_active = ?, updated_at = NOW() - WHERE user_id = ? - `, [newStatus, userId]); - - const statusText = newStatus ? '활성화' : '비활성화'; - - res.json({ - success: true, - message: `사용자 '${existingUser[0].username}'가 성공적으로 ${statusText}되었습니다.`, - data: { - user_id: parseInt(userId), - username: existingUser[0].username, - is_active: newStatus - } - }); - } catch (error) { - console.error('사용자 상태 변경 오류:', error); - res.status(500).json({ - success: false, - error: '사용자 상태 변경에 실패했습니다.' - }); - } -}); +app.use('/api/users', userRoutes); // 📤 파일 업로드 app.use('/api', uploadBgRoutes); diff --git a/api.hyungi.net/middlewares/activityLogger.js b/api.hyungi.net/middlewares/activityLogger.js new file mode 100644 index 0000000..7008cbd --- /dev/null +++ b/api.hyungi.net/middlewares/activityLogger.js @@ -0,0 +1,71 @@ +/** + * 활동 로깅 미들웨어 + * + * HTTP 요청/응답 활동을 기록하는 미들웨어 + * + * @author TK-FB-Project + * @since 2025-12-11 + */ + +const logger = require('../utils/logger'); + +/** + * 활동 로거 미들웨어 + * 모든 HTTP 요청의 시작과 완료를 기록 + */ +const activityLogger = (req, res, next) => { + const start = Date.now(); + + // 응답 완료 시 로깅 + res.on('finish', () => { + const duration = Date.now() - start; + const username = req.user?.username || 'anonymous'; + + const logData = { + method: req.method, + url: req.originalUrl, + statusCode: res.statusCode, + duration: `${duration}ms`, + ip: req.ip, + user: username, + userAgent: req.get('User-Agent') + }; + + // 상태 코드에 따른 로그 레벨 분기 + if (res.statusCode >= 500) { + logger.error('HTTP Request - Server Error', logData); + } else if (res.statusCode >= 400) { + logger.warn('HTTP Request - Client Error', logData); + } else if (res.statusCode >= 300) { + logger.info('HTTP Request - Redirect', logData); + } else { + logger.info('HTTP Request - Success', logData); + } + }); + + next(); +}; + +/** + * 민감한 경로 필터 미들웨어 + * 로그에서 민감한 정보를 제외 + */ +const sensitivePathFilter = (req, res, next) => { + const sensitivePaths = [ + '/api/auth/login', + '/api/auth/refresh-token', + '/api/users/password' + ]; + + // 민감한 경로의 경우 바디 로깅 스킵 + if (sensitivePaths.some(path => req.originalUrl.includes(path))) { + req.skipBodyLog = true; + } + + next(); +}; + +module.exports = { + activityLogger, + sensitivePathFilter +}; diff --git a/api.hyungi.net/routes/userRoutes.js b/api.hyungi.net/routes/userRoutes.js index bd9dc0d..0c46952 100644 --- a/api.hyungi.net/routes/userRoutes.js +++ b/api.hyungi.net/routes/userRoutes.js @@ -1,26 +1,46 @@ -// routes/userRoutes.js - 사용자 관리 라우터 +/** + * 사용자 관리 라우터 + * + * 사용자 CRUD 및 상태 관리를 위한 API 라우트 정의 + * + * @author TK-FB-Project + * @since 2025-12-11 + */ const express = require('express'); const router = express.Router(); const userController = require('../controllers/userController'); const { verifyToken } = require('../middlewares/authMiddleware'); +const logger = require('../utils/logger'); -// 모든 라우트에 인증 미들웨어 적용 +/** + * 모든 라우트에 인증 미들웨어 적용 + */ router.use(verifyToken); -// 관리자 권한 확인 미들웨어 +/** + * 관리자 권한 확인 미들웨어 + */ const adminOnly = (req, res, next) => { if (req.user && (req.user.role === 'admin' || req.user.role === 'system')) { next(); } else { + logger.warn('관리자 권한 없는 접근 시도', { + userId: req.user?.user_id, + username: req.user?.username, + role: req.user?.role, + url: req.originalUrl + }); return res.status(403).json({ success: false, - message: '관리자 권한이 필요합니다.' + message: '관리자 권한이 필요합니다' }); } }; -// 모든 라우트에 관리자 권한 적용 +/** + * 모든 라우트에 관리자 권한 적용 + */ router.use(adminOnly); // 📋 사용자 목록 조회