Files
supabase/apps/ui-library/public/r/password-based-auth-react-router.json
Ivan Vasilov 7fd37e6150 chore: Bump shadcn (#50517)
This PR bumps `shadcn` and regenerates all blocks with the latest CLI.
The blocks have no meaningful change (only a json property reorder).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Updated internal development tooling used by the UI library and Vue
blocks.
* **Tests**
* Improved type consistency in registry-related test utilities,
supporting more reliable validation without changing user-facing
behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-18 10:38:49 +10:00

91 lines
22 KiB
JSON

{
"$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=\"text-2xl\">Sorry, something went wrong.</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=\"text-2xl\">Check Your Email</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=\"text-2xl\">Reset Your Password</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=\"text-2xl\">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=\"text-2xl\">Thank you for signing up!</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=\"text-2xl\">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=\"text-2xl\">Reset Your Password</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"
}