feat(provider): add OpenAI Codex browser OAuth flow (#398)
Co-authored-by: zuolingxuan <zuolingxuan@bytedance.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
880995af19
commit
31e80f256b
@@ -752,10 +752,16 @@ function AddProviderDialog({
|
||||
// OAuth Flow State
|
||||
const [oauthFlowing, setOauthFlowing] = useState(false);
|
||||
const [oauthData, setOauthData] = useState<{
|
||||
mode: 'device';
|
||||
verificationUri: string;
|
||||
userCode: string;
|
||||
expiresIn: number;
|
||||
} | {
|
||||
mode: 'manual';
|
||||
authorizationUrl: string;
|
||||
message?: string;
|
||||
} | null>(null);
|
||||
const [manualCodeInput, setManualCodeInput] = useState('');
|
||||
const [oauthError, setOauthError] = useState<string | null>(null);
|
||||
// For providers that support both OAuth and API key, let the user choose.
|
||||
// Default to the vendor's declared auth mode instead of hard-coding OAuth.
|
||||
@@ -792,13 +798,28 @@ function AddProviderDialog({
|
||||
// Manage OAuth events
|
||||
useEffect(() => {
|
||||
const handleCode = (data: unknown) => {
|
||||
setOauthData(data as { verificationUri: string; userCode: string; expiresIn: number });
|
||||
const payload = data as Record<string, unknown>;
|
||||
if (payload?.mode === 'manual') {
|
||||
setOauthData({
|
||||
mode: 'manual',
|
||||
authorizationUrl: String(payload.authorizationUrl || ''),
|
||||
message: typeof payload.message === 'string' ? payload.message : undefined,
|
||||
});
|
||||
} else {
|
||||
setOauthData({
|
||||
mode: 'device',
|
||||
verificationUri: String(payload.verificationUri || ''),
|
||||
userCode: String(payload.userCode || ''),
|
||||
expiresIn: Number(payload.expiresIn || 300),
|
||||
});
|
||||
}
|
||||
setOauthError(null);
|
||||
};
|
||||
|
||||
const handleSuccess = async (data: unknown) => {
|
||||
setOauthFlowing(false);
|
||||
setOauthData(null);
|
||||
setManualCodeInput('');
|
||||
setValidationError(null);
|
||||
|
||||
const { onClose: close, t: translate } = latestRef.current;
|
||||
@@ -813,8 +834,9 @@ function AddProviderDialog({
|
||||
const store = useProviderStore.getState();
|
||||
await store.refreshProviderSnapshot();
|
||||
|
||||
// Auto-set as default if no default is currently configured
|
||||
if (!store.defaultAccountId && accountId) {
|
||||
// OAuth sign-in should immediately become active default to avoid
|
||||
// leaving runtime on an API-key-only provider/model.
|
||||
if (accountId) {
|
||||
await store.setDefaultAccount(accountId);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -857,6 +879,7 @@ function AddProviderDialog({
|
||||
|
||||
setOauthFlowing(true);
|
||||
setOauthData(null);
|
||||
setManualCodeInput('');
|
||||
setOauthError(null);
|
||||
|
||||
try {
|
||||
@@ -879,6 +902,7 @@ function AddProviderDialog({
|
||||
const handleCancelOAuth = async () => {
|
||||
setOauthFlowing(false);
|
||||
setOauthData(null);
|
||||
setManualCodeInput('');
|
||||
setOauthError(null);
|
||||
pendingOAuthRef.current = null;
|
||||
await hostApiFetch('/api/providers/oauth/cancel', {
|
||||
@@ -886,6 +910,20 @@ function AddProviderDialog({
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmitManualOAuthCode = async () => {
|
||||
const value = manualCodeInput.trim();
|
||||
if (!value) return;
|
||||
try {
|
||||
await hostApiFetch('/api/providers/oauth/submit', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code: value }),
|
||||
});
|
||||
setOauthError(null);
|
||||
} catch (error) {
|
||||
setOauthError(String(error));
|
||||
}
|
||||
};
|
||||
|
||||
const availableTypes = PROVIDER_TYPE_INFO.filter((type) => {
|
||||
const vendor = vendorMap.get(type.id);
|
||||
if (!vendor) {
|
||||
@@ -1198,6 +1236,43 @@ function AddProviderDialog({
|
||||
<Loader2 className="h-10 w-10 animate-spin text-blue-500 mx-auto" />
|
||||
<p className="text-[13px] font-medium text-muted-foreground animate-pulse">{t('aiProviders.oauth.requestingCode')}</p>
|
||||
</div>
|
||||
) : oauthData.mode === 'manual' ? (
|
||||
<div className="space-y-4 w-full">
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-[16px] text-foreground">Complete OpenAI Login</h3>
|
||||
<p className="text-[13px] text-muted-foreground text-left bg-black/5 dark:bg-white/5 p-4 rounded-xl">
|
||||
{oauthData.message || 'Open the authorization page, complete login, then paste the callback URL or code below.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full rounded-full h-[42px] font-semibold"
|
||||
onClick={() => invokeIpc('shell:openExternal', oauthData.authorizationUrl)}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
Open Authorization Page
|
||||
</Button>
|
||||
|
||||
<Input
|
||||
placeholder="Paste callback URL or code"
|
||||
value={manualCodeInput}
|
||||
onChange={(e) => setManualCodeInput(e.target.value)}
|
||||
className={inputClasses}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="w-full rounded-full h-[42px] font-semibold bg-[#0a84ff] hover:bg-[#007aff] text-white"
|
||||
onClick={handleSubmitManualOAuthCode}
|
||||
disabled={!manualCodeInput.trim()}
|
||||
>
|
||||
Submit Code
|
||||
</Button>
|
||||
|
||||
<Button variant="ghost" className="w-full rounded-full h-[42px] font-semibold text-muted-foreground" onClick={handleCancelOAuth}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5 w-full">
|
||||
<div className="space-y-2">
|
||||
@@ -1272,4 +1347,4 @@ function AddProviderDialog({
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,17 @@ import { providerIcons } from '@/assets/providers';
|
||||
/** All supported provider types with UI metadata */
|
||||
export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
|
||||
{ id: 'anthropic', name: 'Anthropic', icon: '🤖', placeholder: 'sk-ant-api03-...', model: 'Claude', requiresApiKey: true },
|
||||
{ id: 'openai', name: 'OpenAI', icon: '💚', placeholder: 'sk-proj-...', model: 'GPT', requiresApiKey: true },
|
||||
{
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
icon: '💚',
|
||||
placeholder: 'sk-proj-...',
|
||||
model: 'GPT',
|
||||
requiresApiKey: true,
|
||||
isOAuth: true,
|
||||
supportsApiKey: true,
|
||||
apiKeyUrl: 'https://platform.openai.com/api-keys',
|
||||
},
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
|
||||
@@ -720,23 +720,44 @@ function ProviderContent({
|
||||
// OAuth Flow State
|
||||
const [oauthFlowing, setOauthFlowing] = useState(false);
|
||||
const [oauthData, setOauthData] = useState<{
|
||||
mode: 'device';
|
||||
verificationUri: string;
|
||||
userCode: string;
|
||||
expiresIn: number;
|
||||
} | {
|
||||
mode: 'manual';
|
||||
authorizationUrl: string;
|
||||
message?: string;
|
||||
} | null>(null);
|
||||
const [manualCodeInput, setManualCodeInput] = useState('');
|
||||
const [oauthError, setOauthError] = useState<string | null>(null);
|
||||
const pendingOAuthRef = useRef<{ accountId: string; label: string } | null>(null);
|
||||
|
||||
// Manage OAuth events
|
||||
useEffect(() => {
|
||||
const handleCode = (data: unknown) => {
|
||||
setOauthData(data as { verificationUri: string; userCode: string; expiresIn: number });
|
||||
const payload = data as Record<string, unknown>;
|
||||
if (payload?.mode === 'manual') {
|
||||
setOauthData({
|
||||
mode: 'manual',
|
||||
authorizationUrl: String(payload.authorizationUrl || ''),
|
||||
message: typeof payload.message === 'string' ? payload.message : undefined,
|
||||
});
|
||||
} else {
|
||||
setOauthData({
|
||||
mode: 'device',
|
||||
verificationUri: String(payload.verificationUri || ''),
|
||||
userCode: String(payload.userCode || ''),
|
||||
expiresIn: Number(payload.expiresIn || 300),
|
||||
});
|
||||
}
|
||||
setOauthError(null);
|
||||
};
|
||||
|
||||
const handleSuccess = async (data: unknown) => {
|
||||
setOauthFlowing(false);
|
||||
setOauthData(null);
|
||||
setManualCodeInput('');
|
||||
setKeyValid(true);
|
||||
|
||||
const payload = (data as { accountId?: string } | undefined) || undefined;
|
||||
@@ -796,6 +817,7 @@ function ProviderContent({
|
||||
|
||||
setOauthFlowing(true);
|
||||
setOauthData(null);
|
||||
setManualCodeInput('');
|
||||
setOauthError(null);
|
||||
|
||||
try {
|
||||
@@ -821,11 +843,26 @@ function ProviderContent({
|
||||
const handleCancelOAuth = async () => {
|
||||
setOauthFlowing(false);
|
||||
setOauthData(null);
|
||||
setManualCodeInput('');
|
||||
setOauthError(null);
|
||||
pendingOAuthRef.current = null;
|
||||
await hostApiFetch('/api/providers/oauth/cancel', { method: 'POST' });
|
||||
};
|
||||
|
||||
const handleSubmitManualOAuthCode = async () => {
|
||||
const value = manualCodeInput.trim();
|
||||
if (!value) return;
|
||||
try {
|
||||
await hostApiFetch('/api/providers/oauth/submit', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code: value }),
|
||||
});
|
||||
setOauthError(null);
|
||||
} catch (error) {
|
||||
setOauthError(String(error));
|
||||
}
|
||||
};
|
||||
|
||||
// On mount, try to restore previously configured provider
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -1303,6 +1340,42 @@ function ProviderContent({
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary mx-auto" />
|
||||
<p className="text-sm text-muted-foreground animate-pulse">Requesting secure login code...</p>
|
||||
</div>
|
||||
) : oauthData.mode === 'manual' ? (
|
||||
<div className="space-y-4 w-full">
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-medium text-lg">Complete OpenAI Login</h3>
|
||||
<p className="text-sm text-muted-foreground text-left mt-2">
|
||||
{oauthData.message || 'Open the authorization page, complete login, then paste the callback URL or code below.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={() => invokeIpc('shell:openExternal', oauthData.authorizationUrl)}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
Open Authorization Page
|
||||
</Button>
|
||||
|
||||
<Input
|
||||
placeholder="Paste callback URL or code"
|
||||
value={manualCodeInput}
|
||||
onChange={(e) => setManualCodeInput(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={handleSubmitManualOAuthCode}
|
||||
disabled={!manualCodeInput.trim()}
|
||||
>
|
||||
Submit Code
|
||||
</Button>
|
||||
|
||||
<Button variant="ghost" size="sm" className="w-full mt-2" onClick={handleCancelOAuth}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 w-full">
|
||||
<div className="space-y-1">
|
||||
|
||||
Reference in New Issue
Block a user