merge: integrate feat/auth into develop
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
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),
|
||||
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]);
|
||||
}
|
||||
Reference in New Issue
Block a user