Add authentication features for admin access

- Introduce a new `auth` module with login, logout, and user verification endpoints for a single admin user.
- Update backend routes to require admin authentication for sensitive operations, enhancing security.
- Modify frontend components to include an authentication provider and gate, ensuring only authorized users can access the application.
- Implement a login page for admin access, improving user experience and security management.
- Update API request handling to redirect unauthorized users to the login page, ensuring proper access control.
This commit is contained in:
Xin Wang
2026-07-09 23:27:50 +08:00
parent d857401ef3
commit 590cb33cbd
20 changed files with 534 additions and 29 deletions

View File

@@ -2,7 +2,8 @@ import type { Metadata } from "next";
import { Geist_Mono, Inter, Cormorant_Garamond } from "next/font/google";
import "./globals.css";
import { cn } from "@/lib/utils";
import { AppShell } from "@/components/layout/AppShell";
import { AuthGate } from "@/components/auth/AuthGate";
import { AuthProvider } from "@/components/auth/AuthProvider";
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
@@ -50,7 +51,9 @@ export default function RootLayout({
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
<body className="min-h-full flex flex-col">
<AppShell>{children}</AppShell>
<AuthProvider>
<AuthGate>{children}</AuthGate>
</AuthProvider>
</body>
</html>
);

View File

@@ -0,0 +1,104 @@
"use client";
import { FormEvent, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { LockKeyhole, LogIn, Video } from "lucide-react";
import { useAuth } from "@/components/auth/AuthProvider";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
export default function LoginPage() {
const router = useRouter();
const { user, loading, login } = useAuth();
const [username, setUsername] = useState("admin");
const [password, setPassword] = useState("");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
if (!loading && user) {
router.replace("/");
}
}, [loading, router, user]);
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError("");
setSubmitting(true);
try {
await login(username.trim(), password);
router.replace("/");
} catch (err) {
setError(err instanceof Error ? err.message : "登录失败");
} finally {
setSubmitting(false);
}
}
return (
<main className="flex min-h-screen items-center justify-center bg-background px-6 py-10 text-foreground">
<Card className="w-full max-w-[420px] rounded-2xl border border-hairline bg-card shadow-lg">
<CardHeader className="gap-3">
<div
className="flex h-11 w-11 items-center justify-center rounded-2xl text-on-primary shadow-sm"
style={{
backgroundColor: "var(--primary)",
backgroundImage:
"radial-gradient(circle at 30% 20%, color-mix(in srgb, var(--gradient-sky) 70%, transparent), transparent 60%), radial-gradient(circle at 80% 90%, color-mix(in srgb, var(--gradient-lavender) 65%, transparent), transparent 55%)",
}}
>
<Video size={22} style={{ color: "var(--primary-foreground)" }} />
</div>
<div>
<CardTitle className="text-xl">AI </CardTitle>
<CardDescription className="mt-1">
使
</CardDescription>
</div>
</CardHeader>
<CardContent>
<form className="space-y-4" onSubmit={handleSubmit}>
<label className="block space-y-2">
<span className="text-sm text-muted-foreground"></span>
<Input
autoComplete="username"
value={username}
onChange={(event) => setUsername(event.target.value)}
placeholder="admin"
/>
</label>
<label className="block space-y-2">
<span className="text-sm text-muted-foreground"></span>
<Input
autoComplete="current-password"
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="请输入密码"
/>
</label>
{error ? (
<div className="flex items-center gap-2 rounded-2xl border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
<LockKeyhole size={15} />
{error}
</div>
) : null}
<Button className="w-full gap-2" size="lg" disabled={submitting}>
<LogIn size={17} />
{submitting ? "登录中..." : "登录"}
</Button>
</form>
</CardContent>
</Card>
</main>
);
}