From 8962e466d6a25ee546d54dc7a9a62faca46de177 Mon Sep 17 00:00:00 2001 From: Quoc Nguyen Date: Mon, 8 Jun 2026 10:08:46 +0700 Subject: [PATCH] feat(providers/codex): bulk add accounts via JSON Add a "Bulk Add" button on /dashboard/providers/codex that imports multiple OAuth accounts at once by pasting a JSON array, single object, or { accounts: [...] } wrapper. - New endpoint POST /api/oauth/codex/bulk-import (serial loop, no token echo) - New BulkImportCodexModal component with JSON textarea + success/failure summary - Persist idToken/lastRefreshAt on first insert via OPTIONAL_FIELDS - Backfill email/chatgptAccountId/chatgptPlanType from JWT when missing - Derive expiresAt from expiresIn when missing - Gated to providerId === "codex" only; other providers unaffected Co-authored-by: Cursor --- .../providers/[id]/BulkImportCodexModal.js | 150 ++++++++++++++++++ .../dashboard/providers/[id]/page.js | 27 ++++ src/app/api/oauth/codex/bulk-import/route.js | 121 ++++++++++++++ src/lib/db/repos/connectionsRepo.js | 2 +- 4 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 src/app/(dashboard)/dashboard/providers/[id]/BulkImportCodexModal.js create mode 100644 src/app/api/oauth/codex/bulk-import/route.js diff --git a/src/app/(dashboard)/dashboard/providers/[id]/BulkImportCodexModal.js b/src/app/(dashboard)/dashboard/providers/[id]/BulkImportCodexModal.js new file mode 100644 index 00000000..d18abefe --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/BulkImportCodexModal.js @@ -0,0 +1,150 @@ +"use client"; + +import { useState } from "react"; +import PropTypes from "prop-types"; +import { Button, Modal } from "@/shared/components"; +import { translate } from "@/i18n/runtime"; + +const PLACEHOLDER = `[ + { + "accessToken": "eyJhbGc...", + "refreshToken": "rt_...", + "idToken": "eyJhbGc...", + "email": "user@example.com" + } +]`; + +function normalizeToArray(parsed) { + if (Array.isArray(parsed)) return parsed; + if (parsed && typeof parsed === "object") { + if (Array.isArray(parsed.accounts)) return parsed.accounts; + return [parsed]; + } + return null; +} + +export default function BulkImportCodexModal({ isOpen, onClose, onSuccess }) { + const [jsonText, setJsonText] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [parseError, setParseError] = useState(""); + const [result, setResult] = useState(null); + + const handleClose = () => { + if (submitting) return; + setJsonText(""); + setParseError(""); + setResult(null); + onClose(); + }; + + const handleSubmit = async () => { + setParseError(""); + setResult(null); + + const trimmed = jsonText.trim(); + if (!trimmed) return; + + let parsed; + try { + parsed = JSON.parse(trimmed); + } catch (err) { + setParseError(`${translate("Invalid JSON")}: ${err.message}`); + return; + } + + const accounts = normalizeToArray(parsed); + if (!accounts || accounts.length === 0) { + setParseError(translate("No accounts found in input")); + return; + } + + setSubmitting(true); + try { + const res = await fetch("/api/oauth/codex/bulk-import", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ accounts }), + }); + const data = await res.json(); + if (!res.ok) { + setParseError(data?.error || `Request failed: ${res.status}`); + return; + } + setResult(data); + if (data.success > 0 && typeof onSuccess === "function") { + onSuccess(); + } + } catch (err) { + setParseError(err.message || translate("Request failed")); + } finally { + setSubmitting(false); + } + }; + + const failedItems = result?.results?.filter((r) => !r.ok) || []; + + return ( + +
+

+ {translate( + "Paste an array of codex account JSON objects. Each must include accessToken (and ideally refreshToken, idToken)." + )} +

+ +