Skip to content
File

Blob: src/client/components/auth/login-page.tsx

typescript80 lines
1import { useEffect } from "react";
2import { useNavigate, useSearch } from "@tanstack/react-router";
3import { LogIn, AlertCircle } from "lucide-react";
4import {
5 BUTTON_BASE_CLASSES,
6 BUTTON_SIZE_CLASSES,
7 BUTTON_VARIANT_CLASSES,
8} from "@/client/components/ui/button-classes";
9import { useDocumentTitle } from "@/client/hooks/use-document-title";
10import { useAuthStore, selectIsAuthenticated } from "@/client/stores/auth-store";
11 
12const ERROR_MESSAGES: Record<string, string> = {
13 oidc_session_expired: "Your sign-in session expired. Please try again.",
14 oidc_provider_error: "The identity provider returned an error. Please try again.",
15 oidc_unverified_email: "Your tessera email is not verified yet. Verify it and retry.",
16 oidc_post_callback_refresh_failed: "Sign-in completed but the session could not be confirmed. Please try again.",
17 tessera_email_conflict: "That email is already used by another account.",
18 identity_conflict: "This identity could not be linked. Contact support.",
19};
20 
21function describeError(code: string | undefined): string | null {
22 if (!code) return null;
23 return ERROR_MESSAGES[code] ?? "Sign-in failed. Please try again.";
24}
25 
26function buildOidcStartUrl(returnTo: string | undefined): string {
27 const target = returnTo && returnTo.startsWith("/") ? returnTo : "/";
28 const params = new URLSearchParams({ return_to: target });
29 return `/api/v1/oidc/start?${params.toString()}`;
30}
31 
32export function LoginPage() {
33 const navigate = useNavigate();
34 const { redirect: redirectTo, error } = useSearch({ from: "/login" });
35 const isAuthenticated = useAuthStore(selectIsAuthenticated);
36 useDocumentTitle("Login");
37 
38 useEffect(() => {
39 if (!isAuthenticated) return;
40 navigate({ to: redirectTo || "/" });
41 }, [isAuthenticated, navigate, redirectTo]);
42 
43 const errorMessage = describeError(typeof error === "string" ? error : undefined);
44 const oidcStartUrl = buildOidcStartUrl(typeof redirectTo === "string" ? redirectTo : undefined);
45 
46 return (
47 <div className="flex min-h-[calc(100vh-8rem)] items-center justify-center px-4">
48 <div className="animate-slide-up w-full max-w-sm">
49 <div className="mb-8 text-center">
50 <h1 className="font-display text-2xl font-bold tracking-tight text-zinc-100">Welcome back</h1>
51 <p className="mt-2 text-sm text-zinc-400">Sign in to your bland account</p>
52 </div>
53
54 {errorMessage && (
55 <div
56 id="login-error"
57 role="alert"
58 className="mb-4 flex items-start gap-2 rounded-lg border border-red-500/20 bg-red-500/10 px-3 py-2.5 text-sm text-red-400"
59 >
60 <AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
61 <span>{errorMessage}</span>
62 </div>
63 )}
64
65 <a
66 href={oidcStartUrl}
67 className={`${BUTTON_BASE_CLASSES} ${BUTTON_SIZE_CLASSES.md} ${BUTTON_VARIANT_CLASSES.primary} w-full`}
68 >
69 <LogIn className="h-4 w-4" />
70 Sign in with tessera
71 </a>
72
73 <p className="mt-6 text-center text-sm text-zinc-500">
74 Have an invite link? Paste it in your browser to join a workspace.
75 </p>
76 </div>
77 </div>
78 );
79}