Files
model-platform/frontend/app/context/AuthContext.tsx
T
2026-08-04 13:35:16 +08:00

259 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
createContext,
type ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { useLocation, useNavigate } from "react-router";
import * as rawApi from "../services/api";
import type { WorkspaceBoundApi } from "../services/api";
export type AuthUser = {
user_id: string;
username: string;
display_name: string;
email: string | null;
status: string;
role_code: string | null;
};
export type AuthWorkspace = {
workspace_id: string;
workspace_code: string;
workspace_name: string;
role_code: string;
role_name: string;
};
type AuthSession = {
user: AuthUser;
workspaces: AuthWorkspace[];
default_workspace_id: string | null;
};
type ApiEnvelope<T> = {
data: T;
};
type AuthContextValue = {
user: AuthUser | null;
workspaces: AuthWorkspace[];
currentWorkspace: AuthWorkspace | null;
loading: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => Promise<void>;
setCurrentWorkspace: (workspaceId: string) => void;
};
const AuthContext = createContext<AuthContextValue | null>(null);
const workspaceStorageKey = "model-platform-current-workspace";
async function authRequest<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(path, {
...init,
credentials: "same-origin",
headers: {
...(init.body ? { "Content-Type": "application/json" } : {}),
...init.headers,
},
});
const payload = await response.json().catch(() => ({})) as
| ApiEnvelope<T>
| { detail?: string };
if (!response.ok) {
throw new Error(
"detail" in payload && typeof payload.detail === "string"
? payload.detail
: `请求失败(HTTP ${response.status}`,
);
}
return (payload as ApiEnvelope<T>).data;
}
function selectWorkspace(
session: AuthSession,
preferredId?: string | null,
): AuthWorkspace | null {
return session.workspaces.find((item) => item.workspace_id === preferredId)
?? session.workspaces.find(
(item) => item.workspace_id === session.default_workspace_id,
)
?? session.workspaces[0]
?? null;
}
export function AuthProvider({ children }: { children: ReactNode }) {
const location = useLocation();
const navigate = useNavigate();
const [user, setUser] = useState<AuthUser | null>(null);
const [workspaces, setWorkspaces] = useState<AuthWorkspace[]>([]);
const [currentWorkspace, setWorkspace] = useState<AuthWorkspace | null>(null);
const [loading, setLoading] = useState(true);
const applySession = useCallback((session: AuthSession) => {
const storedId = typeof window === "undefined"
? null
: window.localStorage.getItem(workspaceStorageKey);
const workspace = selectWorkspace(session, storedId);
setUser(session.user);
setWorkspaces(session.workspaces);
setWorkspace(workspace);
if (workspace && typeof window !== "undefined") {
window.localStorage.setItem(workspaceStorageKey, workspace.workspace_id);
}
}, []);
useEffect(() => {
let cancelled = false;
void authRequest<AuthSession>("/api/v1/auth/me")
.then((session) => {
if (!cancelled) applySession(session);
})
.catch(() => {
if (!cancelled) {
setUser(null);
setWorkspaces([]);
setWorkspace(null);
}
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [applySession]);
useEffect(() => {
if (loading) return;
if (!user && location.pathname !== "/login") {
navigate("/login", { replace: true });
} else if (user && location.pathname === "/login") {
navigate("/workbench", { replace: true });
}
}, [loading, location.pathname, navigate, user]);
const login = useCallback(async (username: string, password: string) => {
const session = await authRequest<AuthSession>("/api/v1/auth/login", {
method: "POST",
body: JSON.stringify({ username, password }),
});
applySession(session);
navigate("/workbench", { replace: true });
}, [applySession, navigate]);
const logout = useCallback(async () => {
try {
await authRequest<{ logged_out: boolean }>("/api/v1/auth/logout", {
method: "POST",
});
} finally {
setUser(null);
setWorkspaces([]);
setWorkspace(null);
navigate("/login", { replace: true });
}
}, [navigate]);
const setCurrentWorkspace = useCallback((workspaceId: string) => {
const workspace = workspaces.find((item) => item.workspace_id === workspaceId);
if (!workspace) return;
setWorkspace(workspace);
window.localStorage.setItem(workspaceStorageKey, workspace.workspace_id);
}, [workspaces]);
const value = useMemo<AuthContextValue>(() => ({
user,
workspaces,
currentWorkspace,
loading,
login,
logout,
setCurrentWorkspace,
}), [currentWorkspace, loading, login, logout, setCurrentWorkspace, user, workspaces]);
if (loading) {
return (
<div className="auth-loading" role="status">
<span />
正在验证登录状态…
</div>
);
}
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth 必须在 AuthProvider 内使用");
}
return context;
}
export function useApi(): WorkspaceBoundApi {
const { currentWorkspace } = useAuth();
const workspaceId = currentWorkspace?.workspace_id ?? "";
return useMemo<WorkspaceBoundApi>(() => ({
listScripts: () => rawApi.listScripts(workspaceId),
createScript: (input) => rawApi.createScript(workspaceId, input),
uploadScript: (file, parentPath, visibility) =>
rawApi.uploadScript(workspaceId, file, parentPath, visibility),
updateScript: (scriptId, input) =>
rawApi.updateScript(workspaceId, scriptId, input),
deleteScript: (scriptId) => rawApi.deleteScript(workspaceId, scriptId),
listWorkspaceDirectories: () => rawApi.listWorkspaceDirectories(workspaceId),
createWorkspaceDirectory: (directoryName, parentPath) =>
rawApi.createWorkspaceDirectory(workspaceId, directoryName, parentPath),
deleteWorkspaceDirectory: (path) =>
rawApi.deleteWorkspaceDirectory(workspaceId, path),
acquireFileLock: (script) => rawApi.acquireFileLock(workspaceId, script),
heartbeatFileLock: (session) => rawApi.heartbeatFileLock(workspaceId, session),
releaseFileLock: (session) => rawApi.releaseFileLock(workspaceId, session),
releaseFileLockOnUnload: (session) =>
rawApi.releaseFileLockOnUnload(workspaceId, session),
createJupyterAccessTicket: (session) =>
rawApi.createJupyterAccessTicket(workspaceId, session),
getLatestScriptVersion: (scriptId) =>
rawApi.getLatestScriptVersion(workspaceId, scriptId),
listScriptVersions: (scriptId) => rawApi.listScriptVersions(workspaceId, scriptId),
publishScriptVersion: (input) => rawApi.publishScriptVersion(workspaceId, input),
listSchedules: () => rawApi.listSchedules(workspaceId),
getSchedule: (scheduleId) => rawApi.getSchedule(workspaceId, scheduleId),
createSchedule: (input) => rawApi.createSchedule(workspaceId, input),
updateSchedule: (scheduleId, input) =>
rawApi.updateSchedule(workspaceId, scheduleId, input),
deleteSchedule: (scheduleId, workflowVersion) =>
rawApi.deleteSchedule(workspaceId, scheduleId, workflowVersion),
listScheduleArtifacts: () => rawApi.listScheduleArtifacts(workspaceId),
hideScheduleArtifact: (versionsId) =>
rawApi.hideScheduleArtifact(workspaceId, versionsId),
listEmployees: () => rawApi.listEmployees(workspaceId),
createEmployee: (input) => rawApi.createEmployee(workspaceId, input),
updateEmployee: (userId, input) =>
rawApi.updateEmployee(workspaceId, userId, input),
deleteEmployee: (userId) => rawApi.deleteEmployee(workspaceId, userId),
createScheduleNode: (scheduleId, input) =>
rawApi.createScheduleNode(workspaceId, scheduleId, input),
updateScheduleNode: (scheduleId, nodeId, input) =>
rawApi.updateScheduleNode(workspaceId, scheduleId, nodeId, input),
deleteScheduleNode: (scheduleId, nodeId, workflowVersion) =>
rawApi.deleteScheduleNode(workspaceId, scheduleId, nodeId, workflowVersion),
createScheduleEdge: (scheduleId, input) =>
rawApi.createScheduleEdge(workspaceId, scheduleId, input),
deleteScheduleEdge: (scheduleId, edgeId, workflowVersion) =>
rawApi.deleteScheduleEdge(workspaceId, scheduleId, edgeId, workflowVersion),
validateSchedule: (scheduleId) => rawApi.validateSchedule(workspaceId, scheduleId),
previewCron: (input) => rawApi.previewCron(workspaceId, input),
runScheduleNow: (scheduleId) => rawApi.runScheduleNow(workspaceId, scheduleId),
listScheduleRuns: (input) => rawApi.listScheduleRuns(workspaceId, input),
getScheduleRun: (runId) => rawApi.getScheduleRun(workspaceId, runId),
}), [workspaceId]);
}