用户系统
This commit is contained in:
161
art-agent/frontend/src/lib/auth-context.tsx
Normal file
161
art-agent/frontend/src/lib/auth-context.tsx
Normal 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>;
|
||||
}
|
||||
Reference in New Issue
Block a user