import crypto from 'crypto' import { query, queryOne, execute, insertReturning } from './db' // 세션 설정 const SESSION_TIMEOUT_MINUTES = 10 // 10분 타임아웃 const SESSION_COOKIE_NAME = 'session_token' const SESSION_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 // 쿠키는 7일 (세션 만료와 별개) interface Session { sessionId: string employeeId: number loginHistoryId: number | null createdAt: Date lastAccessAt: Date expiresAt: Date loginIp: string | null userAgent: string | null } /** * 랜덤 세션 토큰 생성 (Spring Session과 유사) */ function generateSessionToken(): string { return crypto.randomBytes(32).toString('hex') // 64자 hex 문자열 } /** * 세션 생성 */ export async function createSession( employeeId: number, loginHistoryId: number, loginIp: string | null, userAgent: string | null ): Promise { const sessionId = generateSessionToken() const expiresAt = new Date(Date.now() + SESSION_TIMEOUT_MINUTES * 60 * 1000) await execute(` INSERT INTO wr_session (session_id, employee_id, login_history_id, expires_at, login_ip, user_agent) VALUES ($1, $2, $3, $4, $5, $6) `, [sessionId, employeeId, loginHistoryId, expiresAt, loginIp, userAgent]) return sessionId } /** * 세션 조회 (유효한 세션만) */ export async function getSession(sessionId: string): Promise { if (!sessionId) return null const row = await queryOne(` SELECT session_id, employee_id, login_history_id, created_at, last_access_at, expires_at, login_ip, user_agent FROM wr_session WHERE session_id = $1 AND expires_at > NOW() `, [sessionId]) if (!row) return null return { sessionId: row.session_id, employeeId: row.employee_id, loginHistoryId: row.login_history_id, createdAt: row.created_at, lastAccessAt: row.last_access_at, expiresAt: row.expires_at, loginIp: row.login_ip, userAgent: row.user_agent } } /** * 세션 갱신 (Sliding Expiration) * - 마지막 접근 시간 업데이트 * - 만료 시간 연장 */ export async function refreshSession(sessionId: string): Promise { const newExpiresAt = new Date(Date.now() + SESSION_TIMEOUT_MINUTES * 60 * 1000) const result = await execute(` UPDATE wr_session SET last_access_at = NOW(), expires_at = $1 WHERE session_id = $2 AND expires_at > NOW() `, [newExpiresAt, sessionId]) return result.rowCount > 0 } /** * 세션 삭제 (로그아웃) */ export async function deleteSession(sessionId: string): Promise { const result = await execute(` DELETE FROM wr_session WHERE session_id = $1 `, [sessionId]) return result.rowCount > 0 } /** * 사용자의 모든 세션 삭제 (모든 기기에서 로그아웃) */ export async function deleteAllUserSessions(employeeId: number): Promise { const result = await execute(` DELETE FROM wr_session WHERE employee_id = $1 `, [employeeId]) return result.rowCount } /** * 만료된 세션 정리 (배치용) */ export async function cleanupExpiredSessions(): Promise { const result = await execute(` DELETE FROM wr_session WHERE expires_at < NOW() `) return result.rowCount } /** * 세션 쿠키 설정 */ export function setSessionCookie(event: any, sessionId: string) { setCookie(event, SESSION_COOKIE_NAME, sessionId, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: SESSION_COOKIE_MAX_AGE, path: '/' }) } /** * 세션 쿠키 삭제 */ export function deleteSessionCookie(event: any) { deleteCookie(event, SESSION_COOKIE_NAME) } /** * 세션 쿠키에서 세션 ID 가져오기 */ export function getSessionIdFromCookie(event: any): string | null { return getCookie(event, SESSION_COOKIE_NAME) || null } // 설정값 export export { SESSION_TIMEOUT_MINUTES, SESSION_COOKIE_NAME } /** * 인증된 사용자 ID 가져오기 (다른 API에서 사용) * - 세션이 없거나 만료되면 null 반환 * - 세션이 유효하면 자동 갱신 * - [호환성] 기존 user_id 쿠키도 지원 (마이그레이션 기간) */ export async function getAuthenticatedUserId(event: any): Promise { // 1. 새로운 세션 토큰 확인 const sessionId = getSessionIdFromCookie(event) if (sessionId) { const session = await getSession(sessionId) if (session) { await refreshSession(sessionId) return session.employeeId } // 세션 만료 → 쿠키 삭제 deleteSessionCookie(event) } // 2. [호환성] 기존 user_id 쿠키 확인 (마이그레이션 기간) const legacyUserId = getCookie(event, 'user_id') if (legacyUserId) { return parseInt(legacyUserId) } return null } /** * 인증 필수 API용 - 미인증시 에러 throw */ export async function requireAuth(event: any): Promise { const userId = await getAuthenticatedUserId(event) if (!userId) { throw createError({ statusCode: 401, message: '로그인이 필요합니다.' }) } return userId }