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:
34
frontend/src/components/auth/AuthGate.tsx
Normal file
34
frontend/src/components/auth/AuthGate.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { AppShell } from "@/components/layout/AppShell";
|
||||
import { useAuth } from "./AuthProvider";
|
||||
|
||||
export function AuthGate({ children }: { children: ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { user, loading } = useAuth();
|
||||
const isLoginPage = pathname === "/login";
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user && !isLoginPage) {
|
||||
router.replace("/login");
|
||||
}
|
||||
}, [isLoginPage, loading, router, user]);
|
||||
|
||||
if (isLoginPage) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
if (loading || !user) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background text-sm text-muted-foreground">
|
||||
正在校验登录状态...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <AppShell>{children}</AppShell>;
|
||||
}
|
||||
84
frontend/src/components/auth/AuthProvider.tsx
Normal file
84
frontend/src/components/auth/AuthProvider.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { authApi, type AdminUser } from "@/lib/api";
|
||||
|
||||
type AuthContextValue = {
|
||||
user: AdminUser | null;
|
||||
loading: boolean;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
refresh: () => Promise<void>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<AdminUser | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const currentUser = await authApi.me();
|
||||
setUser(currentUser);
|
||||
} catch {
|
||||
setUser(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
authApi
|
||||
.me()
|
||||
.then((currentUser) => {
|
||||
if (active) setUser(currentUser);
|
||||
})
|
||||
.catch(() => {
|
||||
if (active) setUser(null);
|
||||
})
|
||||
.finally(() => {
|
||||
if (active) setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const login = useCallback(async (username: string, password: string) => {
|
||||
const currentUser = await authApi.login({ username, password });
|
||||
setUser(currentUser);
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
await authApi.logout();
|
||||
} finally {
|
||||
setUser(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ user, loading, login, logout, refresh }),
|
||||
[user, loading, login, logout, refresh],
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within AuthProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import {
|
||||
Bot,
|
||||
Boxes,
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
Database,
|
||||
Wrench,
|
||||
Home,
|
||||
LogOut,
|
||||
PlayCircle,
|
||||
Video,
|
||||
} from "lucide-react";
|
||||
import { useAuth } from "@/components/auth/AuthProvider";
|
||||
|
||||
type SidebarProps = {
|
||||
collapsed: boolean;
|
||||
@@ -39,6 +41,8 @@ const monitorSubItems: NavItem[] = [
|
||||
|
||||
export function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { user, logout } = useAuth();
|
||||
|
||||
// 精确匹配或前缀匹配(带 / 边界),让 /assistants/xxx 也高亮"创建助手"
|
||||
const isActive = (href: string) =>
|
||||
@@ -47,6 +51,11 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
const componentActive = componentSubItems.some((item) => isActive(item.href));
|
||||
const monitorActive = monitorSubItems.some((item) => isActive(item.href));
|
||||
|
||||
async function handleLogout() {
|
||||
await logout();
|
||||
router.replace("/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={[
|
||||
@@ -193,10 +202,9 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
</nav>
|
||||
|
||||
<div className="shrink-0 space-y-2 border-t border-sidebar-border bg-sidebar p-3 shadow-[0_-12px_24px_rgba(0,0,0,0.12)]">
|
||||
{/* 个人中心 */}
|
||||
<Link
|
||||
href="/profile"
|
||||
title={collapsed ? "个人中心 · 管理员" : undefined}
|
||||
title={collapsed ? `个人中心 · ${user?.displayName ?? "超级管理员"}` : undefined}
|
||||
className={[
|
||||
"group relative flex w-full items-center overflow-hidden rounded-2xl border py-2 text-left transition-[background-color,color,border-color,box-shadow,transform] duration-200 active:scale-[0.98]",
|
||||
isActive("/profile")
|
||||
@@ -229,11 +237,30 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
].join(" ")}
|
||||
>
|
||||
<span className="block truncate text-sm font-medium text-foreground">
|
||||
管理员
|
||||
{user?.displayName ?? "超级管理员"}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
title={collapsed ? "退出登录" : undefined}
|
||||
className={[
|
||||
"group flex h-10 w-full items-center overflow-hidden rounded-full border border-hairline-strong text-sm text-muted-foreground transition-[background-color,color,border-color,transform] duration-200 hover:bg-surface-strong hover:text-foreground active:scale-[0.98]",
|
||||
collapsed ? "justify-center gap-0 px-0" : "justify-between gap-2 px-3.5",
|
||||
].join(" ")}
|
||||
>
|
||||
<span
|
||||
className={[
|
||||
"min-w-0 truncate transition-all duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]",
|
||||
collapsed ? "w-0 opacity-0 -translate-x-2" : "opacity-100 translate-x-0",
|
||||
].join(" ")}
|
||||
>
|
||||
退出登录
|
||||
</span>
|
||||
<LogOut size={18} className="shrink-0 transition-transform duration-200 group-hover:translate-x-0.5" />
|
||||
</button>
|
||||
|
||||
{/* 收起 / 展开侧栏 */}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const [dark, setDark] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setDark(document.documentElement.classList.contains("dark"));
|
||||
}, []);
|
||||
const [dark, setDark] = useState(() =>
|
||||
typeof document === "undefined"
|
||||
? true
|
||||
: document.documentElement.classList.contains("dark"),
|
||||
);
|
||||
|
||||
function toggle() {
|
||||
const next = !dark;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Bell, HelpCircle, ChevronDown } from "lucide-react";
|
||||
import { Bell, HelpCircle } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
import {
|
||||
|
||||
Reference in New Issue
Block a user