import React, { useEffect, useRef, useState } from "react"; import "./auth.css"; import { resolveBackendUrl } from "../utils/backend.js"; import { getSessionToken, setSessionToken } from "../utils/api.js"; // Premium /auth — GitPilot account (email/password) + "Continue with GitHub". // GitPilot account = who you are; GitHub = repository access (the device flow). // // One layout for every state: (full-viewport background) wraps a // single . Sign in, create account, and the GitHub authorization // step all render INSIDE that same card — no nested wrappers, no second // rectangle. The GitHub step shows "Connecting to GitHub…" with a spinner and // then either redirects (web OAuth) or shows the device code in place. const api = (path) => `${resolveBackendUrl()}${path}`; // The account session also travels in a header (not just the cross-site cookie) // so email/password sign-in survives the Vercel-frontend / HF-backend split. function sessionHeaders() { const t = getSessionToken(); return t ? { "X-GitPilot-Session": t } : {}; } // Persist the portable session token returned by account endpoints. function rememberSession(data) { if (data && data.session_token) setSessionToken(data.session_token); } async function post(path, body) { const r = await fetch(api(path), { method: "POST", headers: { "content-type": "application/json", ...sessionHeaders() }, credentials: "include", // also send/receive the session cookie when same-origin body: JSON.stringify(body || {}), }); let data = {}; try { data = await r.json(); } catch { /* empty body */ } return { ok: r.ok, status: r.status, data }; } async function getJSON(path, opts = {}) { const r = await fetch(api(path), { credentials: "include", ...opts, headers: { ...sessionHeaders(), ...(opts.headers || {}) }, }); let data = {}; try { data = await r.json(); } catch { /* empty body */ } return { ok: r.ok, status: r.status, data }; } const GithubMark = () => ( ); const Eye = ({ off }) => (off ? : ); export default function AuthPage({ onAuthenticated, backendReady = false, connectMode = false }) { const params = new URLSearchParams(window.location.search); const isVerifyLink = !!params.get("token") && window.location.pathname.includes("verify-email"); // connectMode / ?view=github: jump straight to the GitHub device flow (used by // the in-workspace "Connect GitHub" button for already-signed-in accounts). const wantsGithub = !!params.get("code") || params.get("view") === "github" || connectMode; const [mode, setMode] = useState(params.get("mode") === "signup" ? "signup" : "signin"); // Pick the opening view: GitHub flow → github; an email confirmation link // (?token= on /verify-email) → verify; otherwise email. const [view, setView] = useState( wantsGithub ? "github" : isVerifyLink ? "verify" : "email" ); // "email" | "github" | "verify" const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [name, setName] = useState(""); const [showPw, setShowPw] = useState(false); const [busy, setBusy] = useState(false); const [note, setNote] = useState(null); // {kind, text} // GitHub authorization state (rendered inside the same card) const [ghPhase, setGhPhase] = useState("connecting"); // "connecting" | "device" | "error" const [ghDevice, setGhDevice] = useState(null); // {user_code, verification_uri, ...} const [ghError, setGhError] = useState(""); const pollTimer = useRef(null); const stopPolling = useRef(false); const ghStarted = useRef(false); // Email-verification state (dedicated screen, not a silent redirect) const [verifyPhase, setVerifyPhase] = useState("verifying"); // "verifying" | "success" | "error" const [verifyMsg, setVerifyMsg] = useState(""); const [verifyUser, setVerifyUser] = useState(null); const [resendBusy, setResendBusy] = useState(false); const verifyStarted = useRef(false); // Exchange the email-confirmation token once, then show success/error in place. useEffect(() => { if (view !== "verify" || verifyStarted.current) return; verifyStarted.current = true; const token = params.get("token"); setVerifyPhase("verifying"); (async () => { const r = await post("/api/account/verify-email", { token }); // Strip the token from the URL so a refresh can't replay a stale link. window.history.replaceState({}, document.title, "/auth"); if (r.ok) { rememberSession(r.data); setVerifyUser(r.data); setVerifyPhase("success"); } else { setVerifyMsg(r.data.detail || "This confirmation link is invalid or has expired."); setVerifyPhase("error"); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [view]); const resendVerification = async () => { if (!email) { setNote({ kind: "err", text: "Enter your email to resend the link." }); return; } setResendBusy(true); try { await post("/api/account/resend-verification", { email }); setNote({ kind: "ok", text: "If your account needs confirmation, a new link is on its way." }); } finally { setResendBusy(false); } }; // ── GitHub authorization (device + web OAuth), all inside the same card ── function finishGitHub(data) { stopPolling.current = true; if (pollTimer.current) clearTimeout(pollTimer.current); if (!data || !data.access_token || !data.user) { setGhPhase("error"); setGhError("GitHub returned an incomplete session. Please try again."); return; } try { localStorage.setItem("github_token", data.access_token); localStorage.setItem("github_user", JSON.stringify(data.user)); } catch { /* storage may be blocked */ } if (typeof onAuthenticated === "function") { onAuthenticated({ access_token: data.access_token, user: data.user }); } // Linked from inside the workspace → reload into it with the new GitHub token. if (connectMode) { window.location.href = "/"; } } async function pollDevice(deviceCode, interval) { if (stopPolling.current) return; try { const r = await fetch(api("/api/auth/device/poll"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ device_code: deviceCode }), }); if (r.status === 200) { finishGitHub(await r.json()); return; } if (r.status === 202) { pollTimer.current = setTimeout(() => pollDevice(deviceCode, interval), interval * 1000); return; } const err = await r.json().catch(() => ({ error: "Unknown polling error" })); if (err.error === "slow_down") { pollTimer.current = setTimeout(() => pollDevice(deviceCode, interval + 5), (interval + 5) * 1000); return; } throw new Error(err.error || `Polling failed: ${r.status}`); } catch (e) { if (!stopPolling.current) { setGhPhase("error"); setGhError(e.message || "Could not reach GitHub."); } } } async function startGitHub() { setGhPhase("connecting"); setGhError(""); setGhDevice(null); stopPolling.current = false; // If we returned from a web OAuth redirect, exchange the code first. const code = params.get("code"); const state = params.get("state") || ""; if (code) { window.history.replaceState({}, document.title, window.location.pathname); try { const r = await post("/api/auth/callback", { code, state }); if (r.ok) { finishGitHub(r.data); return; } throw new Error(r.data.detail || "GitHub sign-in failed."); } catch (e) { setGhPhase("error"); setGhError(e.message || "GitHub sign-in failed."); return; } } // Try the standard web OAuth redirect; fall back to device flow. try { const r = await getJSON("/api/auth/url"); if (r.ok && r.data.authorization_url) { if (r.data.state) sessionStorage.setItem("gitpilot_oauth_state", r.data.state); window.location.href = r.data.authorization_url; // redirect to GitHub return; } } catch { /* fall through to device flow */ } // Device flow (no client secret configured): show the code in place. try { const r = await post("/api/auth/device/code", {}); if (r.data.error) { if (String(r.data.error).includes("400") || String(r.data.error).includes("Bad Request")) { throw new Error("Device Flow is disabled for this GitHub App. Enable it under GitHub App Settings → General → 'Enable Device Flow'."); } throw new Error(r.data.error); } if (!r.data.device_code) throw new Error("GitHub did not return a device code."); setGhDevice(r.data); setGhPhase("device"); pollDevice(r.data.device_code, r.data.interval || 5); } catch (e) { setGhPhase("error"); setGhError(e.message || "Could not start GitHub sign-in."); } } // Kick off the GitHub flow when entering the github view. useEffect(() => { if (view === "github") { if (ghStarted.current) return; ghStarted.current = true; startGitHub(); } else { ghStarted.current = false; stopPolling.current = true; if (pollTimer.current) clearTimeout(pollTimer.current); } return () => { stopPolling.current = true; if (pollTimer.current) clearTimeout(pollTimer.current); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [view]); const goEmail = () => { stopPolling.current = true; if (pollTimer.current) clearTimeout(pollTimer.current); setGhDevice(null); setGhError(""); setView("email"); }; const submit = async (e) => { e.preventDefault(); setNote(null); setBusy(true); try { if (mode === "signin") { const r = await post("/api/account/login", { email, password }); if (r.ok) { rememberSession(r.data); if (typeof onAuthenticated === "function") onAuthenticated({ user: r.data }); } else if (r.status === 403) setNote({ kind: "err", text: "Verify your email before signing in." }); else setNote({ kind: "err", text: r.data.detail || "Invalid email or password." }); } else { const r = await post("/api/account/signup", { email, password, name: name || null }); if (r.status === 202) setNote({ kind: "ok", text: "Check your email — we sent a confirmation link." }); else setNote({ kind: "err", text: r.data.detail || "Could not create the account." }); } } catch { setNote({ kind: "err", text: "Network error — is the backend reachable?" }); } finally { setBusy(false); } }; const forgot = async () => { if (!email) { setNote({ kind: "err", text: "Enter your email first." }); return; } await post("/api/account/password/forgot", { email }); setNote({ kind: "ok", text: "If an account exists, we'll send a reset link." }); }; // ── Email-confirmation card (landed from the verify-email link) ── if (view === "verify") { return (
← Back to home
GP {verifyPhase === "verifying" && ( <>

Confirming your email…

Activating your GitPilot account. This only takes a moment.

)} {verifyPhase === "success" && ( <>

Email confirmed

Welcome{verifyUser?.name ? `, ${verifyUser.name}` : ""}! Your GitPilot account is active.

)} {verifyPhase === "error" && ( <>

Confirmation link problem

{verifyMsg}

Links expire after 15 minutes. Enter your email to get a fresh one.

{note &&
{note.text}
}
setEmail(e.target.value)} placeholder="Email" />
)}
© {new Date().getFullYear()} GitPilot Inc.
); } // ── GitHub authorization card (same shell, no nested rectangle) ── if (view === "github") { return (
← Back to home
{connectMode ? : } GP {ghPhase === "connecting" && ( <>

Connecting to GitHub…

Opening a secure authorization session for your repositories.

)} {ghPhase === "device" && ghDevice && ( <>

Authorize GitPilot

Enter this code on GitHub to grant repository access.

Open GitHub to authorize ↗
Waiting for authorization…
)} {ghPhase === "error" && ( <>

GitHub sign-in failed

{ghError}
)}
© {new Date().getFullYear()} GitPilot Inc.
); } // ── Email / password card (sign in + create account share this shell) ── return (
← Back to home
{mode === "signup" && ( )} GP {mode === "signin" ? ( <>

GitPilot Enterprise

Agentic AI workflow for your repositories.
Secure. Context-aware. Automated.

) : ( <>

Create your account

Start using GitPilot with a secure workspace.

)}
or
{note &&
{note.text}
} {mode === "signup" && (
setName(e.target.value)} placeholder="Name (optional)" />
)}
setEmail(e.target.value)} placeholder="Email" />
setPassword(e.target.value)} placeholder="Password" />
{mode === "signin" && (
)}
{mode === "signin" ? ( <>Don't have an account? ) : ( <>Already have an account? )}
© {new Date().getFullYear()} GitPilot Inc.
); }