From 9b31aca0c4ab031b0851a8a2d7a18e548ab32c57 Mon Sep 17 00:00:00 2001 From: LZHS Date: Thu, 30 Jul 2026 07:19:21 +0530 Subject: [PATCH 1/3] feat(auth): add dormant egress-bound login v2 --- admin-ui/src/api/credentials.ts | 31 +- admin-ui/src/components/idc-login-dialog.tsx | 66 ++- .../src/components/social-login-dialog.tsx | 91 +++- admin-ui/src/types/api.ts | 22 +- src/admin/handlers.rs | 38 +- src/admin/router.rs | 4 +- src/admin/service.rs | 453 +++++++++++++++--- src/admin/types.rs | 50 +- src/kiro/egress_login.rs | 174 +++++++ src/kiro/mod.rs | 1 + src/kiro/model/credentials.rs | 32 ++ src/kiro/token_manager.rs | 30 +- 12 files changed, 879 insertions(+), 113 deletions(-) create mode 100644 src/kiro/egress_login.rs diff --git a/admin-ui/src/api/credentials.ts b/admin-ui/src/api/credentials.ts index 7e8c0099..9ba8d9c7 100644 --- a/admin-ui/src/api/credentials.ts +++ b/admin-ui/src/api/credentials.ts @@ -24,6 +24,7 @@ import type { ProxyCheckAllResponse, AssignRoundRobinResponse, StartIdcLoginRequest, + EgressLoginCapabilityResponse, StartIdcLoginResponse, PollIdcLoginResponse, StartSocialLoginRequest, @@ -555,6 +556,11 @@ export async function setLogGovernanceConfig( } // 发起 IdC 设备授权登录 +export async function getEgressLoginCapability(): Promise { + const { data } = await api.get('/auth/egress-login-v2') + return data +} + export async function startIdcLogin( req: StartIdcLoginRequest ): Promise { @@ -563,8 +569,13 @@ export async function startIdcLogin( } // 轮询 IdC 登录状态 -export async function pollIdcLogin(sessionId: string): Promise { - const { data } = await api.post(`/auth/idc/poll/${sessionId}`) +export async function pollIdcLogin( + sessionId: string, + sessionNonce?: string, +): Promise { + const { data } = await api.post(`/auth/idc/poll/${sessionId}`, undefined, { + headers: sessionNonce ? { 'x-kiro-egress-nonce': sessionNonce } : undefined, + }) return data } @@ -645,17 +656,25 @@ export async function startSocialLogin( } // 轮询 Social 登录状态 -export async function pollSocialLogin(sessionId: string): Promise { - const { data } = await api.post(`/auth/social/poll/${sessionId}`) +export async function pollSocialLogin( + sessionId: string, + sessionNonce?: string, +): Promise { + const { data } = await api.post(`/auth/social/poll/${sessionId}`, undefined, { + headers: sessionNonce ? { 'x-kiro-egress-nonce': sessionNonce } : undefined, + }) return data } // 手动完成 Social 登录(远程访问时粘贴回调 URL) export async function completeSocialLogin( sessionId: string, - req: CompleteSocialLoginRequest + req: CompleteSocialLoginRequest, + sessionNonce?: string, ): Promise { - const { data } = await api.post(`/auth/social/complete/${sessionId}`, req) + const { data } = await api.post(`/auth/social/complete/${sessionId}`, req, { + headers: sessionNonce ? { 'x-kiro-egress-nonce': sessionNonce } : undefined, + }) return data } diff --git a/admin-ui/src/components/idc-login-dialog.tsx b/admin-ui/src/components/idc-login-dialog.tsx index 4304e26c..59a93323 100644 --- a/admin-ui/src/components/idc-login-dialog.tsx +++ b/admin-ui/src/components/idc-login-dialog.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useRef } from 'react' import { toast } from 'sonner' +import { useQuery } from '@tanstack/react-query' import { ExternalLink, Copy, Loader2, CheckCircle, Check } from 'lucide-react' import { Dialog, @@ -20,7 +21,7 @@ import { } from '@/components/ui/select' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' -import { startIdcLogin, pollIdcLogin } from '@/api/credentials' +import { startIdcLogin, pollIdcLogin, getProxyPool, getEgressLoginCapability } from '@/api/credentials' import type { StartIdcLoginResponse } from '@/types/api' import { extractErrorMessage } from '@/lib/utils' @@ -139,7 +140,21 @@ export function IdcLoginDialog({ open, onOpenChange, onSuccess, mode = 'builder- const [isStarting, setIsStarting] = useState(false) const [session, setSession] = useState(null) const [credentialId, setCredentialId] = useState(null) + const [proxyId, setProxyId] = useState('') + const sessionNonceRef = useRef(undefined) const pollTimerRef = useRef | null>(null) + const { data: proxyPool } = useQuery({ + queryKey: ['proxy-pool'], + queryFn: getProxyPool, + enabled: open, + }) + const { data: egressCapability } = useQuery({ + queryKey: ['egress-login-v2'], + queryFn: getEgressLoginCapability, + enabled: open, + }) + const egressEnabled = egressCapability?.enabled === true + const enabledProxies = proxyPool?.proxies.filter((proxy) => proxy.enabled) ?? [] // 清理轮询定时器 useEffect(() => { @@ -157,6 +172,8 @@ export function IdcLoginDialog({ open, onOpenChange, onSuccess, mode = 'builder- setCredentialId(null) setIsStarting(false) setLinkCopied(false) + setProxyId('') + sessionNonceRef.current = undefined } onOpenChange(v) } @@ -189,13 +206,15 @@ export function IdcLoginDialog({ open, onOpenChange, onSuccess, mode = 'builder- region: region.trim(), startUrl: startUrl.trim() || undefined, email: email.trim() || undefined, + proxyId: egressEnabled && proxyId ? Number(proxyId) : undefined, }) + sessionNonceRef.current = resp.sessionNonce setSession(resp) setStep('waiting') if (incognito) { await copyVerificationUrl(resp) } - schedulePoll(resp.sessionId, resp.pollInterval) + schedulePoll(resp.sessionId, resp.pollInterval, resp.sessionNonce) } catch (e) { toast.error('发起登录失败:' + extractErrorMessage(e)) } finally { @@ -203,12 +222,19 @@ export function IdcLoginDialog({ open, onOpenChange, onSuccess, mode = 'builder- } } - const schedulePoll = (sessionId: string, interval: number) => { + const schedulePoll = (sessionId: string, interval: number, sessionNonce?: string) => { pollTimerRef.current = setTimeout(async () => { try { - const result = await pollIdcLogin(sessionId) + const result = await pollIdcLogin(sessionId, sessionNonce) + const nextNonce = result.status === 'pending' || result.status === 'continue' + ? (result.sessionNonce ?? sessionNonce) + : undefined + sessionNonceRef.current = nextNonce + setSession((current) => current && current.sessionId === sessionId + ? { ...current, sessionNonce: nextNonce } + : current) if (result.status === 'pending') { - schedulePoll(sessionId, interval) + schedulePoll(sessionId, interval, nextNonce) } else if (result.status === 'success') { setCredentialId(result.credentialId) setStep('done') @@ -221,7 +247,13 @@ export function IdcLoginDialog({ open, onOpenChange, onSuccess, mode = 'builder- } } catch (e) { toast.error('轮询状态失败:' + extractErrorMessage(e)) - schedulePoll(sessionId, interval) + if (sessionNonce) { + setStep('form') + setSession(null) + sessionNonceRef.current = undefined + } else { + schedulePoll(sessionId, interval) + } } }, interval * 1000) } @@ -298,6 +330,24 @@ export function IdcLoginDialog({ open, onOpenChange, onSuccess, mode = 'builder- )} + {step === 'form' && egressEnabled && ( +
+ + +
+ )} + {step === 'form' && (