feat(auth): implement user authentication and email verification

Add registration, login, and logout flows with session management
Integrate Resend for email verification tokens
Create frontend auth views and update topbar state
This commit is contained in:
2026-04-30 11:32:46 +08:00
parent 193b4e3fd5
commit 9af8c98401
13 changed files with 898 additions and 11 deletions

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { api, setAuthToken } from '../services/api';
const route = useRoute();
const router = useRouter();
const email = ref('');
const password = ref('');
const busy = ref(false);
const errorMessage = ref('');
async function submitLogin() {
busy.value = true;
errorMessage.value = '';
try {
const response = await api.login({
email: email.value,
password: password.value
});
setAuthToken(response.token);
const redirect =
typeof route.query.redirect === 'string' && route.query.redirect.startsWith('/')
? route.query.redirect
: '/pokemon';
await router.push(redirect);
} catch (error) {
errorMessage.value = error instanceof Error && error.message ? error.message : '登录失败';
} finally {
busy.value = false;
}
}
</script>
<template>
<section class="auth-page">
<div class="auth-panel">
<div class="page-header">
<div>
<h1 class="page-title">登录</h1>
<p class="page-subtitle">使用已验证邮箱进入 Pokopia Wiki</p>
</div>
</div>
<form class="auth-form" @submit.prevent="submitLogin">
<div class="field">
<label for="login-email">邮箱</label>
<input id="login-email" v-model="email" autocomplete="email" required type="email" />
</div>
<div class="field">
<label for="login-password">密码</label>
<input id="login-password" v-model="password" autocomplete="current-password" required type="password" />
</div>
<p v-if="errorMessage" class="auth-message error">{{ errorMessage }}</p>
<button class="primary-button" :disabled="busy" type="submit">
{{ busy ? '登录中' : '登录' }}
</button>
</form>
<p class="auth-switch">
还没有账号
<RouterLink to="/register">注册</RouterLink>
</p>
</div>
</section>
</template>

View File

@@ -0,0 +1,79 @@
<script setup lang="ts">
import { ref } from 'vue';
import { api } from '../services/api';
const email = ref('');
const displayName = ref('');
const password = ref('');
const busy = ref(false);
const message = ref('');
const errorMessage = ref('');
async function submitRegister() {
busy.value = true;
message.value = '';
errorMessage.value = '';
try {
const response = await api.register({
email: email.value,
displayName: displayName.value,
password: password.value
});
message.value = response.message;
} catch (error) {
errorMessage.value = error instanceof Error && error.message ? error.message : '注册失败';
} finally {
busy.value = false;
}
}
</script>
<template>
<section class="auth-page">
<div class="auth-panel">
<div class="page-header">
<div>
<h1 class="page-title">注册</h1>
<p class="page-subtitle">创建账号后需要完成邮箱验证</p>
</div>
</div>
<form class="auth-form" @submit.prevent="submitRegister">
<div class="field">
<label for="register-email">邮箱</label>
<input id="register-email" v-model="email" autocomplete="email" required type="email" />
</div>
<div class="field">
<label for="register-display-name">显示名</label>
<input id="register-display-name" v-model="displayName" autocomplete="nickname" maxlength="40" required />
</div>
<div class="field">
<label for="register-password">密码</label>
<input
id="register-password"
v-model="password"
autocomplete="new-password"
minlength="8"
required
type="password"
/>
</div>
<p v-if="message" class="auth-message">{{ message }}</p>
<p v-if="errorMessage" class="auth-message error">{{ errorMessage }}</p>
<button class="primary-button" :disabled="busy" type="submit">
{{ busy ? '发送中' : '发送验证邮件' }}
</button>
</form>
<p class="auth-switch">
已有账号
<RouterLink to="/login">登录</RouterLink>
</p>
</div>
</section>
</template>

View File

@@ -0,0 +1,48 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { api } from '../services/api';
const route = useRoute();
const busy = ref(true);
const message = ref('');
const errorMessage = ref('');
onMounted(async () => {
const token = typeof route.query.token === 'string' ? route.query.token : '';
if (!token) {
busy.value = false;
errorMessage.value = '验证链接无效或已过期';
return;
}
try {
const response = await api.verifyEmail(token);
message.value = response.message;
} catch (error) {
errorMessage.value = error instanceof Error && error.message ? error.message : '邮箱验证失败';
} finally {
busy.value = false;
}
});
</script>
<template>
<section class="auth-page">
<div class="auth-panel">
<div class="page-header">
<div>
<h1 class="page-title">邮箱验证</h1>
<p class="page-subtitle">完成验证后即可登录</p>
</div>
</div>
<p v-if="busy" class="auth-message">正在验证邮箱</p>
<p v-else-if="message" class="auth-message">{{ message }}</p>
<p v-else class="auth-message error">{{ errorMessage }}</p>
<RouterLink class="primary-button" to="/login">去登录</RouterLink>
</div>
</section>
</template>