first commit
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
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.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user