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; is_system_admin: boolean; permissions: string[]; }; 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 = { data: T; }; type AuthContextValue = { user: AuthUser | null; workspaces: AuthWorkspace[]; currentWorkspace: AuthWorkspace | null; loading: boolean; login: (username: string, password: string) => Promise; logout: () => Promise; setCurrentWorkspace: (workspaceId: string) => void; refreshWorkspaces: () => Promise; }; const AuthContext = createContext(null); const workspaceStorageKey = "model-platform-current-workspace"; async function authRequest(path: string, init: RequestInit = {}): Promise { 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 | { detail?: string }; if (!response.ok) { throw new Error( "detail" in payload && typeof payload.detail === "string" ? payload.detail : `请求失败(HTTP ${response.status})`, ); } return (payload as ApiEnvelope).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(null); const [workspaces, setWorkspaces] = useState([]); const [currentWorkspace, setWorkspace] = useState(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("/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("/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 refreshWorkspaces = useCallback(async () => { try { const session = await authRequest("/api/v1/auth/me"); const storedId = typeof window === "undefined" ? null : window.localStorage.getItem(workspaceStorageKey); const newWorkspace = selectWorkspace(session, storedId); setWorkspaces(session.workspaces); setWorkspace(newWorkspace); if (newWorkspace && typeof window !== "undefined") { window.localStorage.setItem(workspaceStorageKey, newWorkspace.workspace_id); } } catch (error) { console.error("刷新 workspace 列表失败:", error); } }, []); const value = useMemo(() => ({ user, workspaces, currentWorkspace, loading, login, logout, setCurrentWorkspace, refreshWorkspaces, }), [currentWorkspace, loading, login, logout, setCurrentWorkspace, user, workspaces, refreshWorkspaces]); if (loading) { return (
正在验证登录状态…
); } return {children}; } 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(() => ({ listScripts: (parentPath, ownerUserId) => rawApi.listScripts(workspaceId, parentPath, ownerUserId), countScripts: () => rawApi.countScripts(workspaceId), listResources: (parentPath, opts) => rawApi.listResources(workspaceId, parentPath, opts), createScript: (input) => rawApi.createScript(workspaceId, input), uploadScript: (file, parentPath, visibility) => rawApi.uploadScript(workspaceId, file, parentPath, visibility), createResourceUpload: (body) => rawApi.createResourceUpload(workspaceId, body), uploadResourceBytes: (uploadId, fileBytes, contentType) => rawApi.uploadResourceBytes(workspaceId, uploadId, fileBytes, contentType), bindResourceUpload: (uploadId, body) => rawApi.bindResourceUpload(workspaceId, uploadId, body), deleteResource: (resourceId) => rawApi.deleteResource(workspaceId, resourceId), updateScript: (scriptId, input) => rawApi.updateScript(workspaceId, scriptId, input), deleteScript: (scriptId) => rawApi.deleteScript(workspaceId, scriptId), setScriptLock: (scriptId, isLocked) => rawApi.setScriptLock(workspaceId, scriptId, isLocked), listWorkspaceDirectories: (parentPath, ownerUserId) => rawApi.listWorkspaceDirectories(workspaceId, parentPath ?? "", ownerUserId), 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), 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), listPlatformEmployees: () => rawApi.listPlatformEmployees(), createEmployee: (input) => rawApi.createEmployee(workspaceId, input), createPlatformEmployee: (input) => rawApi.createPlatformEmployee(input), updateEmployee: (userId, input) => rawApi.updateEmployee(workspaceId, userId, input), updatePlatformEmployee: (userId, input) => rawApi.updatePlatformEmployee(userId, input), deleteEmployee: (userId) => rawApi.deleteEmployee(workspaceId, userId), deletePlatformEmployee: (userId) => rawApi.deletePlatformEmployee(userId), createScheduleNode: (scheduleId, input) => rawApi.createScheduleNode(workspaceId, scheduleId, input), updateScheduleNode: (scheduleId, nodeId, input) => rawApi.updateScheduleNode(workspaceId, scheduleId, nodeId, input), deleteScheduleNode: (scheduleId, nodeId, workflowVersion, options) => rawApi.deleteScheduleNode( workspaceId, scheduleId, nodeId, workflowVersion, options, ), 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), getScheduleNodeRunArtifacts: (runId, nodeRunId) => rawApi.getScheduleNodeRunArtifacts(workspaceId, runId, nodeRunId), // Workspace (Project) Management - 系统管理接口(跨 workspace,不需要传入 workspaceId) listWorkspaces: () => rawApi.listWorkspaces(), createWorkspace: (input) => rawApi.createWorkspace(input), updateWorkspace: (workspaceId, input) => rawApi.updateWorkspace(workspaceId, input), deleteWorkspace: (workspaceId) => rawApi.deleteWorkspace(workspaceId), listWorkspaceMembers: (workspaceId) => rawApi.listWorkspaceMembers(workspaceId), addWorkspaceMember: (workspaceId, input) => rawApi.addWorkspaceMember(workspaceId, input), updateWorkspaceMember: (workspaceId, userId, input) => rawApi.updateWorkspaceMember(workspaceId, userId, input), deleteWorkspaceMember: (workspaceId, userId) => rawApi.deleteWorkspaceMember(workspaceId, userId), }), [workspaceId]); } export function usePermission(code: string): boolean { const { user } = useAuth(); return user?.permissions.includes(code) ?? false; }