fix(web): 避免刷新时闪现未登录页面

统一认证会话需要异步恢复,首次渲染不能把尚未确认的状态当作未登录。新增 checking、authenticated、unauthenticated 三态鉴权,在检查期间显示中性加载状态并隐藏登录相关操作。

验证:前端 156 项测试通过;TypeScript 类型检查、lint、生产构建和浏览器刷新验收通过。
This commit is contained in:
2026-08-03 15:55:49 +08:00
parent b56769512f
commit 3c8d9839a4
6 changed files with 176 additions and 50 deletions
+51 -47
View File
@@ -119,6 +119,7 @@ import {
verifySecurityEventConnection,
} from './api';
import type { ConsoleData, StatItem } from './app-state';
import { AuthGate, initialAuthenticationStatus } from './components/AuthGate';
import { AppShell } from './components/layout/AppShell';
import { canAccessAdminWorkspace } from './auth-permissions';
import { LoginRequiredPanel } from './components/LoginRequiredPanel';
@@ -228,7 +229,9 @@ export function App() {
const [workspaceTransactionQuery, setWorkspaceTransactionQuery] = useState<WorkspaceTransactionQuery>(() => defaultWorkspaceTransactionQuery());
const [apiDocSection, setApiDocSection] = useState<ApiDocSection>(initialRoute.apiDocSection);
const [playgroundMode, setPlaygroundMode] = useState<PlaygroundMode>(initialRoute.playgroundMode);
const [token, setToken] = useState(readStoredAccessToken);
const [initialAccessToken] = useState(readStoredAccessToken);
const [token, setToken] = useState(initialAccessToken);
const [authenticationStatus, setAuthenticationStatus] = useState(() => initialAuthenticationStatus(initialAccessToken));
const [externalToken, setExternalToken] = useState('');
const [authMode, setAuthMode] = useState<AuthMode>('login');
const [loginForm, setLoginForm] = useState<LoginForm>({ account: '', password: '' });
@@ -337,6 +340,7 @@ export function App() {
setCurrentUser(restored.user);
loadedDataKeysRef.current.add('currentUser');
setToken(restored.credential);
setAuthenticationStatus('authenticated');
setState('idle');
setError('');
};
@@ -356,13 +360,22 @@ export function App() {
});
return;
}
if (oidcCallbackError || readStoredAccessToken() || !identityEnabled) return;
const storedAccessToken = readStoredAccessToken();
if (oidcCallbackError || storedAccessToken || !identityEnabled) {
if (!storedAccessToken) setAuthenticationStatus('unauthenticated');
return;
}
const restored = await restoreOIDCBrowserSession();
if (!restored || cancelled) return;
if (cancelled) return;
if (!restored) {
setAuthenticationStatus('unauthenticated');
return;
}
applyRestoredSession(restored);
};
const handleLoadError = (err: unknown) => {
if (cancelled) return;
setAuthenticationStatus('unauthenticated');
setState('error');
setError(err instanceof Error ? err.message : '统一认证登录失败');
};
@@ -691,6 +704,7 @@ export function App() {
}
persistAccessToken(nextToken);
setToken(nextToken);
setAuthenticationStatus('authenticated');
await ensureRouteData(nextToken, true);
}
@@ -701,6 +715,7 @@ export function App() {
const response = await request();
persistAccessToken(response.accessToken);
setToken(response.accessToken);
setAuthenticationStatus('authenticated');
if (activePage === 'login') {
navigatePath(pathForWorkspaceSection('overview'));
return;
@@ -1298,6 +1313,7 @@ export function App() {
function resetAuthenticatedSession() {
persistAccessToken('');
setToken('');
setAuthenticationStatus('unauthenticated');
loadedDataKeysRef.current = new Set(health ? ['health'] : []);
loadingDataKeysRef.current.clear();
setState('idle');
@@ -1466,14 +1482,32 @@ export function App() {
navigatePath(pathForPlaygroundMode('chat'));
}
const isAuthenticated = Boolean(token);
const isAuthenticated = authenticationStatus === 'authenticated' && Boolean(token);
const loginRequiredPanel = (
<LoginRequiredPanel
authMode={authMode}
externalToken={externalToken}
loginForm={loginForm}
registerForm={registerForm}
state={state}
onAuthModeChange={setAuthMode}
onExternalTokenChange={setExternalToken}
onLoginChange={setLoginForm}
onRegisterChange={setRegisterForm}
onSubmitExternalToken={submitExternalToken}
onSubmitLogin={submitLogin}
onSubmitRegister={submitRegister}
oidcEnabled={oidcEnabled}
onOIDCLogin={loginWithOIDC}
/>
);
return (
<AppShell
activePage={activePage}
authenticationStatus={authenticationStatus}
canAccessAdmin={canAccessAdminWorkspace(currentUser)}
health={health}
isAuthenticated={isAuthenticated}
state={state}
onNavigate={navigatePage}
onLogin={showLogin}
@@ -1510,8 +1544,9 @@ export function App() {
/>
)}
{activePage === 'workspace' && (
isAuthenticated ? (
<WorkspacePage
<AuthGate
status={authenticationStatus}
authenticated={<WorkspacePage
apiKeyForm={apiKeyForm}
apiKeySecret={apiKeySecret}
apiKeySecretsById={apiKeySecretsById}
@@ -1534,29 +1569,14 @@ export function App() {
onTaskQueryChange={navigateWorkspaceTaskQuery}
onTransactionQueryChange={setWorkspaceTransactionQuery}
onUseApiKeyForPlayground={useApiKeyForPlayground}
/>
) : (
<LoginRequiredPanel
authMode={authMode}
externalToken={externalToken}
loginForm={loginForm}
registerForm={registerForm}
state={state}
onAuthModeChange={setAuthMode}
onExternalTokenChange={setExternalToken}
onLoginChange={setLoginForm}
onRegisterChange={setRegisterForm}
onSubmitExternalToken={submitExternalToken}
onSubmitLogin={submitLogin}
onSubmitRegister={submitRegister}
oidcEnabled={oidcEnabled}
onOIDCLogin={loginWithOIDC}
/>
)
/>}
unauthenticated={loginRequiredPanel}
/>
)}
{activePage === 'admin' && (
isAuthenticated ? (
<AdminPage
<AuthGate
status={authenticationStatus}
authenticated={<AdminPage
token={token}
adminTaskQuery={adminTaskQuery}
adminTaskTotal={adminTaskTotal}
@@ -1608,25 +1628,9 @@ export function App() {
onAdminTaskQueryChange={navigateAdminTaskQuery}
onRefreshAdminTasks={refreshAdminTasks}
onSectionChange={navigateAdminSection}
/>
) : (
<LoginRequiredPanel
authMode={authMode}
externalToken={externalToken}
loginForm={loginForm}
registerForm={registerForm}
state={state}
onAuthModeChange={setAuthMode}
onExternalTokenChange={setExternalToken}
onLoginChange={setLoginForm}
onRegisterChange={setRegisterForm}
onSubmitExternalToken={submitExternalToken}
onSubmitLogin={submitLogin}
onSubmitRegister={submitRegister}
oidcEnabled={oidcEnabled}
onOIDCLogin={loginWithOIDC}
/>
)
/>}
unauthenticated={loginRequiredPanel}
/>
)}
{activePage === 'docs' && (
<ApiDocsPage