From 669b18e1c9917f1b8e05b429ea3547ea9893a85c Mon Sep 17 00:00:00 2001 From: yushen Date: Thu, 26 Mar 2026 16:33:49 +0800 Subject: [PATCH] feat(auth): skip email verification for CLI login when already authenticated When the browser has an existing valid session and the login page is opened with cli_callback, show a one-click "Authorize CLI" confirmation instead of requiring email verification again. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/web/app/(auth)/login/page.tsx | 121 ++++++++++++++++++++++++----- 1 file changed, 102 insertions(+), 19 deletions(-) diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx index de94a4ee..dc8d0d37 100644 --- a/apps/web/app/(auth)/login/page.tsx +++ b/apps/web/app/(auth)/login/page.tsx @@ -21,6 +21,28 @@ import { InputOTPGroup, InputOTPSlot, } from "@/components/ui/input-otp"; +import type { User } from "@multica/types"; + +function validateCliCallback(cliCallback: string): boolean { + try { + const cbUrl = new URL(cliCallback); + if (cbUrl.protocol !== "http:") return false; + if (cbUrl.hostname !== "localhost" && cbUrl.hostname !== "127.0.0.1") + return false; + return true; + } catch { + return false; + } +} + +function redirectToCliCallback( + cliCallback: string, + token: string, + cliState: string +) { + const separator = cliCallback.includes("?") ? "&" : "?"; + window.location.href = `${cliCallback}${separator}token=${encodeURIComponent(token)}&state=${encodeURIComponent(cliState)}`; +} function LoginPageContent() { const router = useRouter(); @@ -29,12 +51,38 @@ function LoginPageContent() { const hydrateWorkspace = useWorkspaceStore((s) => s.hydrateWorkspace); const searchParams = useSearchParams(); - const [step, setStep] = useState<"email" | "code">("email"); + const [step, setStep] = useState<"email" | "code" | "cli_confirm">("email"); const [email, setEmail] = useState(""); const [code, setCode] = useState(""); const [error, setError] = useState(""); const [submitting, setSubmitting] = useState(false); const [cooldown, setCooldown] = useState(0); + const [existingUser, setExistingUser] = useState(null); + + // Check for existing session when CLI callback is present. + useEffect(() => { + const cliCallback = searchParams.get("cli_callback"); + if (!cliCallback) return; + + const token = localStorage.getItem("multica_token"); + if (!token) return; + + if (!validateCliCallback(cliCallback)) return; + + // Verify the existing token is still valid. + api.setToken(token); + api + .getMe() + .then((user) => { + setExistingUser(user); + setStep("cli_confirm"); + }) + .catch(() => { + // Token expired/invalid — clear and fall through to normal login. + api.setToken(null); + localStorage.removeItem("multica_token"); + }); + }, [searchParams]); useEffect(() => { if (cooldown <= 0) return; @@ -42,6 +90,14 @@ function LoginPageContent() { return () => clearTimeout(timer); }, [cooldown]); + const handleCliAuthorize = async () => { + const cliCallback = searchParams.get("cli_callback")!; + const cliState = searchParams.get("cli_state") || ""; + const token = localStorage.getItem("multica_token")!; + setSubmitting(true); + redirectToCliCallback(cliCallback, token, cliState); + }; + const handleSendCode = async (e?: React.FormEvent) => { e?.preventDefault(); if (!email) { @@ -57,7 +113,9 @@ function LoginPageContent() { setCooldown(10); } catch (err) { setError( - err instanceof Error ? err.message : "Failed to send code. Make sure the server is running." + err instanceof Error + ? err.message + : "Failed to send code. Make sure the server is running." ); } finally { setSubmitting(false); @@ -72,29 +130,14 @@ function LoginPageContent() { try { const cliCallback = searchParams.get("cli_callback"); if (cliCallback) { - // CLI browser login: verify code, get JWT, redirect to CLI callback. - // Only allow http://localhost callbacks to prevent open redirect / JWT theft. - try { - const cbUrl = new URL(cliCallback); - if (cbUrl.protocol !== "http:") { - setError("Invalid callback URL"); - setSubmitting(false); - return; - } - if (cbUrl.hostname !== "localhost" && cbUrl.hostname !== "127.0.0.1") { - setError("Invalid callback URL"); - setSubmitting(false); - return; - } - } catch { + if (!validateCliCallback(cliCallback)) { setError("Invalid callback URL"); setSubmitting(false); return; } const { token } = await api.verifyCode(email, value); const cliState = searchParams.get("cli_state") || ""; - const separator = cliCallback.includes("?") ? "&" : "?"; - window.location.href = `${cliCallback}${separator}token=${encodeURIComponent(token)}&state=${encodeURIComponent(cliState)}`; + redirectToCliCallback(cliCallback, token, cliState); return; } @@ -126,6 +169,46 @@ function LoginPageContent() { } }; + // CLI confirm step: user is already logged in, just authorize. + if (step === "cli_confirm" && existingUser) { + return ( +
+ + + Authorize CLI + + Allow the CLI to access Multica as{" "} + + {existingUser.email} + + ? + + + + + + + +
+ ); + } + if (step === "code") { return (