refactor: 로그인 API의 DB 스키마 및 구조 개선

- 새로운 DB 스키마(v2) 추가 (테이블명 snake_case, FK 적용)
 - 룰.md에 API 성능 관리 규칙 추가
 - 로그인 관련 로직을 새로운 스키마에 맞게 수정
 - Service와 Model의 역할 분리를 명확하게 리팩토링
This commit is contained in:
2025-07-28 11:11:25 +09:00
parent 30fccd8eb5
commit 5539b09fd8
6 changed files with 495 additions and 173 deletions

View File

@@ -5,6 +5,7 @@ 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 = {
@@ -72,133 +73,7 @@ const recordLoginHistory = async (connection, userId, success, ipAddress, userAg
/**
* 로그인 - DB 연동 (보안 강화)
*/
router.post('/login', checkLoginAttempts, async (req, res) => {
let connection;
try {
const { username, password } = req.body;
const ipAddress = req.ip || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'];
console.log(`[로그인 시도] 사용자: ${username}, IP: ${ipAddress}`);
if (!username || !password) {
return res.status(400).json({ error: '사용자명과 비밀번호를 입력해주세요.' });
}
// DB 연결
connection = await mysql.createConnection(dbConfig);
// 사용자 조회
const [rows] = await connection.execute(
'SELECT * FROM Users WHERE username = ?',
[username]
);
if (rows.length === 0) {
console.log(`[로그인 실패] 사용자를 찾을 수 없음: ${username}`);
recordLoginAttempt(username, false);
// 보안상 구체적인 오류 메시지는 피함
return res.status(401).json({ error: '아이디 또는 비밀번호가 올바르지 않습니다.' });
}
const user = rows[0];
// 계정 활성화 상태 확인
if (user.is_active === false) {
await recordLoginHistory(connection, user.user_id, false, ipAddress, userAgent, 'account_disabled');
return res.status(403).json({ error: '비활성화된 계정입니다. 관리자에게 문의하세요.' });
}
// 계정 잠금 확인
if (user.locked_until && new Date(user.locked_until) > new Date()) {
const remainingTime = Math.ceil((new Date(user.locked_until) - new Date()) / 1000 / 60);
return res.status(429).json({ error: `계정이 잠겨있습니다. ${remainingTime}분 후에 다시 시도하세요.` });
}
// 비밀번호 확인
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
console.log(`[로그인 실패] 비밀번호 불일치: ${username}`);
recordLoginAttempt(username, false);
// 실패 횟수 업데이트
await connection.execute(
'UPDATE Users SET failed_login_attempts = failed_login_attempts + 1 WHERE user_id = ?',
[user.user_id]
);
// 5회 실패 시 계정 잠금
if (user.failed_login_attempts >= 4) {
await connection.execute(
'UPDATE Users SET locked_until = DATE_ADD(NOW(), INTERVAL 15 MINUTE) WHERE user_id = ?',
[user.user_id]
);
}
await recordLoginHistory(connection, user.user_id, false, ipAddress, userAgent, 'invalid_password');
return res.status(401).json({ error: '아이디 또는 비밀번호가 올바르지 않습니다.' });
}
// 로그인 성공
recordLoginAttempt(username, true);
// 마지막 로그인 시간 업데이트 및 실패 횟수 초기화
await connection.execute(
'UPDATE Users SET last_login_at = NOW(), failed_login_attempts = 0, locked_until = NULL WHERE user_id = ?',
[user.user_id]
);
// JWT 토큰 생성
const token = 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 refreshToken = jwt.sign(
{
user_id: user.user_id,
type: 'refresh'
},
process.env.JWT_REFRESH_SECRET || process.env.JWT_SECRET || 'your-refresh-secret',
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d' }
);
// 로그인 이력 기록
await recordLoginHistory(connection, user.user_id, true, ipAddress, userAgent);
console.log(`[로그인 성공] 사용자: ${user.username} (${user.access_level})`);
res.json({
success: true,
token,
refreshToken,
user: {
user_id: user.user_id,
username: user.username,
name: user.name || user.username,
access_level: user.access_level,
worker_id: user.worker_id
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
} finally {
if (connection) {
await connection.end();
}
}
});
router.post('/login', authController.login);
/**
* 토큰 갱신