fromis_9/backend/src/routes/auth.js
caadiq e852f215a3 feat: 보안 강화 및 인증 개선 (Phase 2)
- 로그인 Rate Limit 추가 (5회/분, 마지막 시도 기준 리셋)
- Multipart JSON 파싱 에러 처리 추가
- 로그아웃 시 무한 리다이렉트 버그 수정
- 인증 라우트 가드(RequireAuth) 추가로 비로그인 접근 차단
- Zustand hydration 대기로 페이지 깜빡임 해결
- admin/public 라우트 조건부 렌더링으로 경로 매칭 경고 해결

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 20:47:05 +09:00

133 lines
3.5 KiB
JavaScript

import bcrypt from 'bcrypt';
import { badRequest, unauthorized, serverError } from '../utils/error.js';
/**
* 인증 라우트
* /api/auth/*
*/
export default async function authRoutes(fastify, opts) {
/**
* POST /api/auth/login
* 관리자 로그인
*/
fastify.post('/login', {
config: {
rateLimit: {
max: 5,
timeWindow: '1 minute',
continueExceeding: true, // 차단 중 시도하면 타이머 리셋 (마지막 시도 기준 1분)
keyGenerator: (request) => request.ip,
errorResponseBuilder: () => ({
statusCode: 429,
error: 'Too Many Requests',
message: '로그인 시도가 너무 많습니다. 잠시 후 다시 시도해주세요.',
}),
},
},
schema: {
tags: ['auth'],
summary: '관리자 로그인',
body: {
type: 'object',
required: ['username', 'password'],
properties: {
username: { type: 'string', description: '관리자 아이디' },
password: { type: 'string', description: '비밀번호' },
},
},
response: {
200: {
type: 'object',
properties: {
message: { type: 'string' },
token: { type: 'string' },
user: {
type: 'object',
properties: {
id: { type: 'integer' },
username: { type: 'string' },
},
},
},
},
429: {
type: 'object',
properties: {
statusCode: { type: 'integer' },
error: { type: 'string' },
message: { type: 'string' },
},
},
},
},
}, async (request, reply) => {
const { username, password } = request.body || {};
if (!username || !password) {
return badRequest(reply, '아이디와 비밀번호를 입력해주세요.');
}
try {
const [users] = await fastify.db.query(
'SELECT * FROM admin_users WHERE username = ?',
[username]
);
if (users.length === 0) {
return unauthorized(reply, '아이디 또는 비밀번호가 올바르지 않습니다.');
}
const user = users[0];
const isValidPassword = await bcrypt.compare(password, user.password_hash);
if (!isValidPassword) {
return unauthorized(reply, '아이디 또는 비밀번호가 올바르지 않습니다.');
}
// JWT 토큰 생성
const token = fastify.jwt.sign({
id: user.id,
username: user.username,
});
return {
message: '로그인 성공',
token,
user: { id: user.id, username: user.username },
};
} catch (err) {
fastify.log.error(err);
return serverError(reply, '로그인 처리 중 오류가 발생했습니다.');
}
});
/**
* GET /api/auth/verify
* 토큰 검증
*/
fastify.get('/verify', {
schema: {
tags: ['auth'],
summary: '토큰 검증',
security: [{ bearerAuth: [] }],
response: {
200: {
type: 'object',
properties: {
valid: { type: 'boolean' },
user: {
type: 'object',
properties: {
id: { type: 'integer' },
username: { type: 'string' },
},
},
},
},
},
},
preHandler: [fastify.authenticate],
}, async (request, reply) => {
return { valid: true, user: request.user };
});
}