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

@@ -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>;
}

View 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;
}