import type { Request } from 'express'; import { ApiError } from '../http/errors.js'; import { query } from '../db/pool.js'; export interface UserRow { id: string; username: string; password_hash: string; full_name: string; email: string | null; department: string | null; team_id?: string | null; team_code?: string | null; team_name?: string | null; must_change_password: boolean; enabled: boolean; locked: boolean; roles: string[]; } export type QueryFn = typeof query; export async function findUserByUsername(dbQuery: QueryFn, username: string): Promise { const result = await dbQuery( ` SELECT u.id, u.username, u.password_hash, u.full_name, u.email, u.department, u.team_id, t.code AS team_code, t.name AS team_name, u.must_change_password, u.enabled, u.locked, COALESCE(array_agg(ur.role) FILTER (WHERE ur.role IS NOT NULL), '{}') AS roles FROM users u LEFT JOIN teams t ON t.id = u.team_id LEFT JOIN user_roles ur ON ur.user_id = u.id WHERE u.username = $1 GROUP BY u.id, t.id `, [username], ); return result.rows[0]; } export async function findUserById(dbQuery: QueryFn, id: number): Promise { const result = await dbQuery( ` SELECT u.id, u.username, u.password_hash, u.full_name, u.email, u.department, u.team_id, t.code AS team_code, t.name AS team_name, u.must_change_password, u.enabled, u.locked, COALESCE(array_agg(ur.role) FILTER (WHERE ur.role IS NOT NULL), '{}') AS roles FROM users u LEFT JOIN teams t ON t.id = u.team_id LEFT JOIN user_roles ur ON ur.user_id = u.id WHERE u.id = $1 GROUP BY u.id, t.id `, [id], ); return result.rows[0]; } export async function requireCurrentUser(dbQuery: QueryFn, req: Request): Promise { const userId = req.session.userId; if (!userId) { throw new ApiError(401, '로그인이 필요합니다.'); } const user = await findUserById(dbQuery, userId); if (!user || !user.enabled || user.locked) { req.session.userId = undefined; throw new ApiError(401, '로그인이 필요합니다.'); } return user; } export async function requireAdmin(dbQuery: QueryFn, req: Request): Promise { const user = await requireCurrentUser(dbQuery, req); if (!user.roles.includes('ADMIN')) { throw new ApiError(403, '접근 권한이 없습니다.'); } return user; } export function parseRole(value: string): string { const role = value.trim().toUpperCase() === 'USER' ? 'HOST' : value.trim().toUpperCase(); if (!['ADMIN', 'SECURITY', 'HOST'].includes(role)) { throw new ApiError(400, `알 수 없는 권한입니다: ${value}`); } return role; } export function parseRoles(values: unknown): string[] { if (!Array.isArray(values)) { throw new ApiError(400, '권한은 배열로 입력해야 합니다.'); } const roles = [...new Set(values.map((value) => parseRole(String(value))))]; if (roles.length === 0) { throw new ApiError(400, '권한은 최소 1개 이상 필요합니다.'); } return roles; }