헤더 검색 중앙 정렬·Resend 이메일 OTP·비밀번호 찾기 (v0.0.99)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
111
pages/signup.vue
111
pages/signup.vue
@@ -18,13 +18,15 @@ const { data: siteSettings } = await useFetch('/api/site-settings', {
|
||||
const { data: bootstrapStatus } = await useFetch('/api/auth/bootstrap-status', {
|
||||
default: () => ({
|
||||
hasUsers: true,
|
||||
needsAdminSetup: false
|
||||
needsAdminSetup: false,
|
||||
emailOtpConfigured: false
|
||||
})
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
emailOtp: '',
|
||||
password: '',
|
||||
passwordConfirm: ''
|
||||
})
|
||||
@@ -32,14 +34,20 @@ const form = reactive({
|
||||
const errors = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
emailOtp: '',
|
||||
password: '',
|
||||
passwordConfirm: ''
|
||||
})
|
||||
|
||||
const showSignupPassword = ref(false)
|
||||
const showSignupPasswordConfirm = ref(false)
|
||||
const otpRequestLoading = ref(false)
|
||||
const otpCooldownSeconds = ref(0)
|
||||
let otpCooldownTimerId = null
|
||||
|
||||
const isAdminBootstrapMode = computed(() => Boolean(bootstrapStatus.value?.needsAdminSetup))
|
||||
/** Resend 등 설정이 되어 있고 최초 관리자 모드가 아닐 때만 이메일 OTP 필요 */
|
||||
const emailOtpRequired = computed(() => Boolean(bootstrapStatus.value?.emailOtpConfigured) && !isAdminBootstrapMode.value)
|
||||
const welcomeTitle = computed(() => `Welcome to ${siteSettings.value?.title || 'AFFiNE'}`)
|
||||
const welcomeDescription = computed(() => siteSettings.value?.description || 'Configure your Self Host AFFiNE with a few simple settings.')
|
||||
const stepTwoTitle = computed(() => (isAdminBootstrapMode.value ? '관리자 등록' : '회원 가입'))
|
||||
@@ -54,6 +62,7 @@ const stepTwoDescription = computed(() => (isAdminBootstrapMode.value
|
||||
const resetErrors = () => {
|
||||
errors.username = ''
|
||||
errors.email = ''
|
||||
errors.emailOtp = ''
|
||||
errors.password = ''
|
||||
errors.passwordConfirm = ''
|
||||
}
|
||||
@@ -95,9 +104,65 @@ const validateStepTwo = () => {
|
||||
valid = false
|
||||
}
|
||||
|
||||
if (emailOtpRequired.value) {
|
||||
const digits = String(form.emailOtp || '').replace(/\D/g, '')
|
||||
if (digits.length !== 6) {
|
||||
errors.emailOtp = '이메일로 받은 6자리 인증번호를 입력해 주세요.'
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
|
||||
return valid
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원가입용 이메일 인증번호를 요청한다.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const requestSignupEmailOtp = async () => {
|
||||
submitErrorMessage.value = ''
|
||||
errors.email = ''
|
||||
errors.emailOtp = ''
|
||||
if (!form.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) {
|
||||
errors.email = '인증번호를 받으려면 유효한 이메일을 먼저 입력해 주세요.'
|
||||
return
|
||||
}
|
||||
if (otpCooldownSeconds.value > 0 || otpRequestLoading.value) {
|
||||
return
|
||||
}
|
||||
otpRequestLoading.value = true
|
||||
try {
|
||||
await $fetch('/api/auth/email-otp/request', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email: form.email.trim(),
|
||||
purpose: 'signup'
|
||||
}
|
||||
})
|
||||
otpCooldownSeconds.value = 60
|
||||
if (otpCooldownTimerId) {
|
||||
clearInterval(otpCooldownTimerId)
|
||||
}
|
||||
otpCooldownTimerId = setInterval(() => {
|
||||
otpCooldownSeconds.value -= 1
|
||||
if (otpCooldownSeconds.value <= 0 && otpCooldownTimerId) {
|
||||
clearInterval(otpCooldownTimerId)
|
||||
otpCooldownTimerId = null
|
||||
}
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
submitErrorMessage.value = error?.data?.message || '인증번호 요청에 실패했습니다.'
|
||||
} finally {
|
||||
otpRequestLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (otpCooldownTimerId) {
|
||||
clearInterval(otpCooldownTimerId)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 다음 단계로 이동한다.
|
||||
* @returns {Promise<void>}
|
||||
@@ -119,13 +184,17 @@ const goNextStep = async () => {
|
||||
isSubmitting.value = true
|
||||
|
||||
try {
|
||||
const signupBody = {
|
||||
username: form.username.trim(),
|
||||
email: form.email.trim(),
|
||||
password: form.password
|
||||
}
|
||||
if (emailOtpRequired.value) {
|
||||
signupBody.emailOtp = String(form.emailOtp || '').replace(/\D/g, '')
|
||||
}
|
||||
const signupResult = await $fetch('/api/auth/signup', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
username: form.username.trim(),
|
||||
email: form.email.trim(),
|
||||
password: form.password
|
||||
}
|
||||
body: signupBody
|
||||
})
|
||||
createdAdmin.value = Boolean(signupResult?.isAdmin)
|
||||
|
||||
@@ -207,6 +276,36 @@ const goPreviousStep = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="emailOtpRequired" class="space-y-1.5">
|
||||
<label class="text-xs text-[#d8dee6]">이메일 인증번호</label>
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-stretch">
|
||||
<input
|
||||
v-model="form.emailOtp"
|
||||
class="auth-form-input h-10 min-w-0 flex-1 rounded-[8px] border border-[#1a212a] bg-transparent px-3 text-sm tracking-widest outline-none transition-colors focus:border-[#2f6feb]"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
pattern="[0-9]*"
|
||||
placeholder="6자리"
|
||||
autocomplete="one-time-code"
|
||||
>
|
||||
<button
|
||||
class="auth-signup__otp-send h-10 shrink-0 rounded-[8px] border border-[#1a212a] px-4 text-xs text-[#d8dee6] transition-opacity hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
type="button"
|
||||
:disabled="otpRequestLoading || otpCooldownSeconds > 0"
|
||||
@click="requestSignupEmailOtp"
|
||||
>
|
||||
{{ otpCooldownSeconds > 0 ? `${otpCooldownSeconds}초 후 재요청` : (otpRequestLoading ? '발송 중…' : '인증번호 받기') }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="errors.emailOtp" class="text-xs text-[#e05d67]">
|
||||
{{ errors.emailOtp }}
|
||||
</p>
|
||||
<p class="text-xs text-[#8c95a3]">
|
||||
Resend로 발송됩니다. 메일이 보이지 않으면 스팸함을 확인해 주세요.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs text-[#d8dee6]">{{ isAdminBootstrapMode ? '관리자 비밀번호' : '비밀번호' }}</label>
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user