{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "password-based-auth-react-router", "type": "registry:block", "title": "Password Based Auth flow for React Router and Supabase", "description": "Password Based Auth flow for React Router and Supabase", "dependencies": [ "@supabase/ssr@latest", "@react-router/dev@latest", "@react-router/fs-routes@latest", "@supabase/supabase-js@latest" ], "registryDependencies": [ "button", "card", "input", "label" ], "files": [ { "path": "registry/default/blocks/password-based-auth-react-router/app/routes/auth.confirm.tsx", "content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { type EmailOtpType } from '@supabase/supabase-js'\nimport { type LoaderFunctionArgs, redirect } from 'react-router'\n\nexport async function loader({ request }: LoaderFunctionArgs) {\n const requestUrl = new URL(request.url)\n const token_hash = requestUrl.searchParams.get('token_hash')\n const type = requestUrl.searchParams.get('type') as EmailOtpType | null\n const _next = requestUrl.searchParams.get('next')\n const next = _next?.startsWith('/') ? _next : '/'\n\n if (token_hash && type) {\n const { supabase, headers } = createClient(request)\n const { error } = await supabase.auth.verifyOtp({\n type,\n token_hash,\n })\n if (!error) {\n return redirect(next, { headers })\n } else {\n return redirect(`/auth/error?error=${error?.message}`)\n }\n }\n\n // redirect the user to an error page with some instructions\n return redirect(`/auth/error?error=No token hash or type`)\n}\n", "type": "registry:file", "target": "app/routes/auth.confirm.tsx" }, { "path": "registry/default/blocks/password-based-auth-react-router/app/routes/auth.error.tsx", "content": "import { Card, CardContent, CardHeader, CardTitle } from '@/registry/default/components/ui/card'\nimport { useSearchParams } from 'react-router'\n\nexport default function Page() {\n let [searchParams] = useSearchParams()\n\n return (\n
\n
\n
\n \n \n Sorry, something went wrong.\n \n \n {searchParams?.get('error') ? (\n

\n Code error: {searchParams?.get('error')}\n

\n ) : (\n

An unspecified error occurred.

\n )}\n
\n
\n
\n
\n
\n )\n}\n", "type": "registry:file", "target": "app/routes/auth.error.tsx" }, { "path": "registry/default/blocks/password-based-auth-react-router/app/routes/forgot-password.tsx", "content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport {\n type ActionFunctionArgs,\n Link,\n data,\n redirect,\n useFetcher,\n useSearchParams,\n} from 'react-router'\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n const formData = await request.formData()\n const email = formData.get('email') as string\n\n const { supabase, headers } = createClient(request)\n const origin = new URL(request.url).origin\n\n // Send the actual reset password email\n const { error } = await supabase.auth.resetPasswordForEmail(email, {\n redirectTo: `${origin}/auth/confirm?next=/update-password`,\n })\n\n if (error) {\n return data(\n {\n error: error instanceof Error ? error.message : 'An error occurred',\n data: { email },\n },\n { headers }\n )\n }\n\n return redirect('/forgot-password?success')\n}\n\nexport default function ForgotPassword() {\n const fetcher = useFetcher()\n let [searchParams] = useSearchParams()\n\n const success = !!searchParams.has('success')\n const error = fetcher.data?.error\n const loading = fetcher.state === 'submitting'\n\n return (\n
\n
\n
\n {success ? (\n \n \n Check Your Email\n Password reset instructions sent\n \n \n

\n If you registered using your email and password, you will receive a password reset\n email.\n

\n
\n
\n ) : (\n \n \n Reset Your Password\n \n Type in your email and we'll send you a link to reset your password\n \n \n \n \n
\n
\n \n \n
\n {error &&

{error}

}\n \n
\n
\n Already have an account?{' '}\n \n Login\n \n
\n
\n
\n
\n )}\n
\n
\n
\n )\n}\n", "type": "registry:file", "target": "app/routes/forgot-password.tsx" }, { "path": "registry/default/blocks/password-based-auth-react-router/app/routes/login.tsx", "content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { type ActionFunctionArgs, Link, redirect, useFetcher } from 'react-router'\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n const { supabase, headers } = createClient(request)\n\n const formData = await request.formData()\n\n const email = formData.get('email') as string\n const password = formData.get('password') as string\n\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n\n if (error) {\n return {\n error: error instanceof Error ? error.message : 'An error occurred',\n }\n }\n\n // Update this route to redirect to an authenticated route. The user already has an active session.\n return redirect('/protected', { headers })\n}\n\nexport default function Login() {\n const fetcher = useFetcher()\n\n const error = fetcher.data?.error\n const loading = fetcher.state === 'submitting'\n\n return (\n
\n
\n
\n \n \n Login\n Enter your email below to login to your account\n \n \n \n
\n
\n \n \n
\n
\n
\n \n \n Forgot your password?\n \n
\n \n
\n {error &&

{error}

}\n \n
\n
\n Don't have an account?{' '}\n \n Sign up\n \n
\n
\n
\n
\n
\n
\n
\n )\n}\n", "type": "registry:file", "target": "app/routes/login.tsx" }, { "path": "registry/default/blocks/password-based-auth-react-router/app/routes/logout.tsx", "content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { type ActionFunctionArgs, redirect } from 'react-router'\n\nexport async function loader({ request }: ActionFunctionArgs) {\n const { supabase, headers } = createClient(request)\n\n const { error } = await supabase.auth.signOut()\n\n if (error) {\n console.error(error)\n return { success: false, error: error.message }\n }\n\n // Redirect to dashboard or home page after successful sign-in\n return redirect('/', { headers })\n}\n", "type": "registry:file", "target": "app/routes/logout.tsx" }, { "path": "registry/default/blocks/password-based-auth-react-router/app/routes/protected.tsx", "content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport { type LoaderFunctionArgs, redirect, useLoaderData } from 'react-router'\n\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n const { supabase } = createClient(request)\n\n const { data, error } = await supabase.auth.getUser()\n if (error || !data?.user) {\n return redirect('/login')\n }\n\n return data\n}\n\nexport default function ProtectedPage() {\n let data = useLoaderData()\n\n return (\n
\n

\n Hello {data.user.email}\n

\n \n \n \n
\n )\n}\n", "type": "registry:file", "target": "app/routes/protected.tsx" }, { "path": "registry/default/blocks/password-based-auth-react-router/app/routes/sign-up.tsx", "content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { type ActionFunctionArgs, Link, redirect, useFetcher, useSearchParams } from 'react-router'\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n const { supabase } = createClient(request)\n\n const url = new URL(request.url)\n const origin = url.origin\n\n const formData = await request.formData()\n\n const email = formData.get('email') as string\n const password = formData.get('password') as string\n const repeatPassword = formData.get('repeat-password') as string\n\n if (!password) {\n return {\n error: 'Password is required',\n }\n }\n\n if (password !== repeatPassword) {\n return { error: 'Passwords do not match' }\n }\n\n const { error } = await supabase.auth.signUp({\n email,\n password,\n options: {\n emailRedirectTo: `${origin}/protected`,\n },\n })\n\n if (error) {\n return { error: error.message }\n }\n\n return redirect('/sign-up?success')\n}\n\nexport default function SignUp() {\n const fetcher = useFetcher()\n let [searchParams] = useSearchParams()\n\n const success = !!searchParams.has('success')\n const error = fetcher.data?.error\n const loading = fetcher.state === 'submitting'\n\n return (\n
\n
\n
\n {success ? (\n \n \n Thank you for signing up!\n Check your email to confirm\n \n \n

\n You've successfully signed up. Please check your email to confirm your account\n before signing in.\n

\n
\n
\n ) : (\n \n \n Sign up\n Create a new account\n \n \n \n
\n
\n \n \n
\n
\n
\n \n
\n \n
\n
\n
\n \n
\n \n
\n {error &&

{error}

}\n \n
\n
\n Already have an account?{' '}\n \n Login\n \n
\n
\n
\n
\n )}\n
\n
\n
\n )\n}\n", "type": "registry:file", "target": "app/routes/sign-up.tsx" }, { "path": "registry/default/blocks/password-based-auth-react-router/app/routes/update-password.tsx", "content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { type ActionFunctionArgs, redirect, useFetcher } from 'react-router'\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n const { supabase, headers } = createClient(request)\n const formData = await request.formData()\n const password = formData.get('password') as string\n\n if (!password) {\n return { error: 'Password is required' }\n }\n\n const { error } = await supabase.auth.updateUser({ password: password })\n\n if (error) {\n return {\n error: error instanceof Error ? error.message : 'An error occurred',\n }\n }\n\n // Redirect to sign-in page after successful password update\n return redirect('/protected', { headers })\n}\n\nexport default function Page() {\n const fetcher = useFetcher()\n\n const error = fetcher.data?.error\n const loading = fetcher.state === 'submitting'\n\n return (\n
\n
\n
\n \n \n Reset Your Password\n Please enter your new password below.\n \n \n \n
\n
\n \n \n
\n {error &&

{error}

}\n \n
\n
\n
\n
\n
\n
\n
\n )\n}\n", "type": "registry:file", "target": "app/routes/update-password.tsx" }, { "path": "registry/default/blocks/password-based-auth-react-router/app/routes.ts", "content": "import { type RouteConfig } from '@react-router/dev/routes'\nimport { flatRoutes } from '@react-router/fs-routes'\n\nexport default flatRoutes() satisfies RouteConfig\n", "type": "registry:file", "target": "app/routes.ts" }, { "path": "registry/default/clients/react-router/lib/supabase/client.ts", "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/react-router/lib/supabase/server.ts", "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" }, "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." }