Skip to content
File

Blob: src/client/pages/login-page.tsx

typescript76 lines
1import { LogIn } from "lucide-react";
2import { Navigate, useLocation, useSearchParams } from "react-router-dom";
3import { useAuth } from "@/client/auth";
4import { Button, Card, ErrorBanner, PageHeader } from "@/client/components/ui";
5 
6const DEFAULT_RETURN_TO = "/app/projects";
7 
8const oidcErrorMessages: Record<string, string> = {
9 identity_conflict:
10 "tessera returned an identity that cannot be safely linked to an anvil account. Ask an operator to review the account binding.",
11 oidc_provider_error: "tessera could not complete sign-in. Try again or ask an operator to check the OIDC provider.",
12 oidc_session_expired: "The sign-in session expired. Start a new tessera sign-in.",
13 oidc_unverified_email: "tessera did not return a verified email address for this account.",
14 tessera_email_conflict:
15 "tessera returned an email address already assigned to another anvil account. Ask an operator to resolve the conflict.",
16 user_disabled: "This anvil account is disabled.",
17};
18 
19const sanitizeReturnTo = (value: string | null): string => {
20 if (!value || !value.startsWith("/") || value.startsWith("//") || value.startsWith("/api/")) {
21 return DEFAULT_RETURN_TO;
22 }
23 
24 return value === "/app/login" || value.startsWith("/app/login?") ? DEFAULT_RETURN_TO : value;
25};
26 
27const getOidcErrorMessage = (code: string | null): string | null => {
28 if (!code) return null;
29 return oidcErrorMessages[code] ?? "Sign-in could not be completed. Start a new tessera sign-in.";
30};
31 
32export const LoginPage = () => {
33 const { isAuthenticated, isInitializing } = useAuth();
34 const location = useLocation();
35 const [searchParams] = useSearchParams();
36 const state = location.state as { returnTo?: unknown } | null;
37 const stateReturnTo = typeof state?.returnTo === "string" ? state.returnTo : null;
38 const returnTo = sanitizeReturnTo(searchParams.get("return_to") ?? stateReturnTo);
39 const errorMessage = getOidcErrorMessage(searchParams.get("error"));
40 
41 if (!isInitializing && isAuthenticated) {
42 return <Navigate to={returnTo} replace />;
43 }
44 
45 const startOidc = () => {
46 window.location.assign(`/api/public/oidc/start?return_to=${encodeURIComponent(returnTo)}`);
47 };
48 
49 return (
50 <div className="mx-auto max-w-3xl animate-slide-up space-y-6">
51 <PageHeader title="Sign in" description="Use your tessera account to open an anvil session." />
52
53 <Card>
54 <div className="space-y-5">
55 {errorMessage ? <ErrorBanner message={errorMessage} /> : null}
56
57 <div>
58 <Button
59 variant="primary"
60 type="button"
61 disabled={isInitializing}
62 icon={<LogIn className="h-4 w-4" aria-hidden="true" />}
63 onClick={startOidc}
64 >
65 Sign in with tessera
66 </Button>
67 <p className="mt-3 text-sm leading-6 text-zinc-500">
68 You will be redirected to tessera and returned here after OIDC verification.
69 </p>
70 </div>
71 </div>
72 </Card>
73 </div>
74 );
75};