import { z } from 'zod' import { createError, getRequestIP, readBody } from 'h3' import bcrypt from 'bcrypt' import { safeCompare, setAdminSession } from '../../../../utils/admin-auth' import { getAdminUserByEmail, getMemberBootstrapState, touchUserActivity, upsertBootstrapOwner } from '../../../../repositories/member-repository' import { setMemberSession } from '../../../../utils/member-auth' const loginSchema = z.object({ email: z.string().email(), password: z.string().min(1) }) /** * 이메일에서 최초 관리자 닉네임을 만든다. * @param {string} email - 관리자 이메일 * @returns {string} 닉네임 */ const createBootstrapUsername = (email) => { const localPart = String(email).split('@')[0] || 'admin' return localPart.replace(/[^a-zA-Z0-9._-]/g, '').trim() || 'admin' } /** * 운영 환경 변수 기반 최초 관리자 계정을 생성한다. * @param {{ email: string, password: string }} credentials - 로그인 입력값 * @returns {Promise} 생성된 관리자 */ const createBootstrapAdminUser = async (credentials) => { const config = useRuntimeConfig() const adminEmail = String(config.adminEmail || '').trim().toLowerCase() const adminPassword = String(config.adminPassword || '') if (!adminEmail || !adminPassword || credentials.email.trim().toLowerCase() !== adminEmail || !safeCompare(credentials.password, adminPassword)) { return null } const bootstrap = await getMemberBootstrapState() if (!bootstrap.needsAdminSetup) { return null } const passwordHash = await bcrypt.hash(credentials.password, 12) return upsertBootstrapOwner({ username: createBootstrapUsername(adminEmail), email: adminEmail, passwordHash }) } /** * 관리자 로그인 API * @param {import('h3').H3Event} event - 요청 이벤트 * @returns {Promise<{ email: string }>} 관리자 세션 정보 */ export default defineEventHandler(async (event) => { const parsedBody = loginSchema.safeParse(await readBody(event)) if (!parsedBody.success) { throw createError({ statusCode: 400, message: '로그인 요청 형식이 올바르지 않습니다.' }) } const body = parsedBody.data const adminUser = await getAdminUserByEmail(body.email) || await createBootstrapAdminUser(body) const passwordMatched = adminUser ? await bcrypt.compare(body.password, adminUser.passwordHash) : false if (!adminUser || !passwordMatched) { throw createError({ statusCode: 401, message: '이메일 또는 비밀번호가 올바르지 않습니다.' }) } setAdminSession(event, { userId: adminUser.id, email: adminUser.email }) setMemberSession(event, { userId: adminUser.id, email: adminUser.email }) await touchUserActivity({ userId: adminUser.id, ip: String(getRequestIP(event) || '') }) return { userId: adminUser.id, email: adminUser.email, username: adminUser.username } })