Spaces:
Runtime error
Runtime error
| "use client"; | |
| import { useTranslations } from "next-intl"; | |
| import { useEffect, useState } from "react"; | |
| /** | |
| * OAuth Callback Page | |
| * | |
| * Reads URL params via window.location.search (not useSearchParams) to avoid | |
| * the Next.js Suspense boundary requirement, which can delay hydration in popup | |
| * windows that navigate back from a cross-origin OAuth page (e.g. Google). | |
| * Sends the callback data back via three methods in order of reliability: | |
| * 1. postMessage to window.opener (may be null after COOP cross-origin nav) | |
| * 2. BroadcastChannel (same-origin, works across browsing context groups) | |
| * 3. localStorage storage event (works across browsing context groups) | |
| */ | |
| export default function CallbackPage() { | |
| const [status, setStatus] = useState<"processing" | "success" | "done" | "manual">("processing"); | |
| const [currentUrl] = useState(() => | |
| typeof window === "undefined" ? "" : window.location.href | |
| ); | |
| const t = useTranslations("auth"); | |
| useEffect(() => { | |
| const params = new URLSearchParams(window.location.search); | |
| const code = params.get("code"); | |
| const state = params.get("state"); | |
| const error = params.get("error"); | |
| const errorDescription = params.get("error_description"); | |
| const callbackData = { | |
| code, | |
| state, | |
| error, | |
| errorDescription, | |
| fullUrl: window.location.href, | |
| }; | |
| let sent = false; | |
| let openerSameOrigin = false; | |
| const queueStatusUpdate = (nextStatus: "processing" | "success" | "done" | "manual") => { | |
| queueMicrotask(() => setStatus(nextStatus)); | |
| }; | |
| if (window.opener) { | |
| try { | |
| openerSameOrigin = window.opener.location.origin === window.location.origin; | |
| } catch { | |
| openerSameOrigin = false; | |
| } | |
| } | |
| // Method 1: postMessage to opener (popup mode). | |
| // May be null when Google OAuth's COOP header severs the opener reference. | |
| // | |
| // Only relay {code, state} to a known-trusted target origin. A wildcard "*" | |
| // here would leak the OAuth code/state to a hostile opener — e.g. a page | |
| // that opened this callback URL in a popup to phish the code. The browser | |
| // delivers postMessage only when the opener's origin matches `targetOrigin`, | |
| // so iterating over an allowlist lets the same-origin parent and Codex's | |
| // fixed loopback helper receive it while silently dropping it for any other | |
| // origin. Methods 2 (BroadcastChannel) and 3 (localStorage) cover the | |
| // same-origin fallback when the opener was severed by COOP. | |
| const trustedTargetOrigins = [ | |
| window.location.origin, // Same origin (dashboard popup mode). | |
| "http://localhost:1455", // Codex helper (fixed loopback port). | |
| "http://127.0.0.1:1455", // Same Codex helper, IPv4 literal form. | |
| ]; | |
| if (window.opener) { | |
| for (const origin of trustedTargetOrigins) { | |
| try { | |
| window.opener.postMessage( | |
| { type: "oauth_callback", data: callbackData }, | |
| origin | |
| ); | |
| sent = true; | |
| } catch (e) { | |
| console.log("postMessage failed:", e); | |
| } | |
| } | |
| } | |
| // Method 2: BroadcastChannel — works across browsing context groups for same origin. | |
| try { | |
| const channel = new BroadcastChannel("oauth_callback"); | |
| channel.postMessage(callbackData); | |
| channel.close(); | |
| sent = true; | |
| } catch (e) { | |
| console.log("BroadcastChannel failed:", e); | |
| } | |
| // Method 3: localStorage — triggers storage event in all same-origin windows, | |
| // regardless of browsing context group isolation from COOP. | |
| try { | |
| localStorage.setItem( | |
| "oauth_callback", | |
| JSON.stringify({ ...callbackData, timestamp: Date.now() }) | |
| ); | |
| sent = true; | |
| } catch (e) { | |
| console.log("localStorage failed:", e); | |
| } | |
| if (sent && (code || error)) { | |
| if (window.opener && openerSameOrigin) { | |
| queueStatusUpdate("success"); | |
| setTimeout(() => { | |
| window.close(); | |
| // If close is prevented (browser policy), fall through to manual close prompt. | |
| setTimeout(() => setStatus("done"), 500); | |
| }, 1500); | |
| } else { | |
| // Opened as a tab, opener severed by COOP, or remote dashboard using a | |
| // loopback/tunnel callback. Keep the full URL visible as a manual fallback | |
| // in case the opener cannot receive the cross-origin postMessage. | |
| queueStatusUpdate("manual"); | |
| } | |
| } else { | |
| // No code/error in URL or all send methods failed — show URL for manual copy. | |
| // Batch the URL and status update so they render together (React 18 auto-batching). | |
| queueStatusUpdate("manual"); | |
| } | |
| }, []); | |
| return ( | |
| <div className="min-h-screen flex items-center justify-center"> | |
| <div className="text-center p-8 max-w-md"> | |
| {status === "processing" && ( | |
| <> | |
| <div className="size-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center"> | |
| <span className="material-symbols-outlined text-3xl text-primary animate-spin"> | |
| progress_activity | |
| </span> | |
| </div> | |
| <h1 className="text-xl font-semibold mb-2">{t("processing")}</h1> | |
| <p className="text-text-muted">{t("pleaseWait")}</p> | |
| </> | |
| )} | |
| {(status === "success" || status === "done") && ( | |
| <> | |
| <div className="size-16 mx-auto mb-4 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center"> | |
| <span className="material-symbols-outlined text-3xl text-green-600"> | |
| check_circle | |
| </span> | |
| </div> | |
| <h1 className="text-xl font-semibold mb-2">{t("authSuccess")}</h1> | |
| <p className="text-text-muted"> | |
| {status === "success" ? t("windowWillClose") : t("closeTabNow")} | |
| </p> | |
| </> | |
| )} | |
| {status === "manual" && ( | |
| <> | |
| <div className="size-16 mx-auto mb-4 rounded-full bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center"> | |
| <span className="material-symbols-outlined text-3xl text-yellow-600">info</span> | |
| </div> | |
| <h1 className="text-xl font-semibold mb-2">{t("copyUrl")}</h1> | |
| <p className="text-text-muted mb-4">{t("copyUrlManual")}</p> | |
| <div className="bg-surface border border-border rounded-lg p-3 text-left"> | |
| <code className="text-xs break-all">{currentUrl}</code> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |