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

@@ -1,21 +1,80 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { api, getAuthToken, onAuthTokenChange, setAuthToken, type AuthUser } from './services/api';
const navItems = [
{ label: 'Pokemon', to: '/pokemon' },
{ label: '栖息地', to: '/habitats' },
{ label: '物品 / 材料单', to: '/items' },
{ label: '管理', to: '/admin' }
];
const router = useRouter();
const currentUser = ref<AuthUser | null>(null);
let removeAuthListener: (() => void) | null = null;
async function loadCurrentUser() {
if (!getAuthToken()) {
currentUser.value = null;
return;
}
try {
const response = await api.me();
currentUser.value = response.user;
} catch {
currentUser.value = null;
setAuthToken(null);
}
}
async function logout() {
try {
await api.logout();
} catch {
// The local session is cleared even when the server session is already gone.
}
currentUser.value = null;
setAuthToken(null);
await router.push('/pokemon');
}
onMounted(() => {
void loadCurrentUser();
removeAuthListener = onAuthTokenChange(() => {
void loadCurrentUser();
});
});
onUnmounted(() => {
removeAuthListener?.();
});
</script>
<template>
<div class="app-shell">
<header class="topbar">
<RouterLink class="brand" to="/pokemon">Pokopia Wiki</RouterLink>
<nav class="nav-tabs" aria-label="主导航">
<RouterLink v-for="item in navItems" :key="item.to" :to="item.to">
{{ item.label }}
</RouterLink>
</nav>
<div class="topbar-main">
<RouterLink class="brand" to="/pokemon">Pokopia Wiki</RouterLink>
<nav class="nav-tabs" aria-label="主导航">
<RouterLink v-for="item in navItems" :key="item.to" :to="item.to">
{{ item.label }}
</RouterLink>
</nav>
</div>
<div class="auth-actions">
<template v-if="currentUser">
<span class="auth-user">{{ currentUser.displayName || currentUser.email }}</span>
<button class="plain-button" type="button" @click="logout">退出</button>
</template>
<template v-else>
<RouterLink to="/login">登录</RouterLink>
<RouterLink to="/register">注册</RouterLink>
</template>
</div>
</header>
<main class="page">