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:
71
frontend/src/views/LoginView.vue
Normal file
71
frontend/src/views/LoginView.vue
Normal 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>
|
||||
79
frontend/src/views/RegisterView.vue
Normal file
79
frontend/src/views/RegisterView.vue
Normal 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>
|
||||
48
frontend/src/views/VerifyEmailView.vue
Normal file
48
frontend/src/views/VerifyEmailView.vue
Normal 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>
|
||||
Reference in New Issue
Block a user