헤더 검색 중앙 정렬·Resend 이메일 OTP·비밀번호 찾기 (v0.0.99)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,16 +1,33 @@
|
||||
import bcrypt from 'bcrypt'
|
||||
import { z } from 'zod'
|
||||
import { createError, getRequestIP, readBody } from 'h3'
|
||||
import { createUser, getUserByEmail, isUsernameTaken, touchUserActivity } from '../../repositories/member-repository'
|
||||
import { createUser, getUserByEmail, getMemberBootstrapState, isUsernameTaken, touchUserActivity } from '../../repositories/member-repository'
|
||||
import { verifyAndConsumeEmailOtp } from '../../repositories/email-otp-repository'
|
||||
import { setMemberSession } from '../../utils/member-auth'
|
||||
import { setAdminSession } from '../../utils/admin-auth'
|
||||
import { isResendConfigured } from '../../utils/resend-mail'
|
||||
|
||||
const signupSchema = z.object({
|
||||
username: z.string().trim().min(1),
|
||||
email: z.string().trim().email(),
|
||||
password: z.string().min(8).max(32)
|
||||
password: z.string().min(8).max(32),
|
||||
emailOtp: z.string().regex(/^\d{6}$/).optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* 이메일 OTP가 회원가입에 필요한지
|
||||
* @param {import('nuxt/schema').RuntimeConfig} config - 런타임 설정
|
||||
* @param {{ needsAdminSetup: boolean }} bootstrap - 부트스트랩 상태
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isSignupOtpRequired = (config, bootstrap) => {
|
||||
if (bootstrap.needsAdminSetup) {
|
||||
return false
|
||||
}
|
||||
const hasPepper = Boolean(String(config.emailOtpPepper || config.memberSessionSecret || '').trim())
|
||||
return isResendConfigured(config) && hasPepper
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원 가입 API
|
||||
* @param {import('h3').H3Event} event - 요청 이벤트
|
||||
@@ -27,6 +44,11 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
const body = parsedBody.data
|
||||
const config = useRuntimeConfig()
|
||||
const bootstrap = await getMemberBootstrapState()
|
||||
const otpRequired = isSignupOtpRequired(config, bootstrap)
|
||||
const emailNorm = body.email.trim().toLowerCase()
|
||||
|
||||
const usernameTaken = await isUsernameTaken({
|
||||
username: body.username
|
||||
})
|
||||
@@ -38,7 +60,7 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
const existingUser = await getUserByEmail(body.email)
|
||||
const existingUser = await getUserByEmail(emailNorm)
|
||||
|
||||
if (existingUser) {
|
||||
throw createError({
|
||||
@@ -47,10 +69,33 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (otpRequired) {
|
||||
const otp = body.emailOtp
|
||||
if (!otp) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '이메일 인증번호를 입력해 주세요.'
|
||||
})
|
||||
}
|
||||
const pepper = String(config.emailOtpPepper || config.memberSessionSecret || '').trim()
|
||||
const verify = await verifyAndConsumeEmailOtp({
|
||||
email: emailNorm,
|
||||
purpose: 'signup',
|
||||
code: otp,
|
||||
pepper
|
||||
})
|
||||
if (!verify.ok) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '이메일 인증번호가 올바르지 않거나 만료되었습니다.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(body.password, 12)
|
||||
const created = await createUser({
|
||||
username: body.username,
|
||||
email: body.email,
|
||||
username: body.username.trim(),
|
||||
email: emailNorm,
|
||||
passwordHash
|
||||
})
|
||||
|
||||
@@ -74,4 +119,3 @@ export default defineEventHandler(async (event) => {
|
||||
isAdmin: Boolean(created.isAdmin)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user