66 lines
1.9 KiB
JavaScript
66 lines
1.9 KiB
JavaScript
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'
|
|
import { getRuntimeEnvValue } from '../../../utils/runtime-env'
|
|
|
|
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 pepper = getRuntimeEnvValue('EMAIL_OTP_PEPPER', 'emailOtpPepper', getRuntimeEnvValue('MEMBER_SESSION_SECRET', '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 }
|
|
})
|