import bcrypt from 'bcrypt' import { z } from 'zod' import { createError, readBody } from 'h3' import { updateMemberPasswordByEmail } from '../../../repositories/member-repository' import { verifyAndConsumeEmailOtp } from '../../../repositories/email-otp-repository' const bodySchema = z.object({ email: z.string().trim().email(), code: z.string().regex(/^\d{6}$/), newPassword: z.string().min(8).max(32) }) /** * 이메일 OTP로 비밀번호를 재설정한다. * @param {import('h3').H3Event} event - 요청 이벤트 * @returns {Promise<{ ok: boolean }>} */ export default defineEventHandler(async (event) => { const parsed = bodySchema.safeParse(await readBody(event)) if (!parsed.success) { throw createError({ statusCode: 400, message: '요청 형식이 올바르지 않습니다.' }) } const config = useRuntimeConfig() const pepper = String(config.emailOtpPepper || config.memberSessionSecret || '').trim() if (!pepper) { throw createError({ statusCode: 500, message: '서버 인증 설정이 올바르지 않습니다.' }) } const email = parsed.data.email.trim().toLowerCase() const verify = await verifyAndConsumeEmailOtp({ email, purpose: 'password_reset', code: parsed.data.code, pepper }) if (!verify.ok) { throw createError({ statusCode: 400, message: '인증번호가 올바르지 않거나 만료되었습니다.' }) } const nextHash = await bcrypt.hash(parsed.data.newPassword, 12) const updated = await updateMemberPasswordByEmail({ email, passwordHash: nextHash }) if (!updated) { throw createError({ statusCode: 400, message: '계정을 찾을 수 없습니다. 다시 인증번호를 요청해 주세요.' }) } return { ok: true } })