fix(ssf): 修复安全事件连接基础阻断
为已记录旧迁移版本的环境补充幂等表修复迁移,并让 Transmitter 网络连接在 IPv6 地址不可达时继续尝试 IPv4。失败连接现在通过 If-Match 安全重试,页面同步提供重试入口并清理过期错误提示。\n\n验证:Go securityevents/migrate 测试、Web Vitest 与 TypeScript 类型检查通过。
This commit is contained in:
+13
-3
@@ -1033,7 +1033,8 @@ export function App() {
|
||||
setCoreState('loading');
|
||||
setCoreMessage('');
|
||||
try {
|
||||
const connection = await connectSecurityEventTransmitter(token, transmitterIssuer);
|
||||
const version = securityEventConnection?.connection?.version;
|
||||
const connection = await connectSecurityEventTransmitter(token, transmitterIssuer, version);
|
||||
setSecurityEventConnection(connection);
|
||||
setCoreState('ready');
|
||||
setCoreMessage('认证中心安全事件连接正在验证。');
|
||||
@@ -1045,8 +1046,17 @@ export function App() {
|
||||
}
|
||||
|
||||
async function refreshSecurityEvents() {
|
||||
const connection = await getSecurityEventConnection(token);
|
||||
setSecurityEventConnection(connection);
|
||||
setCoreState('loading');
|
||||
setCoreMessage('');
|
||||
try {
|
||||
const connection = await getSecurityEventConnection(token);
|
||||
setSecurityEventConnection(connection);
|
||||
setCoreState('ready');
|
||||
} catch (err) {
|
||||
setCoreState('error');
|
||||
setCoreMessage(err instanceof Error ? err.message : '刷新安全事件连接失败');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function verifySecurityEvents() {
|
||||
|
||||
@@ -55,6 +55,21 @@ describe('security event connection transport', () => {
|
||||
expect(JSON.parse(String(init.body))).toEqual({ transmitter_issuer: 'https://auth.example/ssf' });
|
||||
expect(String(init.body)).not.toMatch(/secret|bearer|scope|credential/i);
|
||||
expect(new Headers(init.headers).get('Idempotency-Key')).toContain('ssf-connect-');
|
||||
expect(new Headers(init.headers).has('If-Match')).toBe(false);
|
||||
});
|
||||
|
||||
it('sends If-Match when retrying an existing failed connection', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ connected: true }), {
|
||||
status: 202,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
vi.stubGlobal('crypto', { randomUUID: () => '00000000-0000-4000-8000-000000000000' });
|
||||
|
||||
await connectSecurityEventTransmitter('manager-token', 'https://auth.example/ssf', 7);
|
||||
|
||||
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(new Headers(init.headers).get('If-Match')).toBe('W/"7"');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+4
-2
@@ -1007,10 +1007,12 @@ export async function getSecurityEventConnection(token: string): Promise<Securit
|
||||
return request<SecurityEventConnectionResponse>(securityEventConnectionPath, { token });
|
||||
}
|
||||
|
||||
export async function connectSecurityEventTransmitter(token: string, transmitterIssuer: string): Promise<SecurityEventConnectionResponse> {
|
||||
export async function connectSecurityEventTransmitter(token: string, transmitterIssuer: string, version?: number): Promise<SecurityEventConnectionResponse> {
|
||||
const headers: Record<string, string> = { 'Idempotency-Key': `ssf-connect-${crypto.randomUUID()}` };
|
||||
if (version !== undefined) headers['If-Match'] = `W/"${version}"`;
|
||||
return request<SecurityEventConnectionResponse>(securityEventConnectionPath, {
|
||||
body: { transmitter_issuer: transmitterIssuer }, method: 'PUT', token,
|
||||
headers: { 'Idempotency-Key': `ssf-connect-${crypto.randomUUID()}` },
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -474,6 +474,9 @@ function SecurityEventConnectionPanel(props: {
|
||||
{connection.lastErrorCategory && <span>最近错误: {connection.lastErrorCategory}</span>}
|
||||
</div>
|
||||
<div className="fileStorageToolbar">
|
||||
{connection.lifecycleStatus === 'error' && (
|
||||
<Button type="button" size="sm" disabled={props.loading} onClick={() => void props.onConnect(connection.transmitterIssuer)}><Link2 size={14} />重试连接</Button>
|
||||
)}
|
||||
<Button type="button" variant="outline" size="sm" disabled={props.loading} onClick={() => void props.onVerify()}><RefreshCw size={14} />重新验证</Button>
|
||||
<Button type="button" variant="outline" size="sm" disabled={props.loading || connection.lifecycleStatus === 'rotating'} onClick={() => void props.onRotate()}><RotateCcw size={14} />轮换接收凭据</Button>
|
||||
<Button type="button" variant="destructive" size="sm" disabled={props.loading || connection.lifecycleStatus === 'retiring'} onClick={props.onDisconnect}><Unplug size={14} />断开连接</Button>
|
||||
|
||||
Reference in New Issue
Block a user