用户系统

This commit is contained in:
2026-04-15 00:21:43 +08:00
parent 97bbb3f306
commit 47c0863bab
23 changed files with 1640 additions and 56 deletions

View File

@@ -0,0 +1,161 @@
"use client";
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
export interface AuthUser {
id: string;
username: string;
display_name: string;
is_admin: boolean;
}
interface AuthContextValue {
user: AuthUser | null;
token: string | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextValue | null>(null);
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}
const TOKEN_KEY = "epeekit-auth-token";
const REFRESH_KEY = "epeekit-refresh-token";
const USER_KEY = "epeekit-auth-user";
export function getStoredToken(): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(TOKEN_KEY);
}
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null);
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const saveAuth = useCallback((accessToken: string, refreshToken: string, userData: AuthUser) => {
localStorage.setItem(TOKEN_KEY, accessToken);
localStorage.setItem(REFRESH_KEY, refreshToken);
localStorage.setItem(USER_KEY, JSON.stringify(userData));
setToken(accessToken);
setUser(userData);
}, []);
const clearAuth = useCallback(() => {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_KEY);
localStorage.removeItem(USER_KEY);
setToken(null);
setUser(null);
}, []);
// 尝试用 refresh token 获取新 access token
const tryRefresh = useCallback(async (): Promise<boolean> => {
const refreshToken = localStorage.getItem(REFRESH_KEY);
if (!refreshToken) return false;
try {
const resp = await fetch(`${API_URL}/api/auth/refresh`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token: refreshToken }),
});
if (!resp.ok) return false;
const data = await resp.json();
const savedUser = localStorage.getItem(USER_KEY);
if (savedUser && data.access_token) {
const userData = JSON.parse(savedUser) as AuthUser;
localStorage.setItem(TOKEN_KEY, data.access_token);
setToken(data.access_token);
setUser(userData);
return true;
}
return false;
} catch {
return false;
}
}, []);
// 启动时验证 token
useEffect(() => {
async function init() {
const savedToken = localStorage.getItem(TOKEN_KEY);
if (!savedToken) {
setIsLoading(false);
return;
}
try {
const resp = await fetch(`${API_URL}/api/auth/me`, {
headers: { Authorization: `Bearer ${savedToken}` },
});
if (resp.ok) {
const userData = await resp.json();
setToken(savedToken);
setUser(userData);
} else if (resp.status === 401) {
const refreshed = await tryRefresh();
if (!refreshed) clearAuth();
} else {
clearAuth();
}
} catch {
// 网络错误时保留本地缓存的用户信息,允许离线使用
const savedUser = localStorage.getItem(USER_KEY);
if (savedUser) {
setToken(savedToken);
setUser(JSON.parse(savedUser));
}
}
setIsLoading(false);
}
init();
}, [clearAuth, tryRefresh]);
const login = useCallback(async (username: string, password: string) => {
const resp = await fetch(`${API_URL}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({ detail: "登录失败" }));
throw new Error(err.detail || "登录失败");
}
const data = await resp.json();
saveAuth(data.access_token, data.refresh_token, data.user);
}, [saveAuth]);
const logout = useCallback(() => {
clearAuth();
}, [clearAuth]);
const value = useMemo<AuthContextValue>(
() => ({
user,
token,
isAuthenticated: !!token && !!user,
isLoading,
login,
logout,
}),
[user, token, isLoading, login, logout]
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}