// routes/authRoutes.js - 비밀번호 변경 및 보안 기능 포함 완전판 const express = require('express'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const mysql = require('mysql2/promise'); const { verifyToken } = require('../middlewares/authMiddleware'); const router = express.Router(); const authController = require('../controllers/authController'); // DB 연결 설정 const dbConfig = { host: process.env.DB_HOST || 'db_hyungi_net', user: process.env.DB_USER || 'hyungi', password: process.env.DB_PASSWORD, database: process.env.DB_NAME || 'hyungi' }; // 로그인 시도 추적 (메모리 기반 - 실제로는 Redis 권장) const loginAttempts = new Map(); // 로그인 시도 체크 미들웨어 const checkLoginAttempts = (req, res, next) => { const { username } = req.body; const key = `login_${username}`; const attempts = loginAttempts.get(key) || { count: 0, blockedUntil: null }; // 차단 확인 if (attempts.blockedUntil && attempts.blockedUntil > Date.now()) { const remainingTime = Math.ceil((attempts.blockedUntil - Date.now()) / 1000 / 60); return res.status(429).json({ error: `너무 많은 로그인 시도입니다. ${remainingTime}분 후에 다시 시도하세요.` }); } req.loginAttempts = attempts; next(); }; // 로그인 시도 기록 const recordLoginAttempt = (username, success = false) => { const key = `login_${username}`; const attempts = loginAttempts.get(key) || { count: 0, blockedUntil: null }; if (success) { loginAttempts.delete(key); } else { attempts.count += 1; // 5회 실패 시 15분 차단 if (attempts.count >= 5) { attempts.blockedUntil = Date.now() + (15 * 60 * 1000); console.log(`[로그인 차단] 사용자: ${username} - 15분간 차단`); } loginAttempts.set(key, attempts); } }; // 로그인 이력 기록 const recordLoginHistory = async (connection, userId, success, ipAddress, userAgent, failureReason = null) => { try { await connection.execute( `INSERT INTO login_logs (user_id, login_time, ip_address, user_agent, login_status, failure_reason) VALUES (?, NOW(), ?, ?, ?, ?)`, [userId, ipAddress || 'unknown', userAgent || 'unknown', success ? 'success' : 'failed', failureReason] ); } catch (error) { console.error('로그인 이력 기록 실패:', error); // 로그 테이블이 없어도 계속 진행 } }; /** * 로그인 - DB 연동 (보안 강화) */ router.post('/login', authController.login); /** * 토큰 갱신 */ router.post('/refresh-token', async (req, res) => { try { const { refreshToken } = req.body; if (!refreshToken) { return res.status(401).json({ error: '리프레시 토큰이 필요합니다.' }); } // 리프레시 토큰 검증 const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET || process.env.JWT_SECRET); if (decoded.type !== 'refresh') { return res.status(401).json({ error: '유효하지 않은 토큰입니다.' }); } const connection = await mysql.createConnection(dbConfig); // 사용자 정보 조회 const [users] = await connection.execute( 'SELECT * FROM users WHERE user_id = ? AND is_active = TRUE', [decoded.user_id] ); await connection.end(); if (users.length === 0) { return res.status(404).json({ error: '사용자를 찾을 수 없습니다.' }); } const user = users[0]; // 새 토큰 발급 const newToken = jwt.sign( { user_id: user.user_id, username: user.username, access_level: user.access_level, worker_id: user.worker_id, name: user.name || user.username }, process.env.JWT_SECRET || 'your-secret-key', { expiresIn: process.env.JWT_EXPIRES_IN || '24h' } ); const newRefreshToken = jwt.sign( { user_id: user.user_id, type: 'refresh' }, process.env.JWT_REFRESH_SECRET || process.env.JWT_SECRET, { expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d' } ); res.json({ success: true, token: newToken, refreshToken: newRefreshToken }); } catch (error) { if (error.name === 'TokenExpiredError') { return res.status(401).json({ error: '리프레시 토큰이 만료되었습니다.' }); } console.error('Token refresh error:', error); res.status(500).json({ error: '서버 오류가 발생했습니다.' }); } }); /** * 본인 비밀번호 변경 */ router.post('/change-password', verifyToken, async (req, res) => { let connection; try { const { currentPassword, newPassword } = req.body; const userId = req.user.user_id; // 입력값 검증 if (!currentPassword || !newPassword) { return res.status(400).json({ success: false, error: '현재 비밀번호와 새 비밀번호를 입력해주세요.' }); } // 비밀번호 강도 검증 if (newPassword.length < 6) { return res.status(400).json({ success: false, error: '비밀번호는 최소 6자 이상이어야 합니다.' }); } connection = await mysql.createConnection(dbConfig); // 현재 사용자의 비밀번호 조회 const [users] = await connection.execute( 'SELECT password FROM users WHERE user_id = ?', [userId] ); if (users.length === 0) { return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다.' }); } // 현재 비밀번호 확인 const isValidPassword = await bcrypt.compare(currentPassword, users[0].password); if (!isValidPassword) { console.log(`[비밀번호 변경 실패] 현재 비밀번호 불일치 - 사용자: ${req.user.username}`); return res.status(401).json({ success: false, error: '현재 비밀번호가 올바르지 않습니다.' }); } // 새 비밀번호와 현재 비밀번호 동일 여부 확인 const isSamePassword = await bcrypt.compare(newPassword, users[0].password); if (isSamePassword) { return res.status(400).json({ success: false, error: '새 비밀번호는 현재 비밀번호와 달라야 합니다.' }); } // 새 비밀번호 해시화 const hashedPassword = await bcrypt.hash(newPassword, 10); // 비밀번호 업데이트 await connection.execute( 'UPDATE Users SET password = ?, password_changed_at = NOW(), updated_at = NOW() WHERE user_id = ?', [hashedPassword, userId] ); // 비밀번호 변경 로그 기록 try { await connection.execute( 'INSERT INTO password_change_logs (user_id, changed_by_user_id, changed_at, change_type) VALUES (?, ?, NOW(), ?)', [userId, userId, 'self'] ); } catch (logError) { console.log('비밀번호 변경 로그 기록 실패 (테이블 없음)'); } console.log(`[비밀번호 변경 성공] 사용자: ${req.user.username}`); res.json({ success: true, message: '비밀번호가 성공적으로 변경되었습니다.' }); } catch (error) { console.error('Change password error:', error); res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' }); } finally { if (connection) { await connection.end(); } } }); /** * 시스템 관리자의 타인 비밀번호 변경 */ router.post('/admin/change-password', verifyToken, async (req, res) => { let connection; try { const { userId, newPassword } = req.body; const adminId = req.user.user_id; // 권한 확인 (system 레벨만 허용) if (req.user.access_level !== 'system') { return res.status(403).json({ success: false, error: '시스템 관리자만 사용할 수 있는 기능입니다.' }); } // 입력값 검증 if (!userId || !newPassword) { return res.status(400).json({ success: false, error: '사용자 ID와 새 비밀번호를 입력해주세요.' }); } // 비밀번호 강도 검증 if (newPassword.length < 6) { return res.status(400).json({ success: false, error: '비밀번호는 최소 6자 이상이어야 합니다.' }); } connection = await mysql.createConnection(dbConfig); // 대상 사용자 확인 const [users] = await connection.execute( 'SELECT username, name FROM users WHERE user_id = ?', [userId] ); if (users.length === 0) { return res.status(404).json({ success: false, error: '대상 사용자를 찾을 수 없습니다.' }); } const targetUser = users[0]; // 자기 자신의 비밀번호는 이 API로 변경 불가 if (parseInt(userId) === adminId) { return res.status(400).json({ success: false, error: '본인의 비밀번호는 일반 비밀번호 변경 기능을 사용하세요.' }); } // 새 비밀번호 해시화 const hashedPassword = await bcrypt.hash(newPassword, 10); // 비밀번호 업데이트 await connection.execute( 'UPDATE Users SET password = ?, password_changed_at = NOW(), updated_at = NOW(), failed_login_attempts = 0, locked_until = NULL WHERE user_id = ?', [hashedPassword, userId] ); // 비밀번호 변경 로그 기록 try { await connection.execute( 'INSERT INTO password_change_logs (user_id, changed_by_user_id, changed_at, change_type) VALUES (?, ?, NOW(), ?)', [userId, adminId, 'admin'] ); } catch (logError) { console.log('비밀번호 변경 로그 기록 실패 (테이블 없음)'); } console.log(`[관리자 비밀번호 변경] 대상: ${targetUser.username} - 관리자: ${req.user.username}`); res.json({ success: true, message: `${targetUser.name}(${targetUser.username})님의 비밀번호가 변경되었습니다.` }); } catch (error) { console.error('Admin change password error:', error); res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' }); } finally { if (connection) { await connection.end(); } } }); /** * 비밀번호 강도 체크 */ router.post('/check-password-strength', (req, res) => { const { password } = req.body; if (!password) { return res.json({ strength: 0, message: '비밀번호를 입력하세요.' }); } let strength = 0; const feedback = []; // 길이 체크 if (password.length >= 8) strength += 1; if (password.length >= 12) strength += 1; else feedback.push('12자 이상 사용을 권장합니다.'); // 대문자 포함 if (/[A-Z]/.test(password)) strength += 1; else feedback.push('대문자를 포함하세요.'); // 소문자 포함 if (/[a-z]/.test(password)) strength += 1; else feedback.push('소문자를 포함하세요.'); // 숫자 포함 if (/\d/.test(password)) strength += 1; else feedback.push('숫자를 포함하세요.'); // 특수문자 포함 if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) strength += 1; else feedback.push('특수문자를 포함하세요.'); const strengthLevels = ['매우 약함', '약함', '보통', '강함', '매우 강함']; const level = Math.min(Math.floor((strength / 6) * 5), 4); res.json({ strength: level, strengthText: strengthLevels[level], score: strength, maxScore: 6, feedback: feedback }); }); /** * 현재 사용자 정보 조회 */ router.get('/me', verifyToken, async (req, res) => { let connection; try { const userId = req.user.user_id; connection = await mysql.createConnection(dbConfig); const [rows] = await connection.execute( 'SELECT user_id, username, name, email, access_level, worker_id, last_login_at, created_at FROM users WHERE user_id = ?', [userId] ); if (rows.length === 0) { return res.status(404).json({ error: '사용자를 찾을 수 없습니다.' }); } const user = rows[0]; res.json({ user_id: user.user_id, username: user.username, name: user.name || user.username, email: user.email, access_level: user.access_level, worker_id: user.worker_id, last_login_at: user.last_login_at, created_at: user.created_at }); } catch (error) { console.error('Get current user error:', error); res.status(500).json({ error: '서버 오류가 발생했습니다.' }); } finally { if (connection) { await connection.end(); } } }); /** * 사용자 등록 */ router.post('/register', verifyToken, async (req, res) => { let connection; try { const { username, password, name, email, access_level, worker_id } = req.body; // 권한 확인 (admin 이상만 사용자 등록 가능) if (!['admin', 'system'].includes(req.user.access_level)) { return res.status(403).json({ success: false, error: '사용자 등록 권한이 없습니다.' }); } if (!username || !password || !name || !access_level) { return res.status(400).json({ success: false, error: '필수 항목을 모두 입력해주세요.' }); } // 비밀번호 강도 검증 if (password.length < 6) { return res.status(400).json({ success: false, error: '비밀번호는 최소 6자 이상이어야 합니다.' }); } connection = await mysql.createConnection(dbConfig); // 사용자명 중복 체크 const [existing] = await connection.execute( 'SELECT user_id FROM users WHERE username = ?', [username] ); if (existing.length > 0) { return res.status(409).json({ success: false, error: '이미 존재하는 사용자명입니다.' }); } // 이메일 중복 체크 (이메일이 제공된 경우) if (email) { const [existingEmail] = await connection.execute( 'SELECT user_id FROM users WHERE email = ?', [email] ); if (existingEmail.length > 0) { return res.status(409).json({ success: false, error: '이미 사용 중인 이메일입니다.' }); } } // 비밀번호 해시 const hashedPassword = await bcrypt.hash(password, 10); // 사용자 추가 const [result] = await connection.execute( `INSERT INTO Users (username, password, name, email, access_level, worker_id, is_active, created_at, password_changed_at) VALUES (?, ?, ?, ?, ?, ?, TRUE, NOW(), NOW())`, [username, hashedPassword, name, email || null, access_level, worker_id || null] ); // 비밀번호 변경 로그 기록 (초기 설정) try { await connection.execute( 'INSERT INTO password_change_logs (user_id, changed_by_user_id, changed_at, change_type) VALUES (?, ?, NOW(), ?)', [result.insertId, req.user.user_id, 'initial'] ); } catch (logError) { console.log('비밀번호 변경 로그 기록 실패 (테이블 없음)'); } console.log(`[사용자 등록] 새 사용자: ${username} (${access_level}) - 등록자: ${req.user.username}`); res.json({ success: true, message: '사용자가 성공적으로 등록되었습니다.', user: { user_id: result.insertId, username, name, email: email || null, access_level, worker_id: worker_id || null } }); } catch (error) { console.error('Register error:', error); res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' }); } finally { if (connection) { await connection.end(); } } }); /** * 사용자 목록 조회 */ router.get('/users', verifyToken, async (req, res) => { let connection; try { connection = await mysql.createConnection(dbConfig); // 기본 쿼리 let query = ` SELECT user_id, username, name, email, access_level, worker_id, is_active, last_login_at, created_at FROM users WHERE 1=1 `; const params = []; // 필터링 옵션 if (req.query.active !== undefined) { query += ' AND is_active = ?'; params.push(req.query.active === 'true'); } if (req.query.access_level) { query += ' AND access_level = ?'; params.push(req.query.access_level); } query += ' ORDER BY created_at DESC'; const [rows] = await connection.execute(query, params); const userList = rows.map(user => ({ user_id: user.user_id, username: user.username, name: user.name || user.username, email: user.email, access_level: user.access_level, worker_id: user.worker_id, is_active: user.is_active, last_login_at: user.last_login_at, created_at: user.created_at })); res.json(userList); } catch (error) { console.error('Get users error:', error); res.status(500).json({ error: '서버 오류가 발생했습니다.' }); } finally { if (connection) { await connection.end(); } } }); /** * 사용자 정보 수정 */ router.put('/users/:id', verifyToken, async (req, res) => { let connection; try { const userId = parseInt(req.params.id); const { name, email, access_level, worker_id, password, is_active } = req.body; // 권한 확인 if (!['admin', 'system'].includes(req.user.access_level)) { // 일반 사용자는 자신의 정보만 수정 가능 (이름, 이메일만) if (userId !== req.user.user_id) { return res.status(403).json({ success: false, error: '다른 사용자의 정보를 수정할 권한이 없습니다.' }); } if (access_level || worker_id || is_active !== undefined) { return res.status(403).json({ success: false, error: '권한, 작업자 ID, 활성화 상태는 관리자만 수정할 수 있습니다.' }); } } connection = await mysql.createConnection(dbConfig); // 사용자 존재 확인 const [existing] = await connection.execute( 'SELECT user_id, username FROM users WHERE user_id = ?', [userId] ); if (existing.length === 0) { return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다.' }); } // 업데이트할 필드들 준비 let updateFields = []; let updateValues = []; if (name !== undefined) { updateFields.push('name = ?'); updateValues.push(name); } if (email !== undefined) { // 이메일 중복 체크 if (email) { const [emailCheck] = await connection.execute( 'SELECT user_id FROM users WHERE email = ? AND user_id != ?', [email, userId] ); if (emailCheck.length > 0) { return res.status(409).json({ success: false, error: '이미 사용 중인 이메일입니다.' }); } } updateFields.push('email = ?'); updateValues.push(email || null); } if (access_level !== undefined && ['admin', 'system'].includes(req.user.access_level)) { updateFields.push('access_level = ?'); updateValues.push(access_level); } if (worker_id !== undefined && ['admin', 'system'].includes(req.user.access_level)) { updateFields.push('worker_id = ?'); updateValues.push(worker_id || null); } if (is_active !== undefined && ['admin', 'system'].includes(req.user.access_level)) { updateFields.push('is_active = ?'); updateValues.push(is_active); } if (password) { if (password.length < 6) { return res.status(400).json({ success: false, error: '비밀번호는 최소 6자 이상이어야 합니다.' }); } updateFields.push('password = ?'); updateValues.push(await bcrypt.hash(password, 10)); updateFields.push('password_changed_at = NOW()'); // 비밀번호 변경 로그 try { await connection.execute( 'INSERT INTO password_change_logs (user_id, changed_by_user_id, changed_at, change_type) VALUES (?, ?, NOW(), ?)', [userId, req.user.user_id, userId === req.user.user_id ? 'self' : 'admin'] ); } catch (logError) { console.log('비밀번호 변경 로그 기록 실패'); } } if (updateFields.length === 0) { return res.status(400).json({ success: false, error: '수정할 내용이 없습니다.' }); } updateFields.push('updated_at = NOW()'); updateValues.push(userId); // 업데이트 실행 await connection.execute( `UPDATE Users SET ${updateFields.join(', ')} WHERE user_id = ?`, updateValues ); // 업데이트된 사용자 정보 조회 const [updated] = await connection.execute( 'SELECT user_id, username, name, email, access_level, worker_id, is_active FROM users WHERE user_id = ?', [userId] ); console.log(`[사용자 수정] 대상: ${existing[0].username} - 수정자: ${req.user.username}`); res.json({ success: true, message: '사용자 정보가 성공적으로 수정되었습니다.', user: updated[0] }); } catch (error) { console.error('Update user error:', error); res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' }); } finally { if (connection) { await connection.end(); } } }); /** * 사용자 삭제 */ router.delete('/users/:id', verifyToken, async (req, res) => { let connection; try { const userId = parseInt(req.params.id); // 권한 확인 (system만 삭제 가능) if (req.user.access_level !== 'system') { return res.status(403).json({ success: false, error: '사용자 삭제 권한이 없습니다.' }); } // 자기 자신 삭제 방지 if (userId === req.user.user_id) { return res.status(400).json({ success: false, error: '자기 자신은 삭제할 수 없습니다.' }); } connection = await mysql.createConnection(dbConfig); // 사용자 존재 확인 const [existing] = await connection.execute( 'SELECT username FROM users WHERE user_id = ?', [userId] ); if (existing.length === 0) { return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다.' }); } // 소프트 삭제 (실제로는 비활성화) await connection.execute( 'UPDATE Users SET is_active = FALSE, updated_at = NOW() WHERE user_id = ?', [userId] ); // 또는 하드 삭제 (실제로 삭제) // await connection.execute('DELETE FROM Users WHERE user_id = ?', [userId]); console.log(`[사용자 삭제] 대상: ${existing[0].username} - 삭제자: ${req.user.username}`); res.json({ success: true, message: '사용자가 성공적으로 삭제되었습니다.' }); } catch (error) { console.error('Delete user error:', error); res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' }); } finally { if (connection) { await connection.end(); } } }); /** * 로그아웃 */ router.post('/logout', verifyToken, async (req, res) => { console.log(`[로그아웃] 사용자: ${req.user.username}`); // 로그아웃 시간 기록 (선택사항) let connection; try { connection = await mysql.createConnection(dbConfig); await connection.execute( 'UPDATE login_logs SET logout_time = NOW() WHERE user_id = ? AND logout_time IS NULL ORDER BY login_time DESC LIMIT 1', [req.user.user_id] ); } catch (error) { console.error('로그아웃 기록 실패:', error); } finally { if (connection) { await connection.end(); } } res.json({ success: true, message: '성공적으로 로그아웃되었습니다.' }); }); /** * 로그인 이력 조회 */ router.get('/login-history', verifyToken, async (req, res) => { let connection; try { const { limit = 50, offset = 0 } = req.query; const userId = req.user.user_id; connection = await mysql.createConnection(dbConfig); // 본인의 로그인 이력만 조회 (관리자는 전체 조회 가능) let query = ` SELECT log_id, user_id, login_time, logout_time, ip_address, user_agent, login_status FROM login_logs `; const params = []; if (!['admin', 'system'].includes(req.user.access_level)) { query += ' WHERE user_id = ?'; params.push(userId); } else if (req.query.user_id) { query += ' WHERE user_id = ?'; params.push(req.query.user_id); } query += ' ORDER BY login_time DESC LIMIT ? OFFSET ?'; params.push(parseInt(limit), parseInt(offset)); const [rows] = await connection.execute(query, params); res.json({ success: true, data: rows, pagination: { limit: parseInt(limit), offset: parseInt(offset) } }); } catch (error) { console.error('Get login history error:', error); res.status(500).json({ error: '서버 오류가 발생했습니다.' }); } finally { if (connection) { await connection.end(); } } }); module.exports = router;