Files
supabase/apps/ui-library/public/r/password-based-auth-tanstack.json
Ivan Vasilov 69ce915a9e chore: Rename SUPABASE_PUBLISHABLE_OR_ANON_KEY to SUPABASE_PUBLISHABLE_KEY for all blocks (#42652)
This PR renames all `SUPABASE_PUBLISHABLE_OR_ANON_KEY` env vars into
`SUPABASE_PUBLISHABLE_KEY` to make the new API keys default. This is in
coordination with the rest of the docs.

I've also cleaned up the `blocks/vue` package from unused files.

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

* **Breaking Changes**
* Public environment variable names renamed from PUBLISHABLE_OR_ANON_KEY
→ PUBLISHABLE_KEY across all framework integrations; update your
environment configs.

* **Documentation**
* All framework guides, .env examples and registry docs updated to use
the new variable names.

* **Chores**
* Cleaned up UI registry/templates: some example Vue registry items and
autogenerated registry artifacts were removed or simplified.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-11 10:23:16 +01:00

113 lines
26 KiB
JSON

{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "password-based-auth-tanstack",
"type": "registry:block",
"title": "Password Based Auth flow for tanstack and Supabase",
"description": "Password Based Auth flow for tanstack and Supabase",
"dependencies": [
"@supabase/ssr@latest",
"@supabase/supabase-js@latest"
],
"registryDependencies": [
"button",
"card",
"input",
"label"
],
"files": [
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/login.tsx",
"content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { LoginForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/login-form'\n\nexport const Route = createFileRoute('/login')({\n component: Login,\n})\n\nfunction Login() {\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 <LoginForm />\n </div>\n </div>\n )\n}\n",
"type": "registry:file",
"target": "routes/login.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/auth/error.tsx",
"content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { Card, CardContent, CardHeader, CardTitle } from '@/registry/default/components/ui/card'\n\nexport const Route = createFileRoute('/auth/error')({\n component: AuthError,\n validateSearch: (params) => {\n if (params.error && typeof params.error === 'string') {\n return { error: params.error }\n }\n return null\n },\n})\n\nfunction AuthError() {\n const params = Route.useSearch()\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 {params?.error ? (\n <p className=\"text-sm text-muted-foreground\">Code error: {params.error}</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": "routes/auth/error.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/_protected.tsx",
"content": "import { createFileRoute, redirect } from '@tanstack/react-router'\n\nimport { fetchUser } from '@/registry/default/blocks/password-based-auth-tanstack/lib/supabase/fetch-user-server-fn'\n\nexport const Route = createFileRoute('/_protected')({\n beforeLoad: async () => {\n const user = await fetchUser()\n\n if (!user) {\n throw redirect({ to: '/login' })\n }\n\n return {\n user,\n }\n },\n})\n",
"type": "registry:file",
"target": "routes/_protected.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/_protected/protected.tsx",
"content": "import { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/_protected/protected')({\n component: Info,\n loader: async ({ context }) => {\n return {\n user: context.user!,\n }\n },\n})\n\nfunction Info() {\n const data = Route.useLoaderData()\n\n return <p>Hello {data.user.email}</p>\n}\n",
"type": "registry:file",
"target": "routes/_protected/protected.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/auth/confirm.ts",
"content": "import { type EmailOtpType } from '@supabase/supabase-js'\nimport { createFileRoute, redirect } from '@tanstack/react-router'\nimport { createServerFn } from '@tanstack/react-start'\nimport { getRequest } from '@tanstack/react-start/server'\n\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/server'\n\nconst confirmFn = createServerFn({ method: 'GET' })\n .inputValidator((searchParams: unknown) => {\n if (\n searchParams &&\n typeof searchParams === 'object' &&\n 'token_hash' in searchParams &&\n 'type' in searchParams &&\n 'next' in searchParams\n ) {\n return searchParams\n }\n throw new Error('Invalid search params')\n })\n .handler(async (ctx) => {\n const request = getRequest()\n\n if (!request) {\n throw redirect({ to: `/auth/error`, search: { error: 'No request' } })\n }\n\n const searchParams = ctx.data\n const token_hash = searchParams['token_hash'] as string\n const type = searchParams['type'] as EmailOtpType | null\n const _next = searchParams['next'] as string\n const next = _next?.startsWith('/') ? _next : '/'\n\n if (token_hash && type) {\n const supabase = createClient()\n\n const { error } = await supabase.auth.verifyOtp({\n type,\n token_hash,\n })\n console.log(error?.message)\n if (!error) {\n // redirect user to specified redirect URL or root of app\n throw redirect({ href: next })\n } else {\n // redirect the user to an error page with some instructions\n throw redirect({\n to: `/auth/error`,\n search: { error: error?.message },\n })\n }\n }\n\n // redirect the user to an error page with some instructions\n throw redirect({\n to: `/auth/error`,\n search: { error: 'No token hash or type' },\n })\n })\n\nexport const Route = createFileRoute('/auth/confirm')({\n preload: false,\n loader: (opts) => confirmFn({ data: opts.location.search }),\n})\n",
"type": "registry:file",
"target": "routes/auth/confirm.ts"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/components/login-form.tsx",
"content": "import { Link, useNavigate } from '@tanstack/react-router'\nimport { useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\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 function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const navigate = useNavigate()\n\n const handleLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n if (error) throw error\n // Update this route to redirect to an authenticated route. The user already has an active session.\n await navigate({ to: '/protected' })\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n <div className={cn('flex flex-col gap-6', className)} {...props}>\n <Card>\n <CardHeader>\n <CardTitle className=\"text-2xl\">Login</CardTitle>\n <CardDescription>Enter your email below to login to your account</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleLogin}>\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 type=\"email\"\n placeholder=\"m@example.com\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\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\n id=\"password\"\n type=\"password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n />\n </div>\n {error && <p className=\"text-sm text-red-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Logging in...' : 'Login'}\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 </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/sign-up.tsx",
"content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { SignUpForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/sign-up-form'\n\nexport const Route = createFileRoute('/sign-up')({\n component: SignUp,\n})\n\nfunction SignUp() {\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 <SignUpForm />\n </div>\n </div>\n )\n}\n",
"type": "registry:file",
"target": "routes/sign-up.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx",
"content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\n\nexport const Route = createFileRoute('/sign-up-success')({\n component: SignUpSuccess,\n})\n\nfunction SignUpSuccess() {\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\">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 account\n before signing in.\n </p>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n )\n}\n",
"type": "registry:file",
"target": "routes/sign-up-success.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/components/sign-up-form.tsx",
"content": "import { Link, useNavigate } from '@tanstack/react-router'\nimport { useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\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 function SignUpForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [repeatPassword, setRepeatPassword] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const navigate = useNavigate()\n\n const handleSignUp = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setError(null)\n\n if (password !== repeatPassword) {\n setError('Passwords do not match')\n return\n }\n setIsLoading(true)\n\n try {\n const { error } = await supabase.auth.signUp({\n email,\n password,\n options: {\n emailRedirectTo: `${window.location.origin}/protected`,\n },\n })\n if (error) throw error\n await navigate({ to: '/sign-up-success' })\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n <div className={cn('flex flex-col gap-6', className)} {...props}>\n <Card>\n <CardHeader>\n <CardTitle className=\"text-2xl\">Sign up</CardTitle>\n <CardDescription>Create a new account</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSignUp}>\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 type=\"email\"\n placeholder=\"m@example.com\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\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\n id=\"password\"\n type=\"password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n />\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\n id=\"repeat-password\"\n type=\"password\"\n required\n value={repeatPassword}\n onChange={(e) => setRepeatPassword(e.target.value)}\n />\n </div>\n {error && <p className=\"text-sm text-red-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? '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 Login\n </Link>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/forgot-password.tsx",
"content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { ForgotPasswordForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/forgot-password-form'\n\nexport const Route = createFileRoute('/forgot-password')({\n component: ForgotPassword,\n})\n\nfunction ForgotPassword() {\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 <ForgotPasswordForm />\n </div>\n </div>\n )\n}\n",
"type": "registry:file",
"target": "routes/forgot-password.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/update-password.tsx",
"content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { UpdatePasswordForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/update-password-form'\n\nexport const Route = createFileRoute('/update-password')({\n component: UpdatePassword,\n})\n\nfunction UpdatePassword() {\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 <UpdatePasswordForm />\n </div>\n </div>\n )\n}\n",
"type": "registry:file",
"target": "routes/update-password.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/components/forgot-password-form.tsx",
"content": "import { Link } from '@tanstack/react-router'\nimport { useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\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 function ForgotPasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [success, setSuccess] = useState(false)\n const [isLoading, setIsLoading] = useState(false)\n\n const handleForgotPassword = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n // The url which will be included in the email. This URL needs to be configured in your redirect URLs in the Supabase dashboard at https://supabase.com/dashboard/project/_/auth/url-configuration\n const { error } = await supabase.auth.resetPasswordForEmail(email, {\n redirectTo: 'http://localhost:3000/update-password',\n })\n if (error) throw error\n setSuccess(true)\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n <div className={cn('flex flex-col gap-6', className)} {...props}>\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 <form onSubmit={handleForgotPassword}>\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 type=\"email\"\n placeholder=\"m@example.com\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n />\n </div>\n {error && <p className=\"text-sm text-red-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? '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 Login\n </Link>\n </div>\n </form>\n </CardContent>\n </Card>\n )}\n </div>\n )\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/components/update-password-form.tsx",
"content": "import { useNavigate } from '@tanstack/react-router'\nimport { useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\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 function UpdatePasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [password, setPassword] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const navigate = useNavigate()\n\n const handleForgotPassword = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.updateUser({ password })\n if (error) throw error\n // Update this route to redirect to an authenticated route. The user already has an active session.\n await navigate({ to: '/protected' })\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n <div className={cn('flex flex-col gap-6', className)} {...props}>\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 <form onSubmit={handleForgotPassword}>\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 type=\"password\"\n placeholder=\"New password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n />\n </div>\n {error && <p className=\"text-sm text-red-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Saving...' : 'Save new password'}\n </Button>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/lib/supabase/fetch-user-server-fn.ts",
"content": "import type { Factor, User } from '@supabase/supabase-js'\nimport { createServerFn } from '@tanstack/react-start'\n\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/server'\n\ntype SSRSafeUser = User & {\n factors: (Factor & { factor_type: 'phone' | 'totp' })[]\n}\n\nexport const fetchUser: () => Promise<SSRSafeUser | null> = createServerFn({\n method: 'GET',\n}).handler(async () => {\n const supabase = createClient()\n const { data, error } = await supabase.auth.getUser()\n\n if (error) {\n return null\n }\n\n return data.user as SSRSafeUser\n})\n",
"type": "registry:lib"
},
{
"path": "registry/default/clients/tanstack/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/tanstack/lib/supabase/server.ts",
"content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\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`."
}