282 lines
8.7 KiB
TypeScript
282 lines
8.7 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
|
|
import { platformApiClient, setPlatformApiSessionToken } from "../api/client";
|
|
import type {
|
|
AuthSessionResponse,
|
|
CurrentUserResponse,
|
|
LoginRequest,
|
|
RegisterRequest,
|
|
UserProfileUpdateRequest,
|
|
UserThemePreferenceRequest,
|
|
UserThemePreferenceResponse
|
|
} from "../api/types";
|
|
import type { CurrentUserView } from "../contracts/workspace";
|
|
import { capabilitiesForRoles, rolesFromBackendRoles } from "../contracts/workspace";
|
|
import { readWebRuntimeEnv } from "../schemas/env";
|
|
import { defaultThemeBackgroundId, defaultThemePaletteId, loadThemeState } from "../theme/tokens";
|
|
|
|
const sessionTokenStorageKey = "platform-web.session.apiToken";
|
|
const localFallbackEnabled = readWebRuntimeEnv().enableLocalAuthFallback;
|
|
|
|
export const localFallbackUser: CurrentUserView = {
|
|
id: "local-server-operator",
|
|
displayName: "Local Server Operator",
|
|
email: "local.server@example.test",
|
|
status: "active",
|
|
roles: ["serverAdmin"],
|
|
capabilities: capabilitiesForRoles(["serverAdmin"]),
|
|
profile: {
|
|
phone: "",
|
|
qq: "",
|
|
contactNote: "本地开发回退账号,仅限服务器工作台"
|
|
},
|
|
source: "local"
|
|
};
|
|
|
|
export interface AuthFormState {
|
|
mode: "login" | "register";
|
|
pending: boolean;
|
|
error?: string;
|
|
success?: string;
|
|
}
|
|
|
|
export interface SessionState {
|
|
user?: CurrentUserView;
|
|
loaded: boolean;
|
|
authenticated: boolean;
|
|
authUnavailable: boolean;
|
|
localFallbackAvailable: boolean;
|
|
auth: AuthFormState;
|
|
login: (request: LoginRequest) => Promise<void>;
|
|
register: (request: RegisterRequest) => Promise<void>;
|
|
logout: () => Promise<void>;
|
|
continueWithLocalFallback: () => void;
|
|
switchAuthMode: (mode: AuthFormState["mode"]) => void;
|
|
updateProfile: (request: UserProfileUpdateRequest) => Promise<CurrentUserView>;
|
|
updateThemePreference: (request: UserThemePreferenceRequest) => Promise<UserThemePreferenceResponse>;
|
|
}
|
|
|
|
export async function loadCurrentUser(): Promise<CurrentUserView | null> {
|
|
const storedToken = readStoredSessionToken();
|
|
setPlatformApiSessionToken(storedToken);
|
|
if (!storedToken) {
|
|
return null;
|
|
}
|
|
try {
|
|
const response = await platformApiClient.getCurrentUser();
|
|
return currentUserFromResponse(response, "api");
|
|
} catch {
|
|
persistSessionToken(null);
|
|
setPlatformApiSessionToken(null);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function useSession(): SessionState {
|
|
const [user, setUser] = useState<CurrentUserView | undefined>();
|
|
const [loaded, setLoaded] = useState(false);
|
|
const [authUnavailable, setAuthUnavailable] = useState(false);
|
|
const [auth, setAuth] = useState<AuthFormState>({ mode: "login", pending: false });
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
void loadCurrentUser().then((currentUser) => {
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
setUser(currentUser ?? undefined);
|
|
setAuthUnavailable(!currentUser);
|
|
setLoaded(true);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
async function login(request: LoginRequest) {
|
|
setAuth({ mode: "login", pending: true });
|
|
try {
|
|
const response = await platformApiClient.login(request);
|
|
finishAuth(response, "登录成功");
|
|
} catch (error) {
|
|
setAuth({
|
|
mode: "login",
|
|
pending: false,
|
|
error: error instanceof Error ? error.message : "登录失败,请重试。"
|
|
});
|
|
}
|
|
}
|
|
|
|
async function register(request: RegisterRequest) {
|
|
setAuth({ mode: "register", pending: true });
|
|
try {
|
|
const response = await platformApiClient.register(request);
|
|
const nextUser = currentUserFromResponse(response.user, "api");
|
|
if (response.status === "authenticated") {
|
|
setUser(nextUser);
|
|
persistSessionToken(response.sessionId ?? null);
|
|
setPlatformApiSessionToken(response.sessionId ?? null);
|
|
}
|
|
setAuth({
|
|
mode: response.status === "authenticated" ? "login" : "register",
|
|
pending: false,
|
|
success: response.message ?? (response.status === "authenticated" ? "注册成功,已进入工作台。" : "注册申请已提交,等待平台管理员审核。")
|
|
});
|
|
} catch (error) {
|
|
setAuth({
|
|
mode: "register",
|
|
pending: false,
|
|
error: error instanceof Error ? error.message : "注册失败,请稍后重试。"
|
|
});
|
|
}
|
|
}
|
|
|
|
async function logout() {
|
|
try {
|
|
await platformApiClient.logout();
|
|
} catch {
|
|
// Local fallback logout still clears the visible session.
|
|
}
|
|
persistSessionToken(null);
|
|
setPlatformApiSessionToken(null);
|
|
setUser(undefined);
|
|
setAuthUnavailable(false);
|
|
setAuth({ mode: "login", pending: false, success: "已退出登录。" });
|
|
}
|
|
|
|
function continueWithLocalFallback() {
|
|
if (!localFallbackEnabled) {
|
|
setAuth({ mode: "login", pending: false, error: "本地回退未启用,请使用真实账号登录。" });
|
|
return;
|
|
}
|
|
setUser(localFallbackUser);
|
|
setAuthUnavailable(false);
|
|
setAuth({ mode: "login", pending: false, success: "已进入本地回退工作台。" });
|
|
}
|
|
|
|
function switchAuthMode(mode: AuthFormState["mode"]) {
|
|
setAuth({ mode, pending: false });
|
|
}
|
|
|
|
async function updateProfile(request: UserProfileUpdateRequest): Promise<CurrentUserView> {
|
|
const current = user ?? localFallbackUser;
|
|
try {
|
|
const response = await platformApiClient.updateCurrentUserProfile(request);
|
|
const nextUser = currentUserFromResponse(response, "api");
|
|
setUser(nextUser);
|
|
return nextUser;
|
|
} catch {
|
|
const nextUser: CurrentUserView = {
|
|
...current,
|
|
displayName: request.displayName,
|
|
profile: {
|
|
avatarUrl: request.avatarUrl,
|
|
phone: request.phone,
|
|
qq: request.qq,
|
|
contactNote: request.contactNote
|
|
},
|
|
source: "local"
|
|
};
|
|
setUser(nextUser);
|
|
return nextUser;
|
|
}
|
|
}
|
|
|
|
async function updateThemePreference(request: UserThemePreferenceRequest): Promise<UserThemePreferenceResponse> {
|
|
const current = user ?? localFallbackUser;
|
|
try {
|
|
const response = await platformApiClient.updateCurrentUserTheme(request);
|
|
setUser({ ...current, themePreference: response, source: "api" });
|
|
return response;
|
|
} catch {
|
|
const response: UserThemePreferenceResponse = {
|
|
...request,
|
|
userId: current.id,
|
|
persistence: "local",
|
|
updatedAt: new Date().toISOString()
|
|
};
|
|
const nextUser = { ...current, themePreference: response, source: "local" as const };
|
|
setUser(nextUser);
|
|
return response;
|
|
}
|
|
}
|
|
|
|
function finishAuth(response: AuthSessionResponse, fallbackMessage: string) {
|
|
const nextUser = currentUserFromResponse(response.user, "api");
|
|
setUser(nextUser);
|
|
persistSessionToken(response.sessionId ?? null);
|
|
setPlatformApiSessionToken(response.sessionId ?? null);
|
|
setAuthUnavailable(false);
|
|
setAuth({ mode: "login", pending: false, success: response.message ?? fallbackMessage });
|
|
}
|
|
|
|
return {
|
|
user,
|
|
loaded,
|
|
authenticated: Boolean(user),
|
|
authUnavailable,
|
|
localFallbackAvailable: localFallbackEnabled,
|
|
auth,
|
|
login,
|
|
register,
|
|
logout,
|
|
continueWithLocalFallback,
|
|
switchAuthMode,
|
|
updateProfile,
|
|
updateThemePreference
|
|
};
|
|
}
|
|
|
|
export function currentUserFromResponse(response: CurrentUserResponse, source: CurrentUserView["source"]): CurrentUserView {
|
|
const roles = rolesFromBackendRoles(response.roles);
|
|
const themeState = loadThemeState();
|
|
return {
|
|
id: response.id,
|
|
displayName: response.displayName,
|
|
email: response.email,
|
|
status: response.status ?? "active",
|
|
roles,
|
|
capabilities: response.capabilities?.length ? (response.capabilities as CurrentUserView["capabilities"]) : capabilitiesForRoles(roles),
|
|
profile: response.profile ?? {},
|
|
themePreference:
|
|
response.themePreference ??
|
|
(source === "local"
|
|
? {
|
|
userId: response.id,
|
|
paletteId: themeState.paletteId || defaultThemePaletteId,
|
|
backgroundPresetId: themeState.backgroundPresetId || defaultThemeBackgroundId,
|
|
backgroundImage: themeState.backgroundImage,
|
|
persistence: "local",
|
|
updatedAt: new Date().toISOString()
|
|
}
|
|
: undefined),
|
|
source
|
|
};
|
|
}
|
|
|
|
function readStoredSessionToken(): string | null {
|
|
if (typeof window === "undefined") {
|
|
return null;
|
|
}
|
|
try {
|
|
return window.localStorage.getItem(sessionTokenStorageKey);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function persistSessionToken(token: string | null) {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
try {
|
|
if (token) {
|
|
window.localStorage.setItem(sessionTokenStorageKey, token);
|
|
} else {
|
|
window.localStorage.removeItem(sessionTokenStorageKey);
|
|
}
|
|
} catch {
|
|
// Storage can be unavailable; the in-memory session remains usable.
|
|
}
|
|
}
|