chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { UserManager, WebStorageStateStore, type User } from "oidc-client-ts";
|
||||
import { runtimeConfig } from "../runtimeConfig";
|
||||
|
||||
const origin = window.location.origin;
|
||||
|
||||
export const userManager = new UserManager({
|
||||
authority: runtimeConfig.oidcAuthority,
|
||||
client_id: runtimeConfig.oidcClientId,
|
||||
redirect_uri: `${origin}/auth/callback`,
|
||||
post_logout_redirect_uri: origin,
|
||||
response_type: "code",
|
||||
scope: "openid profile",
|
||||
monitorSession: false,
|
||||
automaticSilentRenew: true,
|
||||
accessTokenExpiringNotificationTimeInSeconds: 60,
|
||||
userStore: new WebStorageStateStore({ store: window.sessionStorage })
|
||||
});
|
||||
|
||||
let callbackPromise: Promise<User | undefined> | undefined;
|
||||
|
||||
type SigninState = {
|
||||
returnTo?: unknown;
|
||||
};
|
||||
|
||||
export function startSignin(): Promise<void> {
|
||||
return userManager.signinRedirect({
|
||||
state: { returnTo: currentRelativeURL() }
|
||||
});
|
||||
}
|
||||
|
||||
export async function completeSigninIfNeeded(): Promise<void> {
|
||||
if (window.location.pathname !== "/auth/callback") return;
|
||||
callbackPromise ??= userManager.signinRedirectCallback();
|
||||
const user = await callbackPromise;
|
||||
const returnURL = safeReturnURL(user?.state);
|
||||
window.history.replaceState({}, document.title, returnURL);
|
||||
window.dispatchEvent(new PopStateEvent("popstate", { state: { returnURL } }));
|
||||
}
|
||||
|
||||
function currentRelativeURL(): string {
|
||||
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
}
|
||||
|
||||
function safeReturnURL(state: unknown): string {
|
||||
const returnTo = (state as SigninState | undefined)?.returnTo;
|
||||
if (typeof returnTo !== "string" || !returnTo.startsWith("/") || returnTo.startsWith("//")) return "/";
|
||||
try {
|
||||
const target = new URL(returnTo, window.location.origin);
|
||||
if (target.origin !== window.location.origin || target.pathname === "/auth/callback") return "/";
|
||||
return `${target.pathname}${target.search}${target.hash}`;
|
||||
} catch {
|
||||
return "/";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { User } from "oidc-client-ts";
|
||||
import { APIRequestError, AUTHENTICATION_REQUIRED_EVENT, platformApi, type CurrentUser } from "../api/client";
|
||||
import { completeSigninIfNeeded, startSignin, userManager } from "./oidc";
|
||||
|
||||
type AuthState = {
|
||||
loading: boolean;
|
||||
oidcUser?: User;
|
||||
currentUser?: CurrentUser;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export function useAuth() {
|
||||
const [state, setState] = useState<AuthState>({ loading: true });
|
||||
|
||||
const loadUser = useCallback(async (user?: User | null) => {
|
||||
const currentOIDCUser = user ?? (await userManager.getUser());
|
||||
if (!currentOIDCUser || currentOIDCUser.expired) {
|
||||
if (currentOIDCUser?.expired) {
|
||||
await userManager.removeUser();
|
||||
setState({ loading: false, error: "登录状态已过期,请重新登录" });
|
||||
} else {
|
||||
setState({ loading: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await platformApi.currentUser(currentOIDCUser.access_token);
|
||||
setState({ loading: false, oidcUser: currentOIDCUser, currentUser: response.data });
|
||||
} catch (error) {
|
||||
if (error instanceof APIRequestError && error.status === 401) {
|
||||
await userManager.removeUser();
|
||||
setState({ loading: false, error: "登录状态已经失效,请重新登录" });
|
||||
return;
|
||||
}
|
||||
setState({
|
||||
loading: false,
|
||||
oidcUser: currentOIDCUser,
|
||||
error: error instanceof Error ? error.message : "无法读取当前用户"
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
let authenticationFailureInProgress = false;
|
||||
const initialize = async () => {
|
||||
try {
|
||||
await completeSigninIfNeeded();
|
||||
if (active) await loadUser();
|
||||
} catch (error) {
|
||||
if (active) {
|
||||
setState({ loading: false, error: error instanceof Error ? error.message : "登录回调处理失败" });
|
||||
}
|
||||
}
|
||||
};
|
||||
const onUserLoaded = (user: User) => { if (active) void loadUser(user); };
|
||||
const onUserUnloaded = () => { if (active) setState({ loading: false }); };
|
||||
const clearInvalidSession = (message: string) => {
|
||||
if (!active || authenticationFailureInProgress) return;
|
||||
authenticationFailureInProgress = true;
|
||||
void userManager.removeUser().finally(() => {
|
||||
authenticationFailureInProgress = false;
|
||||
if (active) setState({ loading: false, error: message });
|
||||
});
|
||||
};
|
||||
const onAccessTokenExpired = () => clearInvalidSession("登录状态已过期,请重新登录");
|
||||
const onSilentRenewError = () => clearInvalidSession("登录续期失败,请重新登录");
|
||||
const onAuthenticationRequired = () => clearInvalidSession("登录状态已经失效,请重新登录");
|
||||
userManager.events.addUserLoaded(onUserLoaded);
|
||||
userManager.events.addUserUnloaded(onUserUnloaded);
|
||||
userManager.events.addAccessTokenExpired(onAccessTokenExpired);
|
||||
userManager.events.addSilentRenewError(onSilentRenewError);
|
||||
window.addEventListener(AUTHENTICATION_REQUIRED_EVENT, onAuthenticationRequired);
|
||||
void initialize();
|
||||
return () => {
|
||||
active = false;
|
||||
userManager.events.removeUserLoaded(onUserLoaded);
|
||||
userManager.events.removeUserUnloaded(onUserUnloaded);
|
||||
userManager.events.removeAccessTokenExpired(onAccessTokenExpired);
|
||||
userManager.events.removeSilentRenewError(onSilentRenewError);
|
||||
window.removeEventListener(AUTHENTICATION_REQUIRED_EVENT, onAuthenticationRequired);
|
||||
};
|
||||
}, [loadUser]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
login: () => startSignin(),
|
||||
logout: () => userManager.signoutRedirect(),
|
||||
retry: () => loadUser(state.oidcUser)
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user