From c1cd4f415b867a85789072796e6b333bde873182 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 22:47:56 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routeTree.gen.ts | 51 ++++++++++++++- src/routes/login.tsx | 122 ++++++++++++++++++++++++++++++++++++ src/routes/setup.tsx | 143 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 src/routes/login.tsx create mode 100644 src/routes/setup.tsx diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index d204c26..2c17859 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -9,8 +9,20 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as SetupRouteImport } from './routes/setup' +import { Route as LoginRouteImport } from './routes/login' import { Route as IndexRouteImport } from './routes/index' +const SetupRoute = SetupRouteImport.update({ + id: '/setup', + path: '/setup', + getParentRoute: () => rootRouteImport, +} as any) +const LoginRoute = LoginRouteImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', @@ -19,28 +31,50 @@ const IndexRoute = IndexRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/login': typeof LoginRoute + '/setup': typeof SetupRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/login': typeof LoginRoute + '/setup': typeof SetupRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/login': typeof LoginRoute + '/setup': typeof SetupRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' + fullPaths: '/' | '/login' | '/setup' fileRoutesByTo: FileRoutesByTo - to: '/' - id: '__root__' | '/' + to: '/' | '/login' | '/setup' + id: '__root__' | '/' | '/login' | '/setup' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + LoginRoute: typeof LoginRoute + SetupRoute: typeof SetupRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/setup': { + id: '/setup' + path: '/setup' + fullPath: '/setup' + preLoaderRoute: typeof SetupRouteImport + parentRoute: typeof rootRouteImport + } + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginRouteImport + parentRoute: typeof rootRouteImport + } '/': { id: '/' path: '/' @@ -53,7 +87,18 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + LoginRoute: LoginRoute, + SetupRoute: SetupRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/src/routes/login.tsx b/src/routes/login.tsx new file mode 100644 index 0000000..98e6f15 --- /dev/null +++ b/src/routes/login.tsx @@ -0,0 +1,122 @@ +import { createFileRoute, useNavigate, Link } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { useAuth } from "@/lib/auth"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Scale, Loader2, ShieldCheck } from "lucide-react"; +import { toast } from "sonner"; + +export const Route = createFileRoute("/login")({ + component: LoginPage, +}); + +function LoginPage() { + const { signIn, session, loading } = useAuth(); + const navigate = useNavigate(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + if (!loading && session) navigate({ to: "/" }); + }, [loading, session, navigate]); + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSubmitting(true); + const { error } = await signIn(email.trim(), password); + setSubmitting(false); + if (error) { + toast.error("Sign-in failed", { description: error }); + } else { + navigate({ to: "/" }); + } + }; + + return ( +
+ {/* Brand panel */} +
+
+
+ +
+
+
Counsel
+
+ Law Firm Suite +
+
+
+ +
+

+ Practice management, built for the discipline of law. +

+

+ Cases, clients, documents, time, and billing — secured behind authenticated access and + row-level data protection. Designed for HOA-focused firms and beyond. +

+
+ +
+ + Encrypted in transit · Role-based access · Audit-ready +
+
+ + {/* Form */} +
+ + +
+ + Counsel +
+ Sign in + Restricted access. Contact your administrator for credentials. +
+ +
+
+ + setEmail(e.target.value)} + placeholder="attorney@firm.com" + /> +
+
+ + setPassword(e.target.value)} + /> +
+ +

+ First time setting up the firm?{" "} + + Create the founding admin account + +

+
+
+
+
+
+ ); +} diff --git a/src/routes/setup.tsx b/src/routes/setup.tsx new file mode 100644 index 0000000..334a956 --- /dev/null +++ b/src/routes/setup.tsx @@ -0,0 +1,143 @@ +import { createFileRoute, useNavigate, Link } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/lib/auth"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Loader2, Scale } from "lucide-react"; +import { toast } from "sonner"; + +export const Route = createFileRoute("/setup")({ + component: SetupPage, +}); + +function SetupPage() { + const navigate = useNavigate(); + const { loading, session } = useAuth(); + const [checking, setChecking] = useState(true); + const [allowed, setAllowed] = useState(false); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + // If anyone exists, block access to /setup + (async () => { + const { count } = await supabase + .from("user_roles") + .select("*", { count: "exact", head: true }); + if ((count ?? 0) === 0) setAllowed(true); + setChecking(false); + })(); + }, []); + + useEffect(() => { + if (!loading && session && !allowed) navigate({ to: "/" }); + }, [loading, session, allowed, navigate]); + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSubmitting(true); + const redirectUrl = `${window.location.origin}/`; + const { error } = await supabase.auth.signUp({ + email: email.trim(), + password, + options: { + emailRedirectTo: redirectUrl, + data: { full_name: name.trim() }, + }, + }); + setSubmitting(false); + if (error) { + toast.error("Could not create account", { description: error.message }); + return; + } + toast.success("Founding admin account created"); + navigate({ to: "/" }); + }; + + if (checking) { + return ( +
+ +
+ ); + } + + if (!allowed) { + return ( +
+ + + Setup unavailable + + Firm is already initialized. Use the standard sign-in page. + + + + + + +
+ ); + } + + return ( +
+ + +
+ + Counsel +
+ Create founding admin + + This is a one-time setup. The first account becomes the firm administrator and can invite + attorneys and staff afterward. + +
+ +
+
+ + setName(e.target.value)} /> +
+
+ + setEmail(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +

+ Minimum 10 characters. Avoid commonly leaked passwords. +

+
+ +
+
+
+
+ ); +}