fix(web): 完善统一认证恢复与重配交互

为处理中、失败和清理中的配对展示明确状态与恢复动作,支持放弃并清理、作用域内退役冲突连接,并在 Active 状态引导管理员先安全禁用再使用新接入码。同步修复运行时切换后的浏览器会话残留与确认弹窗焦点约束。

验证:pnpm test;pnpm lint;pnpm build。
This commit is contained in:
2026-07-17 18:31:38 +08:00
parent a312ad880d
commit b179162330
9 changed files with 696 additions and 98 deletions
+45 -15
View File
@@ -122,7 +122,10 @@ import {
persistAccessToken,
readStoredAccessToken,
} from './lib/auth-storage';
import { restoreOIDCBrowserSession } from './lib/oidc-browser-session';
import {
reconcileOIDCBrowserSessionAfterRuntimeChange,
restoreOIDCBrowserSession,
} from './lib/oidc-browser-session';
import {
consumeOIDCCallbackError,
loadOIDCRuntimeConfiguration,
@@ -260,7 +263,15 @@ export function App() {
const [state, setState] = useState<LoadState>('idle');
const [error, setError] = useState('');
const [oidcCallbackError, setOIDCCallbackError] = useState(() => consumeOIDCCallbackError());
const [oidcEnabled, setOIDCEnabled] = useState(false);
const [oidcEnabled, setOIDCEnabled] = useState(false);
const currentCredentialRef = useRef(token);
const resetAuthenticatedSessionRef = useRef<() => void>(() => undefined);
currentCredentialRef.current = token;
resetAuthenticatedSessionRef.current = () => {
resetAuthenticatedSession();
setAuthMode('login');
setError('统一认证配置已变更,当前登录会话已失效,请重新登录。');
};
const loadedDataKeysRef = useRef(new Set<DataKey>());
const loadingDataKeysRef = useRef(new Set<DataKey>());
const loadedTaskQueryKeyRef = useRef('');
@@ -294,29 +305,48 @@ export function App() {
useEffect(() => {
let cancelled = false;
const loadIdentityRuntime = async (force = false) => {
const configuration = await loadOIDCRuntimeConfiguration(force);
if (cancelled) return;
setOIDCEnabled(configuration.enabled && configuration.oidcLogin);
if (oidcCallbackError || readStoredAccessToken() || !configuration.enabled || !configuration.oidcLogin) return;
const restored = await restoreOIDCBrowserSession();
if (!restored || cancelled) return;
const applyRestoredSession = (restored: NonNullable<Awaited<ReturnType<typeof restoreOIDCBrowserSession>>>) => {
if (cancelled) return;
setCurrentUser(restored.user);
loadedDataKeysRef.current.add('currentUser');
setToken(restored.credential);
setState('idle');
setError('');
};
const runtimeChanged = () => { void loadIdentityRuntime(true); };
window.addEventListener('identity-runtime-changed', runtimeChanged);
void loadIdentityRuntime().catch((err) => {
};
const loadIdentityRuntime = async (force = false) => {
const configuration = await loadOIDCRuntimeConfiguration(force);
if (cancelled) return;
const identityEnabled = configuration.enabled && configuration.oidcLogin;
setOIDCEnabled(identityEnabled);
if (force && currentCredentialRef.current === OIDC_BROWSER_SESSION_CREDENTIAL) {
await reconcileOIDCBrowserSessionAfterRuntimeChange({
credential: currentCredentialRef.current,
oidcEnabled: identityEnabled,
onReset: () => {
if (!cancelled) resetAuthenticatedSessionRef.current();
},
onRestored: applyRestoredSession,
});
return;
}
if (oidcCallbackError || readStoredAccessToken() || !identityEnabled) return;
const restored = await restoreOIDCBrowserSession();
if (!restored || cancelled) return;
applyRestoredSession(restored);
};
const handleLoadError = (err: unknown) => {
if (cancelled) return;
setState('error');
setError(err instanceof Error ? err.message : '统一认证登录失败');
});
};
const runtimeChanged = () => {
void loadIdentityRuntime(true).catch(handleLoadError);
};
window.addEventListener('identity-runtime-changed', runtimeChanged);
void loadIdentityRuntime().catch(handleLoadError);
return () => {
cancelled = true;
window.removeEventListener('identity-runtime-changed', runtimeChanged);
window.removeEventListener('identity-runtime-changed', runtimeChanged);
};
}, [oidcCallbackError]);
useEffect(() => {