- add backend `/api/oauth/*` endpoints for provider status, browser/device-code/token login, flow query/polling, and logout - extend API handler with OAuth flow/state tracking and route registration, plus OAuth unit tests - implement frontend credentials page/components for OpenAI, Anthropic, and Google Antigravity login/logout - add OAuth API client and `useCredentialsPage` hook, with new EN/ZH i18n strings
161 lines
5.1 KiB
TypeScript
161 lines
5.1 KiB
TypeScript
import {
|
|
IconBrandOpenai,
|
|
IconClockHour4,
|
|
IconKey,
|
|
IconLoader2,
|
|
IconPlayerStopFilled,
|
|
} from "@tabler/icons-react"
|
|
import { useTranslation } from "react-i18next"
|
|
|
|
import type { OAuthProviderStatus } from "@/api/oauth"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
|
|
import { CredentialCard } from "./credential-card"
|
|
|
|
interface OpenAICredentialCardProps {
|
|
status?: OAuthProviderStatus
|
|
activeAction: string
|
|
token: string
|
|
onTokenChange: (value: string) => void
|
|
onStartBrowserOAuth: () => void
|
|
onStartDeviceCode: () => void
|
|
onStopLoading: () => void
|
|
onSaveToken: () => void
|
|
onAskLogout: () => void
|
|
}
|
|
|
|
export function OpenAICredentialCard({
|
|
status,
|
|
activeAction,
|
|
token,
|
|
onTokenChange,
|
|
onStartBrowserOAuth,
|
|
onStartDeviceCode,
|
|
onStopLoading,
|
|
onSaveToken,
|
|
onAskLogout,
|
|
}: OpenAICredentialCardProps) {
|
|
const { t } = useTranslation()
|
|
const actionBusy = activeAction !== ""
|
|
const browserLoading = activeAction === "openai:browser"
|
|
const deviceLoading = activeAction === "openai:device"
|
|
const oauthLoading = browserLoading || deviceLoading
|
|
const tokenLoading = activeAction === "openai:token"
|
|
|
|
return (
|
|
<CredentialCard
|
|
title="OpenAI"
|
|
description={t(
|
|
"credentials.providers.openai.description",
|
|
"Supports browser OAuth, device code, and token login.",
|
|
)}
|
|
status={status?.status ?? "not_logged_in"}
|
|
authMethod={status?.auth_method}
|
|
details={
|
|
status?.account_id ? (
|
|
<p>
|
|
{t("credentials.labels.account", "Account")}: {status.account_id}
|
|
</p>
|
|
) : null
|
|
}
|
|
actions={
|
|
<div className="border-muted flex h-[120px] flex-col rounded-lg border p-3">
|
|
<div className="flex h-full flex-col gap-3">
|
|
<div className="min-h-8">
|
|
<div className="flex flex-nowrap items-center gap-2 overflow-x-auto">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={actionBusy}
|
|
onClick={onStartBrowserOAuth}
|
|
>
|
|
{browserLoading && (
|
|
<IconLoader2 className="size-4 animate-spin" />
|
|
)}
|
|
<IconBrandOpenai className="size-4" />
|
|
{t("credentials.actions.browser", "Browser OAuth")}
|
|
</Button>
|
|
|
|
{oauthLoading && !deviceLoading && (
|
|
<Button
|
|
size="icon-xs"
|
|
variant="secondary"
|
|
onClick={onStopLoading}
|
|
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
>
|
|
<IconPlayerStopFilled className="size-4" />
|
|
</Button>
|
|
)}
|
|
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={actionBusy}
|
|
onClick={onStartDeviceCode}
|
|
>
|
|
{deviceLoading && (
|
|
<IconLoader2 className="size-4 animate-spin" />
|
|
)}
|
|
<IconClockHour4 className="size-4" />
|
|
{t("credentials.actions.deviceCode", "Device Code")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="min-h-9 flex-1">
|
|
<div className="flex h-full items-center gap-2">
|
|
<Input
|
|
value={token}
|
|
onChange={(e) => onTokenChange(e.target.value)}
|
|
type="password"
|
|
placeholder={t(
|
|
"credentials.fields.openaiToken",
|
|
"OpenAI token",
|
|
)}
|
|
/>
|
|
<Button
|
|
size="sm"
|
|
disabled={actionBusy || !token.trim()}
|
|
onClick={onSaveToken}
|
|
>
|
|
{tokenLoading && (
|
|
<IconLoader2 className="size-4 animate-spin" />
|
|
)}
|
|
<IconKey className="size-4" />
|
|
{t("credentials.actions.saveToken", "Save")}
|
|
</Button>
|
|
{tokenLoading && (
|
|
<Button
|
|
size="icon-sm"
|
|
variant="ghost"
|
|
onClick={onStopLoading}
|
|
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
>
|
|
<IconPlayerStopFilled className="size-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
footer={
|
|
status?.logged_in ? (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
disabled={actionBusy}
|
|
onClick={onAskLogout}
|
|
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
>
|
|
{activeAction === "openai:logout" && (
|
|
<IconLoader2 className="size-4 animate-spin" />
|
|
)}
|
|
{t("credentials.actions.logout", "Logout")}
|
|
</Button>
|
|
) : null
|
|
}
|
|
/>
|
|
)
|
|
}
|