feat: 添加用户登录认证功能

- 新增登录页面(使用 shadcn-svelte 组件)
- 后端添加 JWT 认证 API (/api/auth/login, /api/auth/validate)
- 用户账号通过 server/config.yaml 配置
- 前端路由保护(未登录跳转登录页)
- 侧边栏显示当前用户信息
- 支持退出登录功能
This commit is contained in:
clz
2026-01-11 18:50:01 +08:00
parent 4884993d27
commit 829b3445bc
11 changed files with 639 additions and 16 deletions

View File

@@ -1,8 +1,11 @@
<script lang="ts">
import '../app.css';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { checkHealth } from '$lib/api';
import { auth, type User as AuthUser } from '$lib/stores/auth';
import * as Sidebar from '$lib/components/ui/sidebar';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Avatar from '$lib/components/ui/avatar';
@@ -37,6 +40,17 @@
let themeMode = $state<ThemeMode>('system');
let serverOnline = $state(true);
let checkingHealth = $state(true);
let isAuthenticated = $state(false);
let currentUser = $state<AuthUser | null>(null);
// 订阅认证状态
$effect(() => {
const unsubscribe = auth.subscribe(state => {
isAuthenticated = state.isAuthenticated;
currentUser = state.user;
});
return unsubscribe;
});
async function checkServerHealth() {
checkingHealth = true;
@@ -44,10 +58,23 @@
checkingHealth = false;
}
// 登出
function handleLogout() {
auth.logout();
goto('/login');
}
onMount(() => {
themeMode = loadThemeFromStorage();
applyThemeToDocument(themeMode);
// 检查登录状态,未登录则跳转到登录页
const pathname = $page.url.pathname;
if (!auth.check() && pathname !== '/login' && pathname !== '/health') {
goto('/login');
return;
}
// 检查服务器状态
checkServerHealth();
// 每 30 秒检查一次
@@ -83,12 +110,12 @@
{ href: '/help', label: '帮助', icon: HelpCircle },
];
// 用户数据
const user = {
name: '用户',
email: 'user@example.com',
avatar: ''
};
// 用户数据(从认证状态中获取)
let user = $derived({
name: currentUser?.username || '用户',
email: currentUser?.email || 'user@example.com',
avatar: currentUser?.avatar || ''
});
function isActive(href: string, pathname: string): boolean {
if (href === '/') return pathname === '/';
@@ -106,8 +133,14 @@
};
return titles[pathname] || 'BillAI';
}
// 检查是否是登录页面(登录页不显示侧边栏)
let isLoginPage = $derived($page.url.pathname === '/login');
</script>
{#if isLoginPage}
{@render children()}
{:else}
<Sidebar.Provider>
<Sidebar.Root collapsible="offcanvas">
<!-- Header: Logo + App Name -->
@@ -249,7 +282,7 @@
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item>
<DropdownMenu.Item onclick={handleLogout}>
<LogOut class="mr-2 size-4" />
退出登录
</DropdownMenu.Item>
@@ -266,7 +299,7 @@
<Sidebar.Trigger class="-ml-1" />
<Separator orientation="vertical" class="mr-2 h-4" />
<h1 class="text-lg font-semibold">{getPageTitle($page.url.pathname)}</h1>
<div class="flex-1" />
<div class="flex-1"></div>
<div class="flex items-center gap-3">
<button
class="flex items-center gap-1.5 text-sm hover:opacity-80 transition-opacity"
@@ -299,3 +332,4 @@
</main>
</Sidebar.Inset>
</Sidebar.Provider>
{/if}