import { useQuery } from "@tanstack/react-query"; import { Github } from "lucide-react"; import { useEffect, useState } from "react"; import { useNavigate, useSearchParams } from "react-router"; import { TurnstileWidget } from "@/client/components/turnstile-widget"; import { Button } from "@/client/components/ui/button"; import { GoogleIcon } from "@/client/components/ui/google-icon"; import { Input } from "@/client/components/ui/input"; import { PageError } from "@/client/components/ui/page-error"; import { useSession } from "@/client/lib/auth-client"; import { type SocialProvider, useClientConfig } from "@/client/lib/config"; import { apiJsonRequest } from "@/client/lib/http"; import { normalizeLocalPath } from "@/client/lib/navigation"; import { queryKeys } from "@/client/lib/query"; import { shouldRedirectSignedInFromSignIn, signedOAuthQueryFromParams, socialErrorCallbackURL, } from "@/client/lib/sign-in-oauth"; import { buildSocialSignInRequest } from "@/client/lib/social"; interface OAuthClientPublicInfo { client_id: string; client_name?: string | null; } // Errors set on the URL by Better Auth's social-callback `errorCallbackURL` // or by tessera's own redirects. Unrecognized codes fall through to the // generic "social_unavailable" copy rather than throwing the user back to // /auth-error mid-flow. const SIGN_IN_ERROR_MESSAGES: Record = { social_unavailable: "Could not sign in with that provider. Please try again or use email and password.", signup_disabled: "tessera is invite-only. Ask an admin for an invite.", }; const TURNSTILE_UNAVAILABLE_MESSAGE = "Human verification is unavailable. Sign-in is disabled until this is fixed; contact an administrator."; export const SignInPage = () => { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const redirectTo = normalizeLocalPath(searchParams.get("redirect")); const signedOAuthQuery = signedOAuthQueryFromParams(searchParams); const oauthClientId = signedOAuthQuery ? (searchParams.get("client_id") ?? "") : ""; const session = useSession(); const initialErrorCode = searchParams.get("error"); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [turnstileToken, setTurnstileToken] = useState(null); const [resetKey, setResetKey] = useState(0); const [submitting, setSubmitting] = useState(false); // Lazy initializer so the lookup runs once at mount instead of on every // render. The query string does not change between renders during a // single page session. const [error, setError] = useState(() => initialErrorCode ? (SIGN_IN_ERROR_MESSAGES[initialErrorCode] ?? SIGN_IN_ERROR_MESSAGES.social_unavailable) : null, ); const [socialPending, setSocialPending] = useState(null); const config = useClientConfig(); // `unavailable` covers two distinct failure modes: // - /api/config reports the worker is missing TURNSTILE_SITE_KEY // - the embedded Turnstile widget itself fails to load (network / // CSP / runtime), reported via TurnstileWidget.onUnavailable // Either disables the form; the widget signal is local because // /api/config can succeed but the script can still fail to load. const [widgetUnavailable, setWidgetUnavailable] = useState(false); const turnstileUnavailable = config?.status === "unavailable" || widgetUnavailable; const siteKey = config?.status === "ok" ? config.turnstileSiteKey : null; const socialProviders: SocialProvider[] = config?.status === "ok" ? config.socialProviders : []; const signedInUserId = session.data?.user.id ?? null; useEffect(() => { if ( shouldRedirectSignedInFromSignIn({ sessionPending: session.isPending, signedInUserId, signedOAuthQuery, }) ) { navigate(redirectTo, { replace: true }); } }, [session.isPending, signedInUserId, signedOAuthQuery, navigate, redirectTo]); const { data: oauthClientInfo = null } = useQuery({ queryKey: queryKeys.preloginClient(oauthClientId || null, signedOAuthQuery), queryFn: async () => { if (!signedOAuthQuery) return null; const res = await apiJsonRequest( "/api/auth/oauth2/public-client-prelogin", { client_id: oauthClientId, oauth_query: signedOAuthQuery }, { method: "POST" }, ); return res.ok ? ((await res.json()) as OAuthClientPublicInfo) : null; }, enabled: Boolean(oauthClientId && signedOAuthQuery), }); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); if (!turnstileToken) { setError("Please complete the verification challenge."); return; } setSubmitting(true); try { const signInBody: { email: string; oauth_query?: string; password: string; turnstileToken: string } = { email, password, turnstileToken, }; if (signedOAuthQuery) signInBody.oauth_query = signedOAuthQuery; const res = await apiJsonRequest("/api/sign-in", signInBody, { method: "POST" }); const body = (await res.json().catch(() => null)) as { redirect?: boolean; url?: string; message?: unknown; } | null; if (!res.ok) { setError(typeof body?.message === "string" && body.message.length > 0 ? body.message : "Sign-in failed."); setResetKey((n) => n + 1); return; } if (body?.redirect && body.url) { window.location.assign(body.url); return; } await session.refetch(); navigate(redirectTo, { replace: true }); } catch { setError("Network error. Please try again."); setResetKey((n) => n + 1); } finally { setSubmitting(false); } }; const handleSocial = async (provider: SocialProvider) => { setError(null); if (!turnstileToken) { setError("Please complete the verification challenge."); return; } setSocialPending(provider); try { // errorCallbackURL keeps the signed `oauth_query` so an IdP failure // during an RP-initiated flow lands the user back on /sign-in still // attached to the same /authorize, instead of stranding them outside // the RP completion path. Signed params come first so the extractor's // stop-at-sig invariant survives the local `error` append. const errorCallbackURL = socialErrorCallbackURL(signedOAuthQuery); const body = buildSocialSignInRequest({ provider, callbackURL: redirectTo, errorCallbackURL, turnstileToken, oauthQuery: signedOAuthQuery, }); const res = await apiJsonRequest("/api/sign-in/social", body, { method: "POST" }); const payload = (await res.json().catch(() => null)) as { url?: string; message?: unknown; } | null; if (!res.ok) { setError( typeof payload?.message === "string" && payload.message.length > 0 ? payload.message : `Could not sign in with ${provider}.`, ); setResetKey((n) => n + 1); setSocialPending(null); return; } if (payload?.url) { window.location.assign(payload.url); return; } setError(`Could not sign in with ${provider}: no redirect URL returned.`); setSocialPending(null); } catch (e) { setError(e instanceof Error ? e.message : `Failed to sign in with ${provider}.`); setSocialPending(null); } }; if (session.isPending) return null; // A signed-in user landing on /sign-in normally redirects via the // effect above. When a signed RP-initiated /authorize query is present // (e.g. `prompt=login`), keep the form rendered so the user can // re-authenticate; otherwise the RP's authorization request is dropped. if (signedInUserId && !signedOAuthQuery) return null; const oauthClientName = oauthClientInfo?.client_name ?? oauthClientInfo?.client_id ?? null; // Reserve a slot for the "Continue to " subtitle so the layout // does not bump down when oauthClientInfo resolves after the initial // render. Empty when the user did not arrive from /authorize, sized // to match the resolved-state height when they did. const reserveSubtitleSpace = Boolean(oauthClientId); // Surface the turnstile-unavailable banner without round-tripping // through useState — the form is already disabled in that branch and // dismiss should be a no-op while the failure persists. const displayError = error ?? (turnstileUnavailable ? TURNSTILE_UNAVAILABLE_MESSAGE : null); return (

Sign in

{oauthClientName ? ( <>

Continue to {oauthClientName}.

{oauthClientInfo?.client_name ? (

{oauthClientInfo.client_id}

) : null} ) : null}
setError(null)} className="mb-5" />
setEmail(e.target.value)} /> setPassword(e.target.value)} />
{turnstileUnavailable ? (

Verification is unavailable. Sign-in is disabled.

) : siteKey ? ( setWidgetUnavailable(true)} resetKey={resetKey} /> ) : (

Loading verification…

)}
{socialProviders.length > 0 ? (
{socialProviders.includes("github") ? ( ) : null} {socialProviders.includes("google") ? ( ) : null}
) : null}

A tessera, in Roman antiquity, is a small ceramic or bronze tile bearing an identification mark — presented at a checkpoint, recognized, admitted.

); };