fix(web): 避免刷新时闪现未登录页面
统一认证会话需要异步恢复,首次渲染不能把尚未确认的状态当作未登录。新增 checking、authenticated、unauthenticated 三态鉴权,在检查期间显示中性加载状态并隐藏登录相关操作。 验证:前端 156 项测试通过;TypeScript 类型检查、lint、生产构建和浏览器刷新验收通过。
This commit is contained in:
+51
-47
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user