From 3a2cfd63ae00adbeff89ce3c5a6cc8243038246c Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 11 Jun 2026 08:54:38 -0300 Subject: [PATCH] =?UTF-8?q?refactor(dashboard):=20extract=20auth-import=20?= =?UTF-8?q?modals=20=E2=80=94=20#3501=20Phase=201a=20(#3634)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #3501 Phase 1a: extract 3 auth-import modal clusters. Co-authored-by: oyi77 --- CHANGELOG.md | 1 + file-size-baseline.json | 2 +- .../[id]/ProviderDetailPageClient.tsx | 2169 +---------------- .../modals/ImportClaudeAuthModal.tsx | 730 ++++++ .../modals/ImportCodexAuthModal.tsx | 743 ++++++ .../modals/ImportGeminiAuthModal.tsx | 706 ++++++ .../__tests__/authImportModals.test.tsx | 71 + 7 files changed, 2255 insertions(+), 2167 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/providers/[id]/components/modals/ImportClaudeAuthModal.tsx create mode 100644 src/app/(dashboard)/dashboard/providers/[id]/components/modals/ImportCodexAuthModal.tsx create mode 100644 src/app/(dashboard)/dashboard/providers/[id]/components/modals/ImportGeminiAuthModal.tsx create mode 100644 src/app/(dashboard)/dashboard/providers/[id]/components/modals/__tests__/authImportModals.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 9156f3a298..c6bdb08acd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### ♻️ Code Quality - **Provider-detail god-component decomposition — Phase 0** ([#3501]): introduced `ProviderDetailPageClient.tsx` and reduced `providers/[id]/page.tsx` to a thin 9-line route wrapper (was 12,882 LOC), following the repo's `*PageClient` convention. Added the first-ever smoke render test for the page (Hard Rule #8) as the safety net every later extraction phase is diffed against. Behavior unchanged; the `check-file-size` ratchet now tracks the extracted client. Foundation for Phases 1–6 (strangler-fig). Thanks @oyi77 for the parallel modularization effort in #3627. +- **Provider-detail god-component decomposition — Phase 1a** ([#3501]): extracted the three self-contained auth-import modal clusters (Codex/Claude/Gemini `Import*AuthModal` + `Apply*AuthModal` + their co-located helpers, ~2,160 LOC) into `providers/[id]/components/modals/`. `ProviderDetailPageClient.tsx` drops 12,882 → 10,719 LOC. Behavior unchanged (smoke test green; clusters had clean `{ onClose, onSuccess }` / inline-prop interfaces). Co-authored with @oyi77. --- diff --git a/file-size-baseline.json b/file-size-baseline.json index 32a7a8c4c2..02310f7666 100644 --- a/file-size-baseline.json +++ b/file-size-baseline.json @@ -48,7 +48,7 @@ "src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2570, "src/app/(dashboard)/dashboard/health/page.tsx": 1091, "src/app/(dashboard)/dashboard/playground/components/tabs/ApiTab.tsx": 847, - "src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": 12883, + "src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": 10720, "src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx": 906, "src/app/(dashboard)/dashboard/providers/page.tsx": 1925, "src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1127, diff --git a/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx b/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx index 3b2e74cbbc..315ec228f1 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx @@ -95,6 +95,9 @@ import { getWebSessionCredentialRequirement, type WebSessionCredentialRequirement, } from "./webSessionCredentials"; +import { ImportCodexAuthModal, ApplyCodexAuthModal } from "./components/modals/ImportCodexAuthModal"; +import { ImportClaudeAuthModal, ApplyClaudeAuthModal } from "./components/modals/ImportClaudeAuthModal"; +import { ImportGeminiAuthModal, ApplyGeminiAuthModal } from "./components/modals/ImportGeminiAuthModal"; type CompatByProtocolMap = Partial< Record< @@ -9346,1471 +9349,6 @@ function AddApiKeyModal({ // ──── ImportCodexAuthModal ──────────────────────────────────────────────────── -interface ImportCodexAuthModalProps { - onClose: () => void; - onSuccess: () => void; -} - -type ImportTopTab = "single" | "bulk"; -type BulkSubMode = "upload" | "paste" | "zip"; - -interface BulkEntry { - name: string; - json: unknown; - parseError: string | null; - email: string | null; -} - -function extractEmailFromJwtLocal(idToken: string): string | null { - try { - const parts = idToken.split("."); - if (parts.length !== 3) return null; - const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")); - return typeof payload.email === "string" ? payload.email : null; - } catch { - return null; - } -} - -function previewCodexJson(json: unknown): { valid: boolean; email: string | null } { - try { - const doc = json && typeof json === "object" ? (json as Record) : null; - // Codex CLI no longer writes auth_mode — accept both with and without it. - // Only reject when auth_mode is explicitly set to something other than "chatgpt". - if ( - !doc || - (doc.auth_mode !== undefined && doc.auth_mode !== null && doc.auth_mode !== "chatgpt") - ) - return { valid: false, email: null }; - const tokens = - doc.tokens && typeof doc.tokens === "object" ? (doc.tokens as Record) : null; - if (!tokens?.id_token || typeof tokens.id_token !== "string") - return { valid: false, email: null }; - return { valid: true, email: extractEmailFromJwtLocal(tokens.id_token as string) }; - } catch { - return { valid: false, email: null }; - } -} - -function parseBulkPasteText(text: string): BulkEntry[] { - const trimmed = text.trim(); - if (!trimmed) return []; - - const tryParse = (s: string): BulkEntry => { - try { - const json = JSON.parse(s); - const { email } = previewCodexJson(json); - return { name: email || "unknown", json, parseError: null, email }; - } catch { - return { name: "parse error", json: null, parseError: "Invalid JSON", email: null }; - } - }; - - try { - const arr = JSON.parse(trimmed); - if (Array.isArray(arr)) - return arr.map((item) => { - const { email } = previewCodexJson(item); - return { name: email || "unknown", json: item, parseError: null, email }; - }); - const { email } = previewCodexJson(arr); - return [{ name: email || "unknown", json: arr, parseError: null, email }]; - } catch { - return trimmed - .split(/^---$/m) - .map((s) => tryParse(s.trim())) - .filter((e) => e.json !== null || e.parseError !== null); - } -} - -function ImportCodexAuthModal({ onClose, onSuccess }: ImportCodexAuthModalProps) { - const t = useTranslations("providers"); - const notify = useNotificationStore(); - - // Top-level tab: Single / Bulk - const [topTab, setTopTab] = useState("single"); - - // ── Single mode state ── - const [singleTab, setSingleTab] = useState<"upload" | "paste">("upload"); - const [singleParsedJson, setSingleParsedJson] = useState(null); - const [singleParseError, setSingleParseError] = useState(null); - const [singleDetectedEmail, setSingleDetectedEmail] = useState(null); - const [singlePasteText, setSinglePasteText] = useState(""); - const [singleName, setSingleName] = useState(""); - const [singleEmail, setSingleEmail] = useState(""); - const [singleOverwrite, setSingleOverwrite] = useState(false); - const [singleLoading, setSingleLoading] = useState(false); - const [singleError, setSingleError] = useState(null); - - // ── Bulk mode state ── - const [bulkMode, setBulkMode] = useState("upload"); - const [bulkEntries, setBulkEntries] = useState([]); - const [bulkPasteText, setBulkPasteText] = useState(""); - const [bulkZipExtracting, setBulkZipExtracting] = useState(false); - const [bulkZipError, setBulkZipError] = useState(null); - const [bulkOverwrite, setBulkOverwrite] = useState(false); - const [bulkLoading, setBulkLoading] = useState(false); - const [bulkResult, setBulkResult] = useState<{ - success: number; - failed: number; - errors: { index: number; name: string; message: string }[]; - } | null>(null); - - // ── Single helpers ── - - function handleSinglePreview(json: unknown) { - setSingleParseError(null); - setSingleDetectedEmail(null); - setSingleParsedJson(null); - const { valid, email } = previewCodexJson(json); - if (!valid) { - setSingleParseError(t("codexImportInvalidShape") || "Not a valid Codex auth.json"); - return; - } - setSingleDetectedEmail(email); - if (email && !singleEmail) setSingleEmail(email); - setSingleParsedJson(json); - } - - function handleSingleFileChange(e: React.ChangeEvent) { - const file = e.target.files?.[0]; - if (!file) return; - const reader = new FileReader(); - reader.onload = (ev) => { - try { - handleSinglePreview(JSON.parse(ev.target?.result as string)); - } catch { - setSingleParseError(t("codexImportInvalidJson") || "Could not parse JSON"); - } - }; - reader.readAsText(file); - } - - function handleSinglePasteChange(text: string) { - setSinglePasteText(text); - if (!text.trim()) { - setSingleParsedJson(null); - setSingleParseError(null); - setSingleDetectedEmail(null); - return; - } - try { - handleSinglePreview(JSON.parse(text)); - } catch { - setSingleParseError(t("codexImportInvalidJson") || "Could not parse JSON"); - setSingleParsedJson(null); - } - } - - async function handleSingleSubmit() { - if (!singleParsedJson) return; - setSingleLoading(true); - setSingleError(null); - try { - const res = await fetch("/api/providers/codex-auth/import", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - source: { kind: "json", json: singleParsedJson }, - name: singleName.trim() || undefined, - email: singleEmail.trim() || undefined, - overwriteExisting: singleOverwrite, - }), - }); - const data = await res.json().catch(() => ({})); - if (!res.ok) { - setSingleError( - data.code === "duplicate_account" - ? t("codexImportDuplicate") || - "Account already exists — enable Replace existing to overwrite" - : data.error || t("codexImportFailed") || "Failed to import" - ); - return; - } - notify.success(t("codexImportSuccess") || "Codex connection imported successfully"); - onSuccess(); - } catch { - setSingleError(t("codexImportFailed") || "Failed to import Codex auth"); - } finally { - setSingleLoading(false); - } - } - - // ── Bulk helpers ── - - function handleBulkFilesChange(e: React.ChangeEvent) { - const files = Array.from(e.target.files || []); - const entries: BulkEntry[] = []; - let pending = files.length; - if (pending === 0) return; - files.forEach((file) => { - const reader = new FileReader(); - reader.onload = (ev) => { - try { - const json = JSON.parse(ev.target?.result as string); - const { email } = previewCodexJson(json); - entries.push({ - name: email || file.name.replace(".json", ""), - json, - parseError: null, - email, - }); - } catch { - entries.push({ name: file.name, json: null, parseError: "Invalid JSON", email: null }); - } - if (--pending === 0) setBulkEntries([...entries]); - }; - reader.readAsText(file); - }); - } - - function handleBulkPasteChange(text: string) { - setBulkPasteText(text); - if (!text.trim()) { - setBulkEntries([]); - return; - } - setBulkEntries(parseBulkPasteText(text)); - } - - async function handleZipUpload(e: React.ChangeEvent) { - const file = e.target.files?.[0]; - if (!file) return; - setBulkZipExtracting(true); - setBulkZipError(null); - setBulkEntries([]); - try { - const res = await fetch("/api/providers/codex-auth/zip-extract", { - method: "POST", - headers: { "Content-Type": "application/octet-stream" }, - body: file, - }); - const data = await res.json().catch(() => ({})); - if (!res.ok) { - setBulkZipError(data.error || t("codexImportBulkZipError") || "Failed to extract ZIP"); - return; - } - const extracted: BulkEntry[] = (data.entries || []).map( - (entry: { name: string; json: unknown; parseError: string | null }) => { - if (entry.parseError) - return { name: entry.name, json: null, parseError: entry.parseError, email: null }; - const { email } = previewCodexJson(entry.json); - return { - name: email || entry.name.replace(".json", ""), - json: entry.json, - parseError: null, - email, - }; - } - ); - setBulkEntries(extracted); - } catch { - setBulkZipError(t("codexImportBulkZipError") || "Failed to extract ZIP"); - } finally { - setBulkZipExtracting(false); - } - } - - async function handleBulkSubmit() { - const validEntries = bulkEntries.filter((e) => !e.parseError && e.json !== null); - if (validEntries.length === 0) return; - setBulkLoading(true); - setBulkResult(null); - try { - const res = await fetch("/api/providers/codex-auth/import-bulk", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - entries: validEntries.map((e) => ({ - json: e.json, - name: e.name || undefined, - email: e.email || undefined, - })), - overwriteExisting: bulkOverwrite, - }), - }); - const data = await res.json().catch(() => ({})); - if (!res.ok) { - notify.error(data.error || t("codexImportFailed") || "Failed to import"); - return; - } - setBulkResult({ success: data.success, failed: data.failed, errors: data.errors || [] }); - if (data.success > 0) onSuccess(); - } catch { - notify.error(t("codexImportFailed") || "Failed to import Codex auth"); - } finally { - setBulkLoading(false); - } - } - - const singleCanSubmit = !!singleParsedJson && !singleParseError && !singleLoading; - const validBulkCount = bulkEntries.filter((e) => !e.parseError && e.json !== null).length; - const bulkCanSubmit = validBulkCount > 0 && !bulkLoading && !bulkZipExtracting; - - const TOP_TABS: { id: ImportTopTab; label: string }[] = [ - { id: "single", label: t("codexImportTabSingle") || "Single" }, - { id: "bulk", label: t("codexImportTabBulk") || "Bulk" }, - ]; - - const BULK_MODES: { id: BulkSubMode; label: string }[] = [ - { id: "upload", label: t("codexImportBulkModeUpload") || "Upload files" }, - { id: "paste", label: t("codexImportBulkModePaste") || "Paste list" }, - { id: "zip", label: t("codexImportBulkModeZip") || "ZIP archive" }, - ]; - - return ( - -
- {/* Top-level Single / Bulk tabs */} -
- {TOP_TABS.map(({ id, label }) => ( - - ))} -
- - {/* ── Single tab ── */} - {topTab === "single" && ( - <> - {/* Source sub-tabs */} -
- {(["upload", "paste"] as const).map((id) => ( - - ))} -
- - {singleTab === "upload" && ( -
- - -

- {t("codexImportFileHint") || - "Select the auth.json file exported from Codex or OmniRoute."} -

-
- )} - - {singleTab === "paste" && ( -
- -