{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "password-based-auth-react-router",
  "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",
    "https://supabase.com/library/r/safe-next-path.json"
  ],
  "files": [
    {
      "path": "registry/default/blocks/password-based-auth-react-router/app/routes/auth.confirm.tsx",
      "content": "import { type EmailOtpType } from '@supabase/supabase-js'\nimport { redirect, type LoaderFunctionArgs } from 'react-router'\n\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\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 { useSearchParams } from 'react-router'\n\nimport { Card, CardContent, CardHeader, CardTitle } from '@/registry/default/components/ui/card'\n\nexport default function Page() {\n  let [searchParams] = useSearchParams()\n\n  return (\n    <div className=\"flex min-h-svh w-full items-center justify-center p-6 md:p-10\">\n      <div className=\"w-full max-w-sm\">\n        <div className=\"flex flex-col gap-6\">\n          <Card>\n            <CardHeader>\n              <CardTitle className=\"font-medium text-lg tracking-normal\">\n                Sorry, something went wrong.\n              </CardTitle>\n            </CardHeader>\n            <CardContent>\n              {searchParams?.get('error') ? (\n                <p className=\"text-sm text-muted-foreground\">\n                  Code error: {searchParams?.get('error')}\n                </p>\n              ) : (\n                <p className=\"text-sm text-muted-foreground\">An unspecified error occurred.</p>\n              )}\n            </CardContent>\n          </Card>\n        </div>\n      </div>\n    </div>\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 {\n  data,\n  Link,\n  redirect,\n  useFetcher,\n  useSearchParams,\n  type ActionFunctionArgs,\n} from 'react-router'\n\nimport { 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'\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<typeof action>()\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    <div className=\"flex min-h-svh w-full items-center justify-center p-6 md:p-10\">\n      <div className=\"w-full max-w-sm\">\n        <div className=\"flex flex-col gap-6\">\n          {success ? (\n            <Card>\n              <CardHeader>\n                <CardTitle className=\"font-medium text-lg tracking-normal\">\n                  Check Your Email\n                </CardTitle>\n                <CardDescription>Password reset instructions sent</CardDescription>\n              </CardHeader>\n              <CardContent>\n                <p className=\"text-sm text-muted-foreground\">\n                  If you registered using your email and password, you will receive a password reset\n                  email.\n                </p>\n              </CardContent>\n            </Card>\n          ) : (\n            <Card>\n              <CardHeader>\n                <CardTitle className=\"font-medium text-lg tracking-normal\">\n                  Reset Your Password\n                </CardTitle>\n                <CardDescription>\n                  Type in your email and we&apos;ll send you a link to reset your password\n                </CardDescription>\n              </CardHeader>\n              <CardContent>\n                <fetcher.Form method=\"post\">\n                  <div className=\"flex flex-col gap-6\">\n                    <div className=\"grid gap-2\">\n                      <Label htmlFor=\"email\">Email</Label>\n                      <Input\n                        id=\"email\"\n                        name=\"email\"\n                        type=\"email\"\n                        placeholder=\"m@example.com\"\n                        required\n                      />\n                    </div>\n                    {error && <p className=\"text-sm text-red-500\">{error}</p>}\n                    <Button type=\"submit\" className=\"w-full\" disabled={loading}>\n                      {loading ? 'Sending...' : 'Send reset email'}\n                    </Button>\n                  </div>\n                  <div className=\"mt-4 text-center text-sm\">\n                    Already have an account?{' '}\n                    <Link to=\"/login\" className=\"underline underline-offset-4\">\n                      Sign in\n                    </Link>\n                  </div>\n                </fetcher.Form>\n              </CardContent>\n            </Card>\n          )}\n        </div>\n      </div>\n    </div>\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 { Link, redirect, useFetcher, useSearchParams, type ActionFunctionArgs } from 'react-router'\n\nimport { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'\nimport { 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'\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n  const { supabase, headers } = createClient(request)\n  const origin = new URL(request.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\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(safeNextPath(formData.get('next'), '/protected', origin), { headers })\n}\n\nexport default function Login() {\n  const fetcher = useFetcher<typeof action>()\n  const [searchParams] = useSearchParams()\n\n  const error = fetcher.data?.error\n  const loading = fetcher.state === 'submitting'\n\n  return (\n    <div className=\"flex min-h-svh w-full items-center justify-center p-6 md:p-10\">\n      <div className=\"w-full max-w-sm\">\n        <div className=\"flex flex-col gap-6\">\n          <Card>\n            <CardHeader>\n              <CardTitle className=\"font-medium text-lg tracking-normal\">Sign in</CardTitle>\n              <CardDescription>Enter your email below to sign in to your account</CardDescription>\n            </CardHeader>\n            <CardContent>\n              <fetcher.Form method=\"post\">\n                <input type=\"hidden\" name=\"next\" value={searchParams.get('next') ?? ''} />\n                <div className=\"flex flex-col gap-6\">\n                  <div className=\"grid gap-2\">\n                    <Label htmlFor=\"email\">Email</Label>\n                    <Input\n                      id=\"email\"\n                      name=\"email\"\n                      type=\"email\"\n                      placeholder=\"m@example.com\"\n                      required\n                    />\n                  </div>\n                  <div className=\"grid gap-2\">\n                    <div className=\"flex items-center\">\n                      <Label htmlFor=\"password\">Password</Label>\n                      <Link\n                        to=\"/forgot-password\"\n                        className=\"ml-auto inline-block text-sm underline-offset-4 hover:underline\"\n                      >\n                        Forgot your password?\n                      </Link>\n                    </div>\n                    <Input id=\"password\" type=\"password\" name=\"password\" required />\n                  </div>\n                  {error && <p className=\"text-sm text-red-500\">{error}</p>}\n                  <Button type=\"submit\" className=\"w-full\" disabled={loading}>\n                    {loading ? 'Signing in...' : 'Sign in'}\n                  </Button>\n                </div>\n                <div className=\"mt-4 text-center text-sm\">\n                  Don&apos;t have an account?{' '}\n                  <Link to=\"/sign-up\" className=\"underline underline-offset-4\">\n                    Sign up\n                  </Link>\n                </div>\n              </fetcher.Form>\n            </CardContent>\n          </Card>\n        </div>\n      </div>\n    </div>\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 { redirect, type ActionFunctionArgs } from 'react-router'\n\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\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 { redirect, useLoaderData, type LoaderFunctionArgs } from 'react-router'\n\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\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<typeof loader>()\n\n  return (\n    <div className=\"flex items-center justify-center h-screen gap-2\">\n      <p>\n        Hello <span className=\"text-primary font-semibold\">{data.user.email}</span>\n      </p>\n      <a href=\"/logout\">\n        <Button>Sign out</Button>\n      </a>\n    </div>\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 { Link, redirect, useFetcher, useSearchParams, type ActionFunctionArgs } from 'react-router'\n\nimport { 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'\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<typeof action>()\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    <div className=\"flex min-h-svh w-full items-center justify-center p-6 md:p-10\">\n      <div className=\"w-full max-w-sm\">\n        <div className=\"flex flex-col gap-6\">\n          {success ? (\n            <Card>\n              <CardHeader>\n                <CardTitle className=\"font-medium text-lg tracking-normal\">\n                  Thank you for signing up!\n                </CardTitle>\n                <CardDescription>Check your email to confirm</CardDescription>\n              </CardHeader>\n              <CardContent>\n                <p className=\"text-sm text-muted-foreground\">\n                  You&apos;ve successfully signed up. Please check your email to confirm your\n                  account before signing in.\n                </p>\n              </CardContent>\n            </Card>\n          ) : (\n            <Card>\n              <CardHeader>\n                <CardTitle className=\"font-medium text-lg tracking-normal\">Sign up</CardTitle>\n                <CardDescription>Create a new account</CardDescription>\n              </CardHeader>\n              <CardContent>\n                <fetcher.Form method=\"post\">\n                  <div className=\"flex flex-col gap-6\">\n                    <div className=\"grid gap-2\">\n                      <Label htmlFor=\"email\">Email</Label>\n                      <Input\n                        id=\"email\"\n                        name=\"email\"\n                        type=\"email\"\n                        placeholder=\"m@example.com\"\n                        required\n                      />\n                    </div>\n                    <div className=\"grid gap-2\">\n                      <div className=\"flex items-center\">\n                        <Label htmlFor=\"password\">Password</Label>\n                      </div>\n                      <Input id=\"password\" name=\"password\" type=\"password\" required />\n                    </div>\n                    <div className=\"grid gap-2\">\n                      <div className=\"flex items-center\">\n                        <Label htmlFor=\"repeat-password\">Repeat Password</Label>\n                      </div>\n                      <Input id=\"repeat-password\" name=\"repeat-password\" type=\"password\" required />\n                    </div>\n                    {error && <p className=\"text-sm text-red-500\">{error}</p>}\n                    <Button type=\"submit\" className=\"w-full\" disabled={loading}>\n                      {loading ? 'Creating an account...' : 'Sign up'}\n                    </Button>\n                  </div>\n                  <div className=\"mt-4 text-center text-sm\">\n                    Already have an account?{' '}\n                    <Link to=\"/login\" className=\"underline underline-offset-4\">\n                      Sign in\n                    </Link>\n                  </div>\n                </fetcher.Form>\n              </CardContent>\n            </Card>\n          )}\n        </div>\n      </div>\n    </div>\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 { redirect, useFetcher, type ActionFunctionArgs } from 'react-router'\n\nimport { 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'\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<typeof action>()\n\n  const error = fetcher.data?.error\n  const loading = fetcher.state === 'submitting'\n\n  return (\n    <div className=\"flex min-h-svh w-full items-center justify-center p-6 md:p-10\">\n      <div className=\"w-full max-w-sm\">\n        <div className=\"flex flex-col gap-6\">\n          <Card>\n            <CardHeader>\n              <CardTitle className=\"font-medium text-lg tracking-normal\">\n                Reset Your Password\n              </CardTitle>\n              <CardDescription>Please enter your new password below.</CardDescription>\n            </CardHeader>\n            <CardContent>\n              <fetcher.Form method=\"post\">\n                <div className=\"flex flex-col gap-6\">\n                  <div className=\"grid gap-2\">\n                    <Label htmlFor=\"password\">New password</Label>\n                    <Input\n                      id=\"password\"\n                      name=\"password\"\n                      type=\"password\"\n                      placeholder=\"New password\"\n                      required\n                    />\n                  </div>\n                  {error && <p className=\"text-sm text-red-500\">{error}</p>}\n                  <Button type=\"submit\" className=\"w-full\" disabled={loading}>\n                    {loading ? 'Saving...' : 'Save new password'}\n                  </Button>\n                </div>\n              </fetcher.Form>\n            </CardContent>\n          </Card>\n        </div>\n      </div>\n    </div>\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": "/// <reference types=\"vite/types/importMeta.d.ts\" />\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_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_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_KEY": ""
  },
  "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.",
  "type": "registry:block"
}