312 lines
12 KiB
TypeScript
312 lines
12 KiB
TypeScript
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;
|
||
};
|
||
|
||
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;
|
||
refreshWorkspaces: () => Promise<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 refreshWorkspaces = useCallback(async () => {
|
||
try {
|
||
const session = await authRequest<AuthSession>("/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<AuthContextValue>(() => ({
|
||
user,
|
||
workspaces,
|
||
currentWorkspace,
|
||
loading,
|
||
login,
|
||
logout,
|
||
setCurrentWorkspace,
|
||
refreshWorkspaces,
|
||
}), [currentWorkspace, loading, login, logout, setCurrentWorkspace, user, workspaces, refreshWorkspaces]);
|
||
|
||
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),
|
||
listResources: (opts) => rawApi.listResources(workspaceId, 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?: string) =>
|
||
rawApi.listWorkspaceDirectories(workspaceId, parentPath ?? ""),
|
||
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]);
|
||
}
|