import { useEffect, useRef, useState, type FormEvent } from 'react'; import { CheckCircle2, Link2, RefreshCw, ShieldCheck, Unplug } from 'lucide-react'; import type { IdentityConfigurationRevision, IdentityConfigurationView, IdentityPairingInput, } from '@easyai-ai-gateway/contracts'; import { activateIdentityRevision, cancelIdentityPairing, disableIdentityConfiguration, GatewayApiError, getIdentityConfiguration, retireIdentityPairingSecurityEventConflict, startIdentityPairing, updateIdentityRevisionPolicy, validateIdentityRevision, } from '../../api'; import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmDialog, Input, Label } from '../../components/ui'; const terminalPairingStatuses = new Set(['completed', 'failed', 'expired', 'cancelled']); export function UnifiedIdentityPanel(props: { token: string }) { const [configuration, setConfiguration] = useState(null); const [form, setForm] = useState(defaultPairingInput); const [showPairingForm, setShowPairingForm] = useState(false); const [localTenantKey, setLocalTenantKey] = useState('default'); const [legacyJwtEnabled, setLegacyJwtEnabled] = useState(false); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(''); const [error, setError] = useState(''); const [pollError, setPollError] = useState(''); const [confirmAction, setConfirmAction] = useState<'disable' | 'cancelPairing' | 'retireSecurityEvent' | null>(null); const operationInProgress = useRef(false); async function refresh() { const next = await getIdentityConfiguration(props.token); setConfiguration(next); setPollError(''); if (next.draft) { setLocalTenantKey(next.draft.localTenantKey); setLegacyJwtEnabled(next.draft.legacyJwtEnabled); } return next; } useEffect(() => { void refresh().catch((caught) => setError(errorMessage(caught, '统一认证配置加载失败'))); }, [props.token]); useEffect(() => { const status = configuration?.pairing?.status; const cleanupPending = status === 'cancelled' && configuration?.pairing?.cleanupStatus === 'pending'; if (!status || (terminalPairingStatuses.has(status) && !cleanupPending)) return undefined; const timer = window.setInterval(() => { void refresh().catch(() => setPollError('统一认证状态自动刷新失败,请点击“刷新”重试。')); }, 2000); return () => window.clearInterval(timer); }, [configuration?.pairing?.cleanupStatus, configuration?.pairing?.status, props.token]); async function run(action: () => Promise, success: string, runtimeChanged = false) { if (operationInProgress.current) return false; operationInProgress.current = true; setLoading(true); setError(''); setMessage(''); try { return await executeIdentityOperation({ action, onRuntimeChanged: () => window.dispatchEvent(new Event('identity-runtime-changed')), onSuccess: setMessage, refresh, runtimeChanged, success, }); } catch (caught) { try { await refresh(); } catch { // Preserve the original operation error; the manual refresh remains available. } setError(errorMessage(caught, '统一认证操作失败')); if (isStaleIdentityOperation(caught)) setConfirmAction(null); return false; } finally { operationInProgress.current = false; setLoading(false); } } async function submitPairing(event: FormEvent) { event.preventDefault(); const input = { ...form }; setForm((current) => ({ ...current, onboardingCode: '' })); await run(async () => { await startIdentityPairing(props.token, input); setShowPairingForm(false); }, '接入码已领取,正在由 Gateway 后端准备并保存配置。'); } async function saveDraftPolicy() { const draft = configuration?.draft; if (!draft) return; await run(() => updateIdentityRevisionPolicy(props.token, draft.id, draft.version, { localTenantKey: localTenantKey.trim(), legacyJwtEnabled, }), 'Gateway 本地认证策略已保存。'); } async function retireConflictingSecurityEventConnection() { const pairing = configuration?.pairing; if (!pairing) return false; return run(async () => { await retireIdentityPairingSecurityEventConflict(props.token, pairing.id, pairing.version); }, '现有安全事件连接已进入安全退役;退役完成后本次配对会自动继续。'); } const active = configuration?.active; const draft = configuration?.draft; const previous = configuration?.previous; const pairing = configuration?.pairing; const runtime = configuration?.runtime; const cleanupPending = pairing?.status === 'cancelled' && pairing.cleanupStatus === 'pending'; const cleanupCompleted = pairing?.status === 'cancelled' && pairing.cleanupStatus === 'completed'; const shouldShowPairing = !active && (showPairingForm || !pairing || cleanupCompleted); return (
统一认证 使用认证中心生成的一次性接入码完成标准应用配对;Gateway 自动配置 OIDC、Introspection 与可选 SSF。
{active ? '已启用' : '未启用'}
{(message || error || pollError) &&
{error || pollError || message}
} {active && runtime && (
当前生效配置

Revision {shortID(active.id)} · {active.issuer}

Active
setConfirmAction('disable')} onValidate={() => void run(() => validateIdentityRevision(props.token, active.id, active.version), '当前配置重新验证成功。', true)} />
)} {pairing && (!terminalPairingStatuses.has(pairing.status) || cleanupPending) && ( setConfirmAction('cancelPairing')} onResolveConnectionConflict={() => setConfirmAction('retireSecurityEvent')} /> )} {draft && pairing?.status === 'completed' && (
待启用配置

机器凭据已写入 SecretStore,页面不会显示或保存明文。

{draft.state}
{draft.state === 'draft' && (
)}
{draft.state === 'draft' && <> } {draft.state === 'validated' && ( )} {draft.state === 'failed' && 验证失败:{draft.lastErrorCategory || 'validation_failed'}。请检查地址和租户后使用新接入码重新配对。}
)} {pairing && terminalPairingStatuses.has(pairing.status) && pairing.status !== 'completed' && !cleanupPending && (
{cleanupCompleted ? '旧配对已放弃,临时凭据与本次创建的 SSF 连接已清理。' : `配对${pairing.status === 'expired' ? '已过期' : '失败'}:${pairingFailureMessage(pairing.lastErrorCategory)}。`} {cleanupCompleted ? : }
)} {shouldShowPairing && (
接入认证中心

认证中心保持通用 Application 模型;这里仅消费标准 Manifest 和一次性机器凭据。

接入码 10 分钟有效
void submitPairing(event)}>
)} setConfirmAction(null)} onConfirm={async () => { let succeeded = false; if (confirmAction === 'disable' && active) { succeeded = await run(() => disableIdentityConfiguration(props.token, active.version), '统一认证已禁用,本地管理登录继续可用。', true); } else if (confirmAction === 'cancelPairing' && pairing) { succeeded = await run(() => cancelIdentityPairing(props.token, pairing.id, pairing.version), '已放弃当前草稿,后台正在安全清理临时资源。'); } else if (confirmAction === 'retireSecurityEvent') { succeeded = await retireConflictingSecurityEventConnection(); } if (succeeded) setConfirmAction(null); }} />
); } export function PairingProgressCard({ pairing, loading, onCancel, onResolveConnectionConflict }: { pairing: NonNullable; loading: boolean; onCancel: () => void; onResolveConnectionConflict?: () => void; }) { const cleaning = pairing.status === 'cancelled' && pairing.cleanupStatus === 'pending'; const connectionConflict = pairing.lastErrorCategory === 'security_event_connection_conflict'; const unsafeCredentialHandoff = pairing.lastErrorCategory === 'security_event_credential_handoff_unsafe'; return (
{cleaning ? '正在清理已放弃的配对' : '正在配对认证中心'}{pairingStatusLabel(pairing.status, pairing.cleanupStatus)}
{cleaning ? 'cleanup_pending' : pairing.status}
{pairing.lastErrorCategory && (
{pairingFailureMessage(pairing.lastErrorCategory)}(错误分类:{safePairingCategory(pairing.lastErrorCategory)})。{cleaning ? '后台会继续安全清理。' : connectionConflict ? '此问题不会通过自动重试恢复;请安全退役现有连接,或放弃本次配对。' : unsafeCredentialHandoff ? '此问题不会自动恢复;请先在原配置下断开旧连接,或放弃本次配对。' : '后台正在自动重试;也可以放弃本次配对后重新配置。'}
)}
Exchange: {shortID(pairing.remoteExchangeId)} 有效期至: {new Date(pairing.expiresAt).toLocaleString()} {pairing.lastTraceId && Trace ID: {pairing.lastTraceId}} {pairing.authCenterAuditId && Auth Center Audit ID: {pairing.authCenterAuditId}}
{!cleaning &&
{connectionConflict && }
}
); } export function IdentityRuntimeActions({ loading, onDisable, onValidate, previous, }: { loading: boolean; onDisable: () => void; onValidate: () => void; previous: IdentityConfigurationRevision | null; }) { return (
当前 Active 关联认证中心的 OAuth/SSF 资源;请先禁用统一认证,再使用新接入码重新配置。 {previous && ( 旧版本的远端 OAuth/SSF 资源可能已经变化,不能直接回滚;请使用新的接入码恢复。 )}
); } function RevisionDetails({ revision }: { revision: IdentityConfigurationRevision }) { return (
Issuer: {revision.issuer || '等待 Manifest'} Audience: {revision.audience || '等待 Manifest'} Tenant: {revision.tenantId || '等待 Manifest'} 本地租户: {revision.localTenantKey} 登录 Client: {revision.browserClientId || '未启用'} 服务 Client: {revision.machineClientId || '未启用'} 能力: {revision.capabilities.map(capabilityLabel).join('、') || '等待 Manifest'} Scope: {revision.scopes.join(', ') || '无'} {revision.lastTraceId && Trace ID: {revision.lastTraceId}} {revision.lastAuditId && Audit ID: {revision.lastAuditId}}
); } function IdentityHealth({ label, value }: { label: string; value: string }) { const healthy = value === 'healthy' || value === 'push_healthy'; return
{label}{healthLabel(value)}
; } function defaultPairingInput(): IdentityPairingInput { const webBaseUrl = typeof window === 'undefined' ? '' : window.location.origin; const configuredApiBaseUrl = (import.meta.env.VITE_GATEWAY_API_BASE_URL ?? '').replace(/\/$/, ''); return { authCenterUrl: 'https://auth.51easyai.com', onboardingCode: '', publicBaseUrl: /^https?:\/\//i.test(configuredApiBaseUrl) ? configuredApiBaseUrl : '', webBaseUrl, localTenantKey: 'default', legacyJwtEnabled: false, }; } function pairingStatusLabel(status: string, cleanupStatus?: string) { if (status === 'cancelled' && cleanupStatus === 'pending') return '正在销毁临时凭据并清理由本次 Revision 创建的安全事件连接'; return ({ metadata_pending: '正在提交回调与 Receiver 地址', preparing: '认证中心正在创建或复用标准资源', ready: '正在领取并保存一次性机器凭据', credentials_saved: '正在建立安全事件能力并确认完成' } as Record)[status] ?? status; } const pairingFailureMessages: Readonly> = { security_event_configuration_invalid: '安全事件配置不完整或无效', security_event_discovery_failed: '无法获取或校验 SSF Discovery(网络、HTTP、JSON、Issuer 或端点异常)', security_event_connection_conflict: '已有安全事件连接与本次接入配置冲突', security_event_credential_handoff_unsafe: '旧安全事件连接与本次认证中心或服务 Client 不匹配;系统不会发送旧凭据,请先在原配置下断开旧连接', security_event_connection_binding_missing: '安全事件连接尚未建立', security_event_connection_binding_unavailable: '安全事件连接状态暂时不可用', security_event_connection_binding_invalid: '安全事件连接缺少 Revision 绑定信息', security_event_connection_binding_mismatch: '认证中心返回的安全事件 Audience 或连接归属与本次配置不一致', security_event_retirement_pending: '现有安全事件连接正在安全退役,完成后本次配对会自动继续', security_event_management_token_failed: '服务客户端暂时无法取得安全事件管理 Token', security_event_stream_create_failed: '安全事件 Stream 创建失败', security_event_stream_response_invalid: '安全事件 Stream 返回内容不符合预期', security_event_receiver_activation_failed: 'Gateway 安全事件 Receiver 启动失败', security_event_preparation_failed: '安全事件能力准备失败', metadata_submission_failed: '业务地址提交失败', exchange_status_unavailable: '认证中心资源准备状态暂时不可用', credential_delivery_failed: '一次性机器凭据领取或保存失败', exchange_expired: '一次性接入 Exchange 已过期', remote_exchange_failed: '认证中心未能完成应用资源准备,请结合 Audit ID 排查', remote_state_invalid: '认证中心返回了不符合当前流程的 Exchange 状态', exchange_completion_failed: 'Gateway 暂时无法确认认证中心 Exchange 已完成', cleanup_revision_unavailable: '待清理的统一认证草稿暂时不可用', cleanup_security_event_unavailable: '安全事件清理服务暂时不可用', cleanup_security_event_retirement_pending: '安全事件 Stream 已断开,正在等待安全退役窗口结束', cleanup_security_event_connection_cleanup_failed: '安全事件连接清理暂时失败', cleanup_security_event_secret_cleanup_failed: '安全事件临时凭据清理暂时失败', cleanup_security_event_failed: '安全事件连接清理暂时失败', cleanup_secret_store_failed: '统一认证临时凭据清理暂时失败', cleanup_finalize_failed: '清理结果暂时无法确认', }; export function pairingFailureMessage(category?: string) { return pairingFailureMessages[category ?? ''] ?? '统一认证配对步骤暂时失败'; } function safePairingCategory(category: string) { return Object.hasOwn(pairingFailureMessages, category) ? category : 'pairing_step_failed'; } export function isStaleIdentityOperation(caught: unknown) { return caught instanceof GatewayApiError && (caught.details.status === 409 || caught.details.status === 412); } export async function executeIdentityOperation(options: { action: () => Promise; onRuntimeChanged: () => void; onSuccess: (message: string) => void; refresh: () => Promise; runtimeChanged: boolean; success: string; }) { await options.action(); try { await options.refresh(); } catch (caught) { if (!options.runtimeChanged) throw caught; const followUp = isIdentityRefreshAuthenticationLoss(caught) ? '当前统一认证会话已失效,请重新登录。' : '最新状态刷新失败,请稍后点击“刷新”确认。'; options.onSuccess(`${options.success} 操作已提交;${followUp}`); options.onRuntimeChanged(); return true; } options.onSuccess(options.success); if (options.runtimeChanged) options.onRuntimeChanged(); return true; } function isIdentityRefreshAuthenticationLoss(caught: unknown) { return caught instanceof GatewayApiError && caught.details.status === 401; } function capabilityLabel(value: string) { return ({ oidc_login: 'OIDC 登录', api_access: 'API 验证', token_introspection: 'Token Introspection', machine_to_machine: '机器调用', session_revocation: 'SSF 会话撤销' } as Record)[value] ?? value; } function healthLabel(value: string) { return ({ healthy: '健康', disabled: '未启用', push_healthy: 'Push 健康', introspection_fallback: 'Introspection 降级', bootstrap: '启动保护' } as Record)[value] ?? value; } function shortID(value: string) { return value ? `${value.slice(0, 8)}…` : '-'; } function errorMessage(caught: unknown, fallback: string) { return caught instanceof Error ? caught.message : fallback; }