Browse Source

feat(auth): captcha UI on login and register pages

Made-with: Cursor
main
npmrun 7 hours ago
parent
commit
e7c1c91877
  1. 56
      app/pages/login/index.vue
  2. 56
      app/pages/register/index.vue

56
app/pages/login/index.vue

@ -11,6 +11,7 @@ definePageMeta({
type LoginFormState = { type LoginFormState = {
username: string username: string
password: string password: string
captchaAnswer: string
} }
type LoginResult = { type LoginResult = {
@ -25,6 +26,25 @@ const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,20}$/
const state = reactive<LoginFormState>({ const state = reactive<LoginFormState>({
username: '', username: '',
password: '', password: '',
captchaAnswer: '',
})
const captchaIdRef = ref('')
const captchaImageSrc = ref('')
async function refreshCaptcha() {
state.captchaAnswer = ''
captchaImageSrc.value = ''
const res = await fetchData<{ captchaId: string; imageSvg: string }>('/api/auth/captcha', {
method: 'GET',
notify: false,
})
captchaIdRef.value = res.captchaId
captchaImageSrc.value = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(res.imageSvg)}`
}
onMounted(() => {
void refreshCaptcha()
}) })
const loading = ref(false) const loading = ref(false)
@ -50,6 +70,10 @@ const validate = (formState: LoginFormState): FormError[] => {
errors.push({ name: 'password', message: '密码至少 6 位' }) errors.push({ name: 'password', message: '密码至少 6 位' })
} }
if (!formState.captchaAnswer?.trim()) {
errors.push({ name: 'captchaAnswer', message: '请输入验证码' })
}
return errors return errors
} }
@ -61,7 +85,12 @@ const onSubmit = async (_event: FormSubmitEvent<LoginFormState>) => {
try { try {
const res = await fetchData<LoginResult>('/api/auth/login', { const res = await fetchData<LoginResult>('/api/auth/login', {
method: 'POST', method: 'POST',
body: { username: state.username, password: state.password }, body: {
username: state.username,
password: state.password,
captchaId: captchaIdRef.value,
captchaAnswer: state.captchaAnswer,
},
notify: false, notify: false,
}) })
@ -77,6 +106,11 @@ const onSubmit = async (_event: FormSubmitEvent<LoginFormState>) => {
} catch (error: unknown) { } catch (error: unknown) {
resultType.value = 'error' resultType.value = 'error'
resultMessage.value = getApiErrorMessage(error) resultMessage.value = getApiErrorMessage(error)
try {
await refreshCaptcha()
} catch {
/* ignore captcha refresh errors */
}
} finally { } finally {
loading.value = false loading.value = false
} }
@ -124,6 +158,26 @@ const onSubmit = async (_event: FormSubmitEvent<LoginFormState>) => {
/> />
</UFormField> </UFormField>
<UFormField label="验证码" name="captchaAnswer" required>
<div class="flex flex-wrap gap-2 items-center">
<img
v-if="captchaImageSrc"
:src="captchaImageSrc"
alt="验证码"
class="h-10 rounded border border-default bg-elevated shrink-0"
/>
<UInput
v-model="state.captchaAnswer"
placeholder="请输入图中字符"
class="flex-1 min-w-[8rem]"
autocomplete="off"
/>
<UButton type="button" color="neutral" variant="outline" class="shrink-0" @click="refreshCaptcha">
换一张
</UButton>
</div>
</UFormField>
<UButton type="submit" block :loading="loading"> <UButton type="submit" block :loading="loading">
立即登录 立即登录
</UButton> </UButton>

56
app/pages/register/index.vue

@ -19,6 +19,7 @@ definePageMeta({
type RegisterFormState = { type RegisterFormState = {
username: string username: string
password: string password: string
captchaAnswer: string
} }
const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,20}$/ const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,20}$/
@ -26,6 +27,25 @@ const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,20}$/
const state = reactive<RegisterFormState>({ const state = reactive<RegisterFormState>({
username: '', username: '',
password: '', password: '',
captchaAnswer: '',
})
const captchaIdRef = ref('')
const captchaImageSrc = ref('')
async function refreshCaptcha() {
state.captchaAnswer = ''
captchaImageSrc.value = ''
const res = await fetchData<{ captchaId: string; imageSvg: string }>('/api/auth/captcha', {
method: 'GET',
notify: false,
})
captchaIdRef.value = res.captchaId
captchaImageSrc.value = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(res.imageSvg)}`
}
onMounted(() => {
void refreshCaptcha()
}) })
const loading = ref(false) const loading = ref(false)
@ -49,6 +69,10 @@ const validate = (formState: RegisterFormState): FormError[] => {
errors.push({ name: 'password', message: '密码至少 6 位' }) errors.push({ name: 'password', message: '密码至少 6 位' })
} }
if (!formState.captchaAnswer?.trim()) {
errors.push({ name: 'captchaAnswer', message: '请输入验证码' })
}
return errors return errors
} }
@ -60,7 +84,12 @@ const onSubmit = async (_event: FormSubmitEvent<RegisterFormState>) => {
try { try {
await fetchData<unknown>('/api/auth/register', { await fetchData<unknown>('/api/auth/register', {
method: 'POST', method: 'POST',
body: { username: state.username, password: state.password }, body: {
username: state.username,
password: state.password,
captchaId: captchaIdRef.value,
captchaAnswer: state.captchaAnswer,
},
notify: false, notify: false,
}) })
@ -75,6 +104,11 @@ const onSubmit = async (_event: FormSubmitEvent<RegisterFormState>) => {
} catch (error: unknown) { } catch (error: unknown) {
resultType.value = 'error' resultType.value = 'error'
resultMessage.value = getApiErrorMessage(error) resultMessage.value = getApiErrorMessage(error)
try {
await refreshCaptcha()
} catch {
/* ignore captcha refresh errors */
}
} finally { } finally {
loading.value = false loading.value = false
} }
@ -119,6 +153,26 @@ const onSubmit = async (_event: FormSubmitEvent<RegisterFormState>) => {
/> />
</UFormField> </UFormField>
<UFormField label="验证码" name="captchaAnswer" required>
<div class="flex flex-wrap gap-2 items-center">
<img
v-if="captchaImageSrc"
:src="captchaImageSrc"
alt="验证码"
class="h-10 rounded border border-default bg-elevated shrink-0"
/>
<UInput
v-model="state.captchaAnswer"
placeholder="请输入图中字符"
class="flex-1 min-w-[8rem]"
autocomplete="off"
/>
<UButton type="button" color="neutral" variant="outline" class="shrink-0" @click="refreshCaptcha">
换一张
</UButton>
</div>
</UFormField>
<UButton type="submit" block :loading="loading"> <UButton type="submit" block :loading="loading">
立即注册 立即注册
</UButton> </UButton>

Loading…
Cancel
Save