Files

298 lines
9.4 KiB
TypeScript

import { useEffect, useState } from "react";
import { platformApiClient, setPlatformApiAuthFailureHandler, 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;
let currentUserLoadPromise: Promise<CurrentUserView | null> | null = null;
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> {
if (currentUserLoadPromise) return currentUserLoadPromise;
currentUserLoadPromise = loadCurrentUserOnce().finally(() => {
currentUserLoadPromise = null;
});
return currentUserLoadPromise;
}
async function loadCurrentUserOnce(): Promise<CurrentUserView | null> {
const storedToken = readStoredSessionToken();
setPlatformApiSessionToken(storedToken);
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;
};
}, []);
useEffect(() => {
setPlatformApiAuthFailureHandler(() => {
persistSessionToken(null);
setUser(undefined);
setAuthUnavailable(false);
setAuth({ mode: "login", pending: false, error: "会话已失效,请重新登录。" });
});
return () => setPlatformApiAuthFailureHandler(null);
}, []);
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.
}
}