import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { useNotificationStore } from '@/stores'; import { oauthApi, type OAuthProvider, type IFlowCookieAuthResponse } from '@/services/api/oauth'; import styles from './OAuthPage.module.scss'; interface ProviderState { url?: string; state?: string; status?: 'idle' | 'waiting' | 'success' | 'error'; error?: string; polling?: boolean; projectId?: string; projectIdError?: string; callbackUrl?: string; callbackSubmitting?: boolean; callbackStatus?: 'success' | 'error'; callbackError?: string; } interface IFlowCookieState { cookie: string; loading: boolean; result?: IFlowCookieAuthResponse; error?: string; errorType?: 'error' | 'warning'; } const PROVIDERS: { id: OAuthProvider; titleKey: string; hintKey: string; urlLabelKey: string }[] = [ { id: 'codex', titleKey: 'auth_login.codex_oauth_title', hintKey: 'auth_login.codex_oauth_hint', urlLabelKey: 'auth_login.codex_oauth_url_label' }, { id: 'anthropic', titleKey: 'auth_login.anthropic_oauth_title', hintKey: 'auth_login.anthropic_oauth_hint', urlLabelKey: 'auth_login.anthropic_oauth_url_label' }, { id: 'antigravity', titleKey: 'auth_login.antigravity_oauth_title', hintKey: 'auth_login.antigravity_oauth_hint', urlLabelKey: 'auth_login.antigravity_oauth_url_label' }, { id: 'gemini-cli', titleKey: 'auth_login.gemini_cli_oauth_title', hintKey: 'auth_login.gemini_cli_oauth_hint', urlLabelKey: 'auth_login.gemini_cli_oauth_url_label' }, { id: 'qwen', titleKey: 'auth_login.qwen_oauth_title', hintKey: 'auth_login.qwen_oauth_hint', urlLabelKey: 'auth_login.qwen_oauth_url_label' }, { id: 'iflow', titleKey: 'auth_login.iflow_oauth_title', hintKey: 'auth_login.iflow_oauth_hint', urlLabelKey: 'auth_login.iflow_oauth_url_label' } ]; const CALLBACK_SUPPORTED: OAuthProvider[] = ['codex', 'anthropic', 'antigravity', 'gemini-cli', 'iflow']; export function OAuthPage() { const { t } = useTranslation(); const { showNotification } = useNotificationStore(); const [states, setStates] = useState>({} as Record); const [iflowCookie, setIflowCookie] = useState({ cookie: '', loading: false }); const timers = useRef>({}); useEffect(() => { return () => { Object.values(timers.current).forEach((timer) => window.clearInterval(timer)); }; }, []); const updateProviderState = (provider: OAuthProvider, next: Partial) => { setStates((prev) => ({ ...prev, [provider]: { ...(prev[provider] ?? {}), ...next } })); }; const startPolling = (provider: OAuthProvider, state: string) => { if (timers.current[provider]) { clearInterval(timers.current[provider]); } const timer = window.setInterval(async () => { try { const res = await oauthApi.getAuthStatus(state); if (res.status === 'ok') { updateProviderState(provider, { status: 'success', polling: false }); showNotification(t('auth_login.codex_oauth_status_success'), 'success'); window.clearInterval(timer); delete timers.current[provider]; } else if (res.status === 'error') { updateProviderState(provider, { status: 'error', error: res.error, polling: false }); showNotification(`${t('auth_login.codex_oauth_status_error')} ${res.error || ''}`, 'error'); window.clearInterval(timer); delete timers.current[provider]; } } catch (err: any) { updateProviderState(provider, { status: 'error', error: err?.message, polling: false }); window.clearInterval(timer); delete timers.current[provider]; } }, 3000); timers.current[provider] = timer; }; const startAuth = async (provider: OAuthProvider) => { const projectId = provider === 'gemini-cli' ? (states[provider]?.projectId || '').trim() : undefined; if (provider === 'gemini-cli' && !projectId) { const message = t('auth_login.gemini_cli_project_id_required'); updateProviderState(provider, { projectIdError: message }); showNotification(message, 'warning'); return; } if (provider === 'gemini-cli') { updateProviderState(provider, { projectIdError: undefined }); } updateProviderState(provider, { status: 'waiting', polling: true, error: undefined, callbackStatus: undefined, callbackError: undefined, callbackUrl: '' }); try { const res = await oauthApi.startAuth( provider, provider === 'gemini-cli' ? { projectId: projectId! } : undefined ); updateProviderState(provider, { url: res.url, state: res.state, status: 'waiting', polling: true }); if (res.state) { startPolling(provider, res.state); } } catch (err: any) { updateProviderState(provider, { status: 'error', error: err?.message, polling: false }); showNotification(`${t('auth_login.codex_oauth_start_error')} ${err?.message || ''}`, 'error'); } }; const copyLink = async (url?: string) => { if (!url) return; try { await navigator.clipboard.writeText(url); showNotification(t('notification.link_copied'), 'success'); } catch { showNotification('Copy failed', 'error'); } }; const submitCallback = async (provider: OAuthProvider) => { const redirectUrl = (states[provider]?.callbackUrl || '').trim(); if (!redirectUrl) { showNotification(t('auth_login.oauth_callback_required'), 'warning'); return; } updateProviderState(provider, { callbackSubmitting: true, callbackStatus: undefined, callbackError: undefined }); try { await oauthApi.submitCallback(provider, redirectUrl); updateProviderState(provider, { callbackSubmitting: false, callbackStatus: 'success' }); showNotification(t('auth_login.oauth_callback_success'), 'success'); } catch (err: any) { const errorMessage = err?.status === 404 ? t('auth_login.oauth_callback_upgrade_hint', { defaultValue: 'Please update CLI Proxy API or check the connection.' }) : err?.message; updateProviderState(provider, { callbackSubmitting: false, callbackStatus: 'error', callbackError: errorMessage }); const notificationMessage = errorMessage ? `${t('auth_login.oauth_callback_error')} ${errorMessage}` : t('auth_login.oauth_callback_error'); showNotification(notificationMessage, 'error'); } }; const submitIflowCookie = async () => { const cookie = iflowCookie.cookie.trim(); if (!cookie) { showNotification(t('auth_login.iflow_cookie_required'), 'warning'); return; } setIflowCookie((prev) => ({ ...prev, loading: true, error: undefined, errorType: undefined, result: undefined })); try { const res = await oauthApi.iflowCookieAuth(cookie); if (res.status === 'ok') { setIflowCookie((prev) => ({ ...prev, loading: false, result: res })); showNotification(t('auth_login.iflow_cookie_status_success'), 'success'); } else { setIflowCookie((prev) => ({ ...prev, loading: false, error: res.error, errorType: 'error' })); showNotification(`${t('auth_login.iflow_cookie_status_error')} ${res.error || ''}`, 'error'); } } catch (err: any) { if (err?.status === 409) { const message = t('auth_login.iflow_cookie_config_duplicate'); setIflowCookie((prev) => ({ ...prev, loading: false, error: message, errorType: 'warning' })); showNotification(message, 'warning'); return; } setIflowCookie((prev) => ({ ...prev, loading: false, error: err?.message, errorType: 'error' })); showNotification(`${t('auth_login.iflow_cookie_start_error')} ${err?.message || ''}`, 'error'); } }; return (

{t('nav.oauth', { defaultValue: 'OAuth' })}

{PROVIDERS.map((provider) => { const state = states[provider.id] || {}; const canSubmitCallback = CALLBACK_SUPPORTED.includes(provider.id) && Boolean(state.url); return (
startAuth(provider.id)} loading={state.polling}> {t('common.login')} } >
{t(provider.hintKey)}
{provider.id === 'gemini-cli' && ( updateProviderState(provider.id, { projectId: e.target.value, projectIdError: undefined }) } placeholder={t('auth_login.gemini_cli_project_id_placeholder')} /> )} {state.url && (
{t(provider.urlLabelKey)}
{state.url}
)} {canSubmitCallback && (
updateProviderState(provider.id, { callbackUrl: e.target.value, callbackStatus: undefined, callbackError: undefined }) } placeholder={t('auth_login.oauth_callback_placeholder')} />
{state.callbackStatus === 'success' && state.status === 'waiting' && (
{t('auth_login.oauth_callback_status_success')}
)} {state.callbackStatus === 'error' && (
{t('auth_login.oauth_callback_status_error')} {state.callbackError || ''}
)}
)} {state.status && state.status !== 'idle' && (
{state.status === 'success' ? t('auth_login.codex_oauth_status_success') : state.status === 'error' ? `${t('auth_login.codex_oauth_status_error')} ${state.error || ''}` : t('auth_login.codex_oauth_status_waiting')}
)}
); })} {/* iFlow Cookie 登录 */} {t('auth_login.iflow_cookie_button')} } >
{t('auth_login.iflow_cookie_hint')}
{t('auth_login.iflow_cookie_key_hint')}
setIflowCookie((prev) => ({ ...prev, cookie: e.target.value }))} placeholder={t('auth_login.iflow_cookie_placeholder')} />
{iflowCookie.error && (
{iflowCookie.errorType === 'warning' ? t('auth_login.iflow_cookie_status_duplicate') : t('auth_login.iflow_cookie_status_error')}{' '} {iflowCookie.error}
)} {iflowCookie.result && iflowCookie.result.status === 'ok' && (
{t('auth_login.iflow_cookie_result_title')}
{iflowCookie.result.email && (
{t('auth_login.iflow_cookie_result_email')} {iflowCookie.result.email}
)} {iflowCookie.result.expired && (
{t('auth_login.iflow_cookie_result_expired')} {iflowCookie.result.expired}
)} {iflowCookie.result.saved_path && (
{t('auth_login.iflow_cookie_result_path')} {iflowCookie.result.saved_path}
)} {iflowCookie.result.type && (
{t('auth_login.iflow_cookie_result_type')} {iflowCookie.result.type}
)}
)}
); }