Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions admin-ui/src/api/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
ProxyCheckAllResponse,
AssignRoundRobinResponse,
StartIdcLoginRequest,
EgressLoginCapabilityResponse,
StartIdcLoginResponse,
PollIdcLoginResponse,
StartSocialLoginRequest,
Expand Down Expand Up @@ -555,6 +556,11 @@ export async function setLogGovernanceConfig(
}

// 发起 IdC 设备授权登录
export async function getEgressLoginCapability(): Promise<EgressLoginCapabilityResponse> {
const { data } = await api.get<EgressLoginCapabilityResponse>('/auth/egress-login-v2')
return data
}

export async function startIdcLogin(
req: StartIdcLoginRequest
): Promise<StartIdcLoginResponse> {
Expand All @@ -563,8 +569,13 @@ export async function startIdcLogin(
}

// 轮询 IdC 登录状态
export async function pollIdcLogin(sessionId: string): Promise<PollIdcLoginResponse> {
const { data } = await api.post<PollIdcLoginResponse>(`/auth/idc/poll/${sessionId}`)
export async function pollIdcLogin(
sessionId: string,
sessionNonce?: string,
): Promise<PollIdcLoginResponse> {
const { data } = await api.post<PollIdcLoginResponse>(`/auth/idc/poll/${sessionId}`, undefined, {
headers: sessionNonce ? { 'x-kiro-egress-nonce': sessionNonce } : undefined,
})
return data
}

Expand Down Expand Up @@ -645,17 +656,25 @@ export async function startSocialLogin(
}

// 轮询 Social 登录状态
export async function pollSocialLogin(sessionId: string): Promise<PollSocialLoginResponse> {
const { data } = await api.post<PollSocialLoginResponse>(`/auth/social/poll/${sessionId}`)
export async function pollSocialLogin(
sessionId: string,
sessionNonce?: string,
): Promise<PollSocialLoginResponse> {
const { data } = await api.post<PollSocialLoginResponse>(`/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<PollSocialLoginResponse> {
const { data } = await api.post<PollSocialLoginResponse>(`/auth/social/complete/${sessionId}`, req)
const { data } = await api.post<PollSocialLoginResponse>(`/auth/social/complete/${sessionId}`, req, {
headers: sessionNonce ? { 'x-kiro-egress-nonce': sessionNonce } : undefined,
})
return data
}

Expand Down
66 changes: 58 additions & 8 deletions admin-ui/src/components/idc-login-dialog.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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'

Expand Down Expand Up @@ -139,7 +140,21 @@ export function IdcLoginDialog({ open, onOpenChange, onSuccess, mode = 'builder-
const [isStarting, setIsStarting] = useState(false)
const [session, setSession] = useState<StartIdcLoginResponse | null>(null)
const [credentialId, setCredentialId] = useState<number | null>(null)
const [proxyId, setProxyId] = useState('')
const sessionNonceRef = useRef<string | undefined>(undefined)
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | 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(() => {
Expand All @@ -157,6 +172,8 @@ export function IdcLoginDialog({ open, onOpenChange, onSuccess, mode = 'builder-
setCredentialId(null)
setIsStarting(false)
setLinkCopied(false)
setProxyId('')
sessionNonceRef.current = undefined
}
onOpenChange(v)
}
Expand Down Expand Up @@ -189,26 +206,35 @@ 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 {
setIsStarting(false)
}
}

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')
Expand All @@ -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)
}
Expand Down Expand Up @@ -298,6 +330,24 @@ export function IdcLoginDialog({ open, onOpenChange, onSuccess, mode = 'builder-
</div>
)}

{step === 'form' && egressEnabled && (
<div className="space-y-1.5">
<label htmlFor="idc-egress-proxy" className="text-sm font-medium">授权出口</label>
<Select value={proxyId} onValueChange={setProxyId}>
<SelectTrigger id="idc-egress-proxy">
<SelectValue placeholder="选择托管代理" />
</SelectTrigger>
<SelectContent>
{enabledProxies.map((proxy) => (
<SelectItem key={proxy.id} value={String(proxy.id)}>
{proxy.label || `代理 #${proxy.id}`}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}

{step === 'form' && (
<label className="flex items-start gap-2 rounded-lg border bg-muted/40 p-3 cursor-pointer">
<input
Expand Down Expand Up @@ -374,14 +424,14 @@ export function IdcLoginDialog({ open, onOpenChange, onSuccess, mode = 'builder-
<CheckCircle className="h-10 w-10 text-green-500" />
<p className="text-sm font-medium">登录成功</p>
<p className="text-xs text-muted-foreground">
凭据 #{credentialId} 已添加并启用
凭据 #{credentialId} 已添加{session?.slotId ? '并隔离' : '并启用'}
</p>
</div>
)}

<DialogFooter>
{step === 'form' && (
<Button onClick={handleStart} disabled={isStarting}>
<Button onClick={handleStart} disabled={isStarting || (egressEnabled && !proxyId)}>
{isStarting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
发起登录
</Button>
Expand Down
Loading
Loading