mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-02 19:00:49 +08:00
feat: update document title for clarity, add logo image, enhance OAuthPage with iFlow cookie authentication feature, and improve localization for remote access messages
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>cli-proxy-webui-react</title>
|
<title>CLI Proxy API Management Center</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -431,7 +431,8 @@
|
|||||||
"iflow_cookie_result_email": "Account",
|
"iflow_cookie_result_email": "Account",
|
||||||
"iflow_cookie_result_expired": "Expires At",
|
"iflow_cookie_result_expired": "Expires At",
|
||||||
"iflow_cookie_result_path": "Saved Path",
|
"iflow_cookie_result_path": "Saved Path",
|
||||||
"iflow_cookie_result_type": "Type"
|
"iflow_cookie_result_type": "Type",
|
||||||
|
"remote_access_disabled": "This login method is not available for remote access. Please access from localhost."
|
||||||
},
|
},
|
||||||
"usage_stats": {
|
"usage_stats": {
|
||||||
"title": "Usage Statistics",
|
"title": "Usage Statistics",
|
||||||
|
|||||||
@@ -431,7 +431,8 @@
|
|||||||
"iflow_cookie_result_email": "账号",
|
"iflow_cookie_result_email": "账号",
|
||||||
"iflow_cookie_result_expired": "过期时间",
|
"iflow_cookie_result_expired": "过期时间",
|
||||||
"iflow_cookie_result_path": "保存路径",
|
"iflow_cookie_result_path": "保存路径",
|
||||||
"iflow_cookie_result_type": "类型"
|
"iflow_cookie_result_type": "类型",
|
||||||
|
"remote_access_disabled": "远程访问不支持此登录方式,请从本地 (localhost) 访问"
|
||||||
},
|
},
|
||||||
"usage_stats": {
|
"usage_stats": {
|
||||||
"title": "使用统计",
|
"title": "使用统计",
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Input } from '@/components/ui/Input';
|
||||||
import { useNotificationStore } from '@/stores';
|
import { useNotificationStore } from '@/stores';
|
||||||
import { oauthApi, type OAuthProvider } from '@/services/api/oauth';
|
import { oauthApi, type OAuthProvider, type IFlowCookieAuthResponse } from '@/services/api/oauth';
|
||||||
|
import { isLocalhost } from '@/utils/connection';
|
||||||
|
|
||||||
interface ProviderState {
|
interface ProviderState {
|
||||||
url?: string;
|
url?: string;
|
||||||
@@ -13,6 +15,13 @@ interface ProviderState {
|
|||||||
polling?: boolean;
|
polling?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IFlowCookieState {
|
||||||
|
cookie: string;
|
||||||
|
loading: boolean;
|
||||||
|
result?: IFlowCookieAuthResponse;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const PROVIDERS: { id: OAuthProvider; titleKey: string; hintKey: string; urlLabelKey: string }[] = [
|
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: '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: 'anthropic', titleKey: 'auth_login.anthropic_oauth_title', hintKey: 'auth_login.anthropic_oauth_hint', urlLabelKey: 'auth_login.anthropic_oauth_url_label' },
|
||||||
@@ -26,8 +35,12 @@ export function OAuthPage() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { showNotification } = useNotificationStore();
|
const { showNotification } = useNotificationStore();
|
||||||
const [states, setStates] = useState<Record<OAuthProvider, ProviderState>>({} as Record<OAuthProvider, ProviderState>);
|
const [states, setStates] = useState<Record<OAuthProvider, ProviderState>>({} as Record<OAuthProvider, ProviderState>);
|
||||||
|
const [iflowCookie, setIflowCookie] = useState<IFlowCookieState>({ cookie: '', loading: false });
|
||||||
const timers = useRef<Record<string, number>>({});
|
const timers = useRef<Record<string, number>>({});
|
||||||
|
|
||||||
|
// 检测是否为本地访问
|
||||||
|
const isLocal = useMemo(() => isLocalhost(window.location.hostname), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
Object.values(timers.current).forEach((timer) => window.clearInterval(timer));
|
Object.values(timers.current).forEach((timer) => window.clearInterval(timer));
|
||||||
@@ -103,22 +116,58 @@ export function OAuthPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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, 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 }));
|
||||||
|
showNotification(`${t('auth_login.iflow_cookie_status_error')} ${res.error || ''}`, 'error');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setIflowCookie((prev) => ({ ...prev, loading: false, error: err?.message }));
|
||||||
|
showNotification(`${t('auth_login.iflow_cookie_start_error')} ${err?.message || ''}`, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="stack">
|
<div className="stack">
|
||||||
{PROVIDERS.map((provider) => {
|
{PROVIDERS.map((provider) => {
|
||||||
const state = states[provider.id] || {};
|
const state = states[provider.id] || {};
|
||||||
|
// 非本地访问时禁用所有 OAuth 登录方式
|
||||||
|
const isDisabled = !isLocal;
|
||||||
return (
|
return (
|
||||||
<Card
|
<div
|
||||||
key={provider.id}
|
key={provider.id}
|
||||||
title={t(provider.titleKey)}
|
style={isDisabled ? { opacity: 0.6, pointerEvents: 'none' } : undefined}
|
||||||
extra={
|
|
||||||
<Button onClick={() => startAuth(provider.id)} loading={state.polling}>
|
|
||||||
{t('common.login')}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="hint">{t(provider.hintKey)}</div>
|
<Card
|
||||||
{state.url && (
|
title={t(provider.titleKey)}
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
onClick={() => startAuth(provider.id)}
|
||||||
|
loading={state.polling}
|
||||||
|
disabled={isDisabled}
|
||||||
|
>
|
||||||
|
{t('common.login')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="hint">{t(provider.hintKey)}</div>
|
||||||
|
{isDisabled && (
|
||||||
|
<div className="status-badge warning" style={{ marginTop: 8 }}>
|
||||||
|
{t('auth_login.remote_access_disabled')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isDisabled && state.url && (
|
||||||
<div className="connection-box">
|
<div className="connection-box">
|
||||||
<div className="label">{t(provider.urlLabelKey)}</div>
|
<div className="label">{t(provider.urlLabelKey)}</div>
|
||||||
<div className="value">{state.url}</div>
|
<div className="value">{state.url}</div>
|
||||||
@@ -136,18 +185,77 @@ export function OAuthPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="status-badge" style={{ marginTop: 8 }}>
|
{!isDisabled && (
|
||||||
{state.status === 'success'
|
<div className="status-badge" style={{ marginTop: 8 }}>
|
||||||
? t('auth_login.codex_oauth_status_success')
|
{state.status === 'success'
|
||||||
: state.status === 'error'
|
? t('auth_login.codex_oauth_status_success')
|
||||||
? `${t('auth_login.codex_oauth_status_error')} ${state.error || ''}`
|
: state.status === 'error'
|
||||||
: state.status === 'waiting'
|
? `${t('auth_login.codex_oauth_status_error')} ${state.error || ''}`
|
||||||
? t('auth_login.codex_oauth_status_waiting')
|
: state.status === 'waiting'
|
||||||
: t('common.info')}
|
? t('auth_login.codex_oauth_status_waiting')
|
||||||
</div>
|
: t('common.info')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* iFlow Cookie 登录 */}
|
||||||
|
<Card
|
||||||
|
title={t('auth_login.iflow_cookie_title')}
|
||||||
|
extra={
|
||||||
|
<Button onClick={submitIflowCookie} loading={iflowCookie.loading}>
|
||||||
|
{t('auth_login.iflow_cookie_button')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="hint">{t('auth_login.iflow_cookie_hint')}</div>
|
||||||
|
<div className="form-item" style={{ marginTop: 12 }}>
|
||||||
|
<label className="label">{t('auth_login.iflow_cookie_label')}</label>
|
||||||
|
<Input
|
||||||
|
value={iflowCookie.cookie}
|
||||||
|
onChange={(e) => setIflowCookie((prev) => ({ ...prev, cookie: e.target.value }))}
|
||||||
|
placeholder={t('auth_login.iflow_cookie_placeholder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{iflowCookie.error && (
|
||||||
|
<div className="status-badge error" style={{ marginTop: 8 }}>
|
||||||
|
{t('auth_login.iflow_cookie_status_error')} {iflowCookie.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{iflowCookie.result && iflowCookie.result.status === 'ok' && (
|
||||||
|
<div className="connection-box" style={{ marginTop: 12 }}>
|
||||||
|
<div className="label">{t('auth_login.iflow_cookie_result_title')}</div>
|
||||||
|
<div className="key-value-list">
|
||||||
|
{iflowCookie.result.email && (
|
||||||
|
<div className="key-value-item">
|
||||||
|
<span className="key">{t('auth_login.iflow_cookie_result_email')}</span>
|
||||||
|
<span className="value">{iflowCookie.result.email}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{iflowCookie.result.expired && (
|
||||||
|
<div className="key-value-item">
|
||||||
|
<span className="key">{t('auth_login.iflow_cookie_result_expired')}</span>
|
||||||
|
<span className="value">{iflowCookie.result.expired}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{iflowCookie.result.saved_path && (
|
||||||
|
<div className="key-value-item">
|
||||||
|
<span className="key">{t('auth_login.iflow_cookie_result_path')}</span>
|
||||||
|
<span className="value">{iflowCookie.result.saved_path}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{iflowCookie.result.type && (
|
||||||
|
<div className="key-value-item">
|
||||||
|
<span className="key">{t('auth_login.iflow_cookie_result_type')}</span>
|
||||||
|
<span className="value">{iflowCookie.result.type}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ export interface OAuthStartResponse {
|
|||||||
state?: string;
|
state?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IFlowCookieAuthResponse {
|
||||||
|
status: 'ok' | 'error';
|
||||||
|
error?: string;
|
||||||
|
saved_path?: string;
|
||||||
|
email?: string;
|
||||||
|
expired?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const WEBUI_SUPPORTED: OAuthProvider[] = ['codex', 'anthropic', 'antigravity', 'gemini-cli', 'iflow'];
|
const WEBUI_SUPPORTED: OAuthProvider[] = ['codex', 'anthropic', 'antigravity', 'gemini-cli', 'iflow'];
|
||||||
|
|
||||||
export const oauthApi = {
|
export const oauthApi = {
|
||||||
@@ -28,5 +37,9 @@ export const oauthApi = {
|
|||||||
getAuthStatus: (state: string) =>
|
getAuthStatus: (state: string) =>
|
||||||
apiClient.get<{ status: 'ok' | 'wait' | 'error'; error?: string }>(`/get-auth-status`, {
|
apiClient.get<{ status: 'ok' | 'wait' | 'error'; error?: string }>(`/get-auth-status`, {
|
||||||
params: { state }
|
params: { state }
|
||||||
})
|
}),
|
||||||
|
|
||||||
|
/** iFlow cookie 认证 */
|
||||||
|
iflowCookieAuth: (cookie: string) =>
|
||||||
|
apiClient.post<IFlowCookieAuthResponse>('/iflow-auth-url', { cookie })
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user