Add a tanstack block for password-based auth.

This commit is contained in:
Ivan Vasilov
2025-03-22 15:13:42 +01:00
parent d68881f0d5
commit a26a2d36c2
25 changed files with 1297 additions and 2 deletions
+12
View File
@@ -31,6 +31,18 @@ export const Index: Record<string, any> = {
chunks: []
}
,
"password-based-auth-tanstack": {
name: "password-based-auth-tanstack",
type: "registry:block",
registryDependencies: ["button","card","input","label"],
component: React.lazy(() => import("@/registry/default/blocks/password-based-auth-tanstack/routes/login.tsx")),
source: "",
files: ["registry/default/blocks/password-based-auth-tanstack/routes/login.tsx","registry/default/blocks/password-based-auth-tanstack/routes/auth/error.tsx","registry/default/blocks/password-based-auth-tanstack/routes/_protected.tsx","registry/default/blocks/password-based-auth-tanstack/routes/_protected/info.tsx","registry/default/blocks/password-based-auth-tanstack/routes/auth/confirm.ts","registry/default/blocks/password-based-auth-tanstack/components/login-form.tsx","registry/default/blocks/password-based-auth-tanstack/routes/sign-up.tsx","registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx","registry/default/blocks/password-based-auth-tanstack/components/sign-up-form.tsx","registry/default/blocks/password-based-auth-tanstack/routes/forgot-password.tsx","registry/default/blocks/password-based-auth-tanstack/routes/update-password.tsx","registry/default/blocks/password-based-auth-tanstack/components/forgot-password-form.tsx","registry/default/blocks/password-based-auth-tanstack/components/update-password-form.tsx","registry/default/blocks/password-based-auth-tanstack/lib/supabase/fetch-user-server-fn.ts","registry/default/clients/tanstack/lib/supabase/client.ts","registry/default/clients/tanstack/lib/supabase/server.ts"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"dropzone-nextjs": {
name: "dropzone-nextjs",
type: "registry:component",
+18
View File
@@ -45,6 +45,12 @@ export const frameworkPages: Record<string, SidebarNavGroup> = {
nextjs: {
title: 'Next.js',
items: [
{
title: 'Client',
href: '/docs/nextjs/client',
items: [],
commandItemLabel: 'Supabase Client for Next.js',
},
{
title: 'Password-Based Auth',
href: '/docs/nextjs/password-based-auth',
@@ -85,6 +91,18 @@ export const frameworkPages: Record<string, SidebarNavGroup> = {
tanstack: {
title: 'Tanstack Start',
items: [
{
title: 'Client',
href: '/docs/tanstack/client',
items: [],
commandItemLabel: 'Supabase Client for Tanstack Start',
},
{
title: 'Password-Based Auth',
href: '/docs/tanstack/password-based-auth',
items: [],
commandItemLabel: 'Password-Based Auth for Tanstack Start',
},
{
title: 'Dropzone',
href: '/docs/tanstack/dropzone',
@@ -7,6 +7,19 @@ description: Supabase client for Tanstack Start
<BlockItem name="supabase-client-tanstack" description="Supabase Client for Tanstack Start" />
The easiest way to use Tanstack Start with Supabase is to follow the following steps:
1. npx degit https://github.com/tanstack/router/examples/react/start-bare start-bare
2. cd start-basic
3. npm install
4. npm run dev
5. Follow this guide https://ui.shadcn.com/docs/installation/manual to install shadcn in your project
- `"@/*"` should be set to ["./src/*"] in `tsconfig.json`
6. Install this block to get the Supabase clients
7. Add VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY to your .env file
## Folder structure
<RegistryBlock itemName="supabase-client-tanstack" />
@@ -0,0 +1,32 @@
---
title: Tanstack Start
description: Supabase client for Tanstack Start
---
## Installation
<BlockItem name="password-based-auth-tanstack" description="Supabase Client for Tanstack Start" />
1. Follow the steps in /ui/docs/tanstack/password-based-auth to setup your repo.
2. Add the following property to your `createRootRoute` function:
```ts
import { fetchUser } from "@/lib/supabase/fetch-user-server-fn";
...
beforeLoad: async () => {
const user = await fetchUser();
return {
user,
};
},
...
```
3. Try to access the /info route, you should be redirected to login screen. If you create an account and try accessing the /info page, you should see your email.
## Folder structure
<RegistryBlock itemName="password-based-auth-tanstack" />
+1 -1
View File
@@ -1,5 +1,5 @@
# Supabase UI Library
Last updated: 2025-03-21T23:10:11.061Z
Last updated: 2025-03-22T14:10:43.312Z
## Overview
Library of components for your project. The components integrate with Supabase and are shadcn compatible.
@@ -0,0 +1,108 @@
{
"$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 { LoginForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/login-form'\nimport { createFileRoute } from '@tanstack/react-router'\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 { Card, CardContent, CardHeader, CardTitle } from '@/registry/default/components/ui/card'\nimport { createFileRoute } from '@tanstack/react-router'\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\nexport const Route = createFileRoute('/_protected')({\n beforeLoad: ({ context }) => {\n if (!context.user) {\n throw redirect({ to: '/login' })\n }\n },\n})\n",
"type": "registry:file",
"target": "routes/_protected.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/_protected/info.tsx",
"content": "import { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/_protected/info')({\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/info.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/auth/confirm.ts",
"content": "import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/server'\nimport { type EmailOtpType } from '@supabase/supabase-js'\nimport { createFileRoute, redirect } from '@tanstack/react-router'\nimport { createServerFn } from '@tanstack/react-start'\nimport { getWebRequest } from '@tanstack/react-start/server'\n\nconst confirmFn = createServerFn({ method: 'GET' })\n .validator((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 = getWebRequest()\n\n if (!request) {\n throw redirect({ to: `/auth/error`, params: { 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\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 params: { 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 params: { 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 { 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'\nimport { Link, useNavigate } from '@tanstack/react-router'\nimport { useState } from 'react'\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 const supabase = createClient()\n\n const handleLogin = async (e: React.FormEvent) => {\n e.preventDefault()\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: '/info' })\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 { SignUpForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/sign-up-form'\nimport { createFileRoute } from '@tanstack/react-router'\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 {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { createFileRoute } from '@tanstack/react-router'\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'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 { 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'\nimport { Link, useNavigate } from '@tanstack/react-router'\nimport { useState } from 'react'\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 const supabase = createClient()\n\n const handleSignUp = async (e: React.FormEvent) => {\n e.preventDefault()\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 })\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 { ForgotPasswordForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/forgot-password-form'\nimport { createFileRoute } from '@tanstack/react-router'\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 { UpdatePasswordForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/update-password-form'\nimport { createFileRoute } from '@tanstack/react-router'\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 { 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'\nimport { Link } from '@tanstack/react-router'\nimport { useState } from 'react'\n\nexport function ForgotPasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const supabase = createClient()\n\n const handleForgotPassword = async (e: React.FormEvent) => {\n e.preventDefault()\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 } 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>\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 </div>\n )\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/components/update-password-form.tsx",
"content": "import { 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'\nimport { useNavigate } from '@tanstack/react-router'\nimport { useState } from 'react'\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 const supabase = createClient()\n\n const handleForgotPassword = async (e: React.FormEvent) => {\n e.preventDefault()\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: '/info' })\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 <div className=\"mt-4 text-center text-sm\">\n Already have an account?{' '}\n <a href=\"/login\" className=\"underline underline-offset-4\">\n Login\n </a>\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 { createClient } from '@/registry/default/clients/tanstack/lib/supabase/server'\nimport type { Factor, User } from '@supabase/supabase-js'\nimport { createServerFn } from '@tanstack/react-start'\n\ntype SSRSafeUser = User & {\n factors: (Factor & { factor_type: 'phone' | 'totp' })[]\n}\n\nexport const fetchUser = createServerFn({ method: 'GET' }).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_ANON_KEY!\n )\n}\n",
"type": "registry:lib"
},
{
"path": "registry/default/clients/tanstack/lib/supabase/server.ts",
"content": "import { createServerClient } from '@supabase/ssr'\nimport { parseCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(process.env.VITE_SUPABASE_URL!, process.env.VITE_SUPABASE_ANON_KEY!, {\n cookies: {\n getAll() {\n return Object.entries(parseCookies()).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",
"type": "registry:lib"
}
]
}
+83
View File
@@ -116,6 +116,89 @@
],
"dependencies": ["@supabase/supabase-js@latest"]
},
{
"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",
"registryDependencies": ["button", "card", "input", "label"],
"dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"],
"files": [
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/login.tsx",
"type": "registry:file",
"target": "routes/login.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/auth/error.tsx",
"type": "registry:file",
"target": "routes/auth/error.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/_protected.tsx",
"type": "registry:file",
"target": "routes/_protected.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/_protected/info.tsx",
"type": "registry:file",
"target": "routes/_protected/info.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/auth/confirm.ts",
"type": "registry:file",
"target": "routes/auth/confirm.ts"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/components/login-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/sign-up.tsx",
"type": "registry:file",
"target": "routes/sign-up.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx",
"type": "registry:file",
"target": "routes/sign-up-success.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/components/sign-up-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/forgot-password.tsx",
"type": "registry:file",
"target": "routes/forgot-password.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/update-password.tsx",
"type": "registry:file",
"target": "routes/update-password.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/components/forgot-password-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/components/update-password-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/lib/supabase/fetch-user-server-fn.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/tanstack/lib/supabase/client.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/tanstack/lib/supabase/server.ts",
"type": "registry:lib"
}
]
},
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "dropzone-nextjs",
+5
View File
@@ -3,6 +3,8 @@ import { clients } from './clients'
import dropzone from './default/blocks/dropzone/registry-item.json' assert { type: 'json' }
import passwordBasedAuthNextjs from './default/blocks/password-based-auth-nextjs/registry-item.json' assert { type: 'json' }
import passwordBasedAuthReact from './default/blocks/password-based-auth-react/registry-item.json' assert { type: 'json' }
import passwordBasedAuthTanstack from './default/blocks/password-based-auth-tanstack/registry-item.json' assert { type: 'json' }
import realtimeCursor from './default/blocks/realtime-cursor/registry-item.json' assert { type: 'json' }
import { registryItemAppend } from './utils'
@@ -20,9 +22,12 @@ const combine = (component: Registry['items'][number]) => {
const nextjsClient = clients.find((client) => client.name === 'supabase-client-nextjs')
const reactClient = clients.find((client) => client.name === 'supabase-client-react')
const tanstackClient = clients.find((client) => client.name === 'supabase-client-tanstack')
export const blocks = [
registryItemAppend(passwordBasedAuthNextjs as RegistryItem, [nextjsClient!]),
registryItemAppend(passwordBasedAuthReact as RegistryItem, [reactClient!]),
registryItemAppend(passwordBasedAuthTanstack as RegistryItem, [tanstackClient!]),
...combine(dropzone as RegistryItem),
...combine(realtimeCursor as RegistryItem),
] as Registry['items']
@@ -0,0 +1,79 @@
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import { Link } from '@tanstack/react-router'
import { useState } from 'react'
export function ForgotPasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const supabase = createClient()
const handleForgotPassword = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setError(null)
try {
// 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
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: 'http://localhost:3000/update-password',
})
if (error) throw error
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Reset Your Password</CardTitle>
<CardDescription>
Type in your email and we&apos;ll send you a link to reset your password
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleForgotPassword}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Sending...' : 'Send reset email'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<Link to="/login" className="underline underline-offset-4">
Login
</Link>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,99 @@
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import { Link, useNavigate } from '@tanstack/react-router'
import { useState } from 'react'
export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const navigate = useNavigate()
const supabase = createClient()
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setError(null)
try {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) throw error
// Update this route to redirect to an authenticated route. The user already has an active session.
await navigate({ to: '/info' })
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>Enter your email below to login to your account</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleLogin}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<Link
to="/forgot-password"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</Link>
</div>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{' '}
<Link to="/sign-up" className="underline underline-offset-4">
Sign up
</Link>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,110 @@
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import { Link, useNavigate } from '@tanstack/react-router'
import { useState } from 'react'
export function SignUpForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [repeatPassword, setRepeatPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const navigate = useNavigate()
const supabase = createClient()
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault()
setError(null)
if (password !== repeatPassword) {
setError('Passwords do not match')
return
}
setIsLoading(true)
try {
const { error } = await supabase.auth.signUp({
email,
password,
})
if (error) throw error
await navigate({ to: '/sign-up-success' })
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Sign up</CardTitle>
<CardDescription>Create a new account</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSignUp}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
</div>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="repeat-password">Repeat Password</Label>
</div>
<Input
id="repeat-password"
type="password"
required
value={repeatPassword}
onChange={(e) => setRepeatPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Creating an account...' : 'Sign up'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<Link to="/login" className="underline underline-offset-4">
Login
</Link>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,77 @@
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import { useNavigate } from '@tanstack/react-router'
import { useState } from 'react'
export function UpdatePasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const navigate = useNavigate()
const supabase = createClient()
const handleForgotPassword = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setError(null)
try {
const { error } = await supabase.auth.updateUser({ password })
if (error) throw error
// Update this route to redirect to an authenticated route. The user already has an active session.
await navigate({ to: '/info' })
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Reset Your Password</CardTitle>
<CardDescription>Please enter your new password below.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleForgotPassword}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="password">New password</Label>
<Input
id="password"
type="password"
placeholder="New password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Saving...' : 'Save new password'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<a href="/login" className="underline underline-offset-4">
Login
</a>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,18 @@
import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/server'
import type { Factor, User } from '@supabase/supabase-js'
import { createServerFn } from '@tanstack/react-start'
type SSRSafeUser = User & {
factors: (Factor & { factor_type: 'phone' | 'totp' })[]
}
export const fetchUser = createServerFn({ method: 'GET' }).handler(async () => {
const supabase = createClient()
const { data, error } = await supabase.auth.getUser()
if (error) {
return null
}
return data.user as SSRSafeUser
})
@@ -0,0 +1,75 @@
{
"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",
"registryDependencies": ["button", "card", "input", "label"],
"dependencies": ["@supabase/ssr@latest"],
"files": [
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/login.tsx",
"type": "registry:file",
"target": "routes/login.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/auth/error.tsx",
"type": "registry:file",
"target": "routes/auth/error.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/_protected.tsx",
"type": "registry:file",
"target": "routes/_protected.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/_protected/info.tsx",
"type": "registry:file",
"target": "routes/_protected/info.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/auth/confirm.ts",
"type": "registry:file",
"target": "routes/auth/confirm.ts"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/components/login-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/sign-up.tsx",
"type": "registry:file",
"target": "routes/sign-up.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx",
"type": "registry:file",
"target": "routes/sign-up-success.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/components/sign-up-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/forgot-password.tsx",
"type": "registry:file",
"target": "routes/forgot-password.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/update-password.tsx",
"type": "registry:file",
"target": "routes/update-password.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/components/forgot-password-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/components/update-password-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/lib/supabase/fetch-user-server-fn.ts",
"type": "registry:lib"
}
]
}
@@ -0,0 +1,338 @@
// SUPABASE NOTE: THIS FILE WAS ADDED TO SATISFY TANSTACK BUILD CONFIGURATION. IT IS NOT INCLUDED IN THE BLOCK.
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
// Import Routes
import { Route as rootRoute } from './routes/__root'
import { Route as ProtectedImport } from './routes/_protected'
import { Route as ProtectedInfoImport } from './routes/_protected/info'
import { Route as AuthConfirmImport } from './routes/auth/confirm'
import { Route as AuthErrorImport } from './routes/auth/error'
import { Route as ForgotPasswordImport } from './routes/forgot-password'
import { Route as IndexImport } from './routes/index'
import { Route as LoginImport } from './routes/login'
import { Route as SignUpImport } from './routes/sign-up'
import { Route as SignUpSuccessImport } from './routes/sign-up-success'
import { Route as UpdatePasswordImport } from './routes/update-password'
// Create/Update Routes
const UpdatePasswordRoute = UpdatePasswordImport.update({
id: '/update-password',
path: '/update-password',
getParentRoute: () => rootRoute,
} as any)
const SignUpSuccessRoute = SignUpSuccessImport.update({
id: '/sign-up-success',
path: '/sign-up-success',
getParentRoute: () => rootRoute,
} as any)
const SignUpRoute = SignUpImport.update({
id: '/sign-up',
path: '/sign-up',
getParentRoute: () => rootRoute,
} as any)
const LoginRoute = LoginImport.update({
id: '/login',
path: '/login',
getParentRoute: () => rootRoute,
} as any)
const ForgotPasswordRoute = ForgotPasswordImport.update({
id: '/forgot-password',
path: '/forgot-password',
getParentRoute: () => rootRoute,
} as any)
const ProtectedRoute = ProtectedImport.update({
id: '/_protected',
getParentRoute: () => rootRoute,
} as any)
const IndexRoute = IndexImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRoute,
} as any)
const AuthErrorRoute = AuthErrorImport.update({
id: '/auth/error',
path: '/auth/error',
getParentRoute: () => rootRoute,
} as any)
const AuthConfirmRoute = AuthConfirmImport.update({
id: '/auth/confirm',
path: '/auth/confirm',
getParentRoute: () => rootRoute,
} as any)
const ProtectedInfoRoute = ProtectedInfoImport.update({
id: '/info',
path: '/info',
getParentRoute: () => ProtectedRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/_protected': {
id: '/_protected'
path: ''
fullPath: ''
preLoaderRoute: typeof ProtectedImport
parentRoute: typeof rootRoute
}
'/forgot-password': {
id: '/forgot-password'
path: '/forgot-password'
fullPath: '/forgot-password'
preLoaderRoute: typeof ForgotPasswordImport
parentRoute: typeof rootRoute
}
'/login': {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginImport
parentRoute: typeof rootRoute
}
'/sign-up': {
id: '/sign-up'
path: '/sign-up'
fullPath: '/sign-up'
preLoaderRoute: typeof SignUpImport
parentRoute: typeof rootRoute
}
'/sign-up-success': {
id: '/sign-up-success'
path: '/sign-up-success'
fullPath: '/sign-up-success'
preLoaderRoute: typeof SignUpSuccessImport
parentRoute: typeof rootRoute
}
'/update-password': {
id: '/update-password'
path: '/update-password'
fullPath: '/update-password'
preLoaderRoute: typeof UpdatePasswordImport
parentRoute: typeof rootRoute
}
'/_protected/info': {
id: '/_protected/info'
path: '/info'
fullPath: '/info'
preLoaderRoute: typeof ProtectedInfoImport
parentRoute: typeof ProtectedImport
}
'/auth/confirm': {
id: '/auth/confirm'
path: '/auth/confirm'
fullPath: '/auth/confirm'
preLoaderRoute: typeof AuthConfirmImport
parentRoute: typeof rootRoute
}
'/auth/error': {
id: '/auth/error'
path: '/auth/error'
fullPath: '/auth/error'
preLoaderRoute: typeof AuthErrorImport
parentRoute: typeof rootRoute
}
}
}
// Create and export the route tree
interface ProtectedRouteChildren {
ProtectedInfoRoute: typeof ProtectedInfoRoute
}
const ProtectedRouteChildren: ProtectedRouteChildren = {
ProtectedInfoRoute: ProtectedInfoRoute,
}
const ProtectedRouteWithChildren = ProtectedRoute._addFileChildren(ProtectedRouteChildren)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'': typeof ProtectedRouteWithChildren
'/forgot-password': typeof ForgotPasswordRoute
'/login': typeof LoginRoute
'/sign-up': typeof SignUpRoute
'/sign-up-success': typeof SignUpSuccessRoute
'/update-password': typeof UpdatePasswordRoute
'/info': typeof ProtectedInfoRoute
'/auth/confirm': typeof AuthConfirmRoute
'/auth/error': typeof AuthErrorRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'': typeof ProtectedRouteWithChildren
'/forgot-password': typeof ForgotPasswordRoute
'/login': typeof LoginRoute
'/sign-up': typeof SignUpRoute
'/sign-up-success': typeof SignUpSuccessRoute
'/update-password': typeof UpdatePasswordRoute
'/info': typeof ProtectedInfoRoute
'/auth/confirm': typeof AuthConfirmRoute
'/auth/error': typeof AuthErrorRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/_protected': typeof ProtectedRouteWithChildren
'/forgot-password': typeof ForgotPasswordRoute
'/login': typeof LoginRoute
'/sign-up': typeof SignUpRoute
'/sign-up-success': typeof SignUpSuccessRoute
'/update-password': typeof UpdatePasswordRoute
'/_protected/info': typeof ProtectedInfoRoute
'/auth/confirm': typeof AuthConfirmRoute
'/auth/error': typeof AuthErrorRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| ''
| '/forgot-password'
| '/login'
| '/sign-up'
| '/sign-up-success'
| '/update-password'
| '/info'
| '/auth/confirm'
| '/auth/error'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| ''
| '/forgot-password'
| '/login'
| '/sign-up'
| '/sign-up-success'
| '/update-password'
| '/info'
| '/auth/confirm'
| '/auth/error'
id:
| '__root__'
| '/'
| '/_protected'
| '/forgot-password'
| '/login'
| '/sign-up'
| '/sign-up-success'
| '/update-password'
| '/_protected/info'
| '/auth/confirm'
| '/auth/error'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ProtectedRoute: typeof ProtectedRouteWithChildren
ForgotPasswordRoute: typeof ForgotPasswordRoute
LoginRoute: typeof LoginRoute
SignUpRoute: typeof SignUpRoute
SignUpSuccessRoute: typeof SignUpSuccessRoute
UpdatePasswordRoute: typeof UpdatePasswordRoute
AuthConfirmRoute: typeof AuthConfirmRoute
AuthErrorRoute: typeof AuthErrorRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ProtectedRoute: ProtectedRouteWithChildren,
ForgotPasswordRoute: ForgotPasswordRoute,
LoginRoute: LoginRoute,
SignUpRoute: SignUpRoute,
SignUpSuccessRoute: SignUpSuccessRoute,
UpdatePasswordRoute: UpdatePasswordRoute,
AuthConfirmRoute: AuthConfirmRoute,
AuthErrorRoute: AuthErrorRoute,
}
export const routeTree = rootRoute
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.tsx",
"children": [
"/",
"/_protected",
"/forgot-password",
"/login",
"/sign-up",
"/sign-up-success",
"/update-password",
"/auth/confirm",
"/auth/error"
]
},
"/": {
"filePath": "index.tsx"
},
"/_protected": {
"filePath": "_protected.tsx",
"children": [
"/_protected/info"
]
},
"/forgot-password": {
"filePath": "forgot-password.tsx"
},
"/login": {
"filePath": "login.tsx"
},
"/sign-up": {
"filePath": "sign-up.tsx"
},
"/sign-up-success": {
"filePath": "sign-up-success.tsx"
},
"/update-password": {
"filePath": "update-password.tsx"
},
"/_protected/info": {
"filePath": "_protected/info.tsx",
"parent": "/_protected"
},
"/auth/confirm": {
"filePath": "auth/confirm.ts"
},
"/auth/error": {
"filePath": "auth/error.tsx"
}
}
}
ROUTE_MANIFEST_END */
@@ -0,0 +1,9 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_protected')({
beforeLoad: ({ context }) => {
if (!context.user) {
throw redirect({ to: '/login' })
}
},
})
@@ -0,0 +1,16 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_protected/info')({
component: Info,
loader: async ({ context }) => {
return {
user: context.user!,
}
},
})
function Info() {
const data = Route.useLoaderData()
return <p>Hello {data.user.email}</p>
}
@@ -0,0 +1,62 @@
import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/server'
import { type EmailOtpType } from '@supabase/supabase-js'
import { createFileRoute, redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { getWebRequest } from '@tanstack/react-start/server'
const confirmFn = createServerFn({ method: 'GET' })
.validator((searchParams: unknown) => {
if (
searchParams &&
typeof searchParams === 'object' &&
'token_hash' in searchParams &&
'type' in searchParams &&
'next' in searchParams
) {
return searchParams
}
throw new Error('Invalid search params')
})
.handler(async (ctx) => {
const request = getWebRequest()
if (!request) {
throw redirect({ to: `/auth/error`, params: { error: 'No request' } })
}
const searchParams = ctx.data
const token_hash = searchParams['token_hash'] as string
const type = searchParams['type'] as EmailOtpType | null
const next = (searchParams['next'] ?? '/') as string
if (token_hash && type) {
const supabase = createClient()
const { error } = await supabase.auth.verifyOtp({
type,
token_hash,
})
console.log(error?.message)
if (!error) {
// redirect user to specified redirect URL or root of app
throw redirect({ href: next })
} else {
// redirect the user to an error page with some instructions
throw redirect({
to: `/auth/error`,
params: { error: error?.message },
})
}
}
// redirect the user to an error page with some instructions
throw redirect({
to: `/auth/error`,
params: { error: 'No token hash or type' },
})
})
export const Route = createFileRoute('/auth/confirm')({
preload: false,
loader: (opts) => confirmFn({ data: opts.location.search }),
})
@@ -0,0 +1,37 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/registry/default/components/ui/card'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/auth/error')({
component: AuthError,
validateSearch: (params) => {
if (params.error && typeof params.error === 'string') {
return { error: params.error }
}
return null
},
})
function AuthError() {
const params = Route.useSearch()
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<div className="flex flex-col gap-6">
<Card>
<CardHeader>
<CardTitle className="text-2xl">Sorry, something went wrong.</CardTitle>
</CardHeader>
<CardContent>
{params?.error ? (
<p className="text-sm text-muted-foreground">Code error: {params.error}</p>
) : (
<p className="text-sm text-muted-foreground">An unspecified error occurred.</p>
)}
</CardContent>
</Card>
</div>
</div>
</div>
)
}
@@ -0,0 +1,16 @@
import { ForgotPasswordForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/forgot-password-form'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/forgot-password')({
component: ForgotPassword,
})
function ForgotPassword() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<ForgotPasswordForm />
</div>
</div>
)
}
@@ -0,0 +1,16 @@
import { LoginForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/login-form'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/login')({
component: Login,
})
function Login() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<LoginForm />
</div>
</div>
)
}
@@ -0,0 +1,35 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/sign-up-success')({
component: SignUpSuccess,
})
function SignUpSuccess() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<div className="flex flex-col gap-6">
<Card>
<CardHeader>
<CardTitle className="text-2xl">Thank you for signing up!</CardTitle>
<CardDescription>Check your email to confirm</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
You've successfully signed up. Please check your email to confirm your account
before signing in.
</p>
</CardContent>
</Card>
</div>
</div>
</div>
)
}
@@ -0,0 +1,16 @@
import { SignUpForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/sign-up-form'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/sign-up')({
component: SignUp,
})
function SignUp() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<SignUpForm />
</div>
</div>
)
}
@@ -0,0 +1,16 @@
import { UpdatePasswordForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/update-password-form'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/update-password')({
component: UpdatePassword,
})
function UpdatePassword() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<UpdatePasswordForm />
</div>
</div>
)
}
+6 -1
View File
@@ -36,5 +36,10 @@
".contentlayer/generated",
"./../../packages/ui/src/**/*.d.ts"
],
"exclude": ["node_modules", "./scripts/build-registry.mts"]
"exclude": [
"node_modules",
"./scripts/build-registry.mts",
// This file is only used in a registry block and not included in the app.
"./registry/default/blocks/password-based-auth-tanstack/lib/supabase/fetch-user-server-fn.ts"
]
}