diff --git a/apps/ui-library/__registry__/index.tsx b/apps/ui-library/__registry__/index.tsx index b1efc02e6ce..a3fcd4b347e 100644 --- a/apps/ui-library/__registry__/index.tsx +++ b/apps/ui-library/__registry__/index.tsx @@ -31,6 +31,18 @@ export const Index: Record = { 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", diff --git a/apps/ui-library/config/docs.ts b/apps/ui-library/config/docs.ts index be5fafe5734..722620c80f6 100644 --- a/apps/ui-library/config/docs.ts +++ b/apps/ui-library/config/docs.ts @@ -45,6 +45,12 @@ export const frameworkPages: Record = { 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 = { 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', diff --git a/apps/ui-library/content/docs/tanstack/client.mdx b/apps/ui-library/content/docs/tanstack/client.mdx index 3dd1e162b8c..8329ca3d77a 100644 --- a/apps/ui-library/content/docs/tanstack/client.mdx +++ b/apps/ui-library/content/docs/tanstack/client.mdx @@ -7,6 +7,19 @@ 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 diff --git a/apps/ui-library/content/docs/tanstack/password-based-auth.mdx b/apps/ui-library/content/docs/tanstack/password-based-auth.mdx new file mode 100644 index 00000000000..6e21f452006 --- /dev/null +++ b/apps/ui-library/content/docs/tanstack/password-based-auth.mdx @@ -0,0 +1,32 @@ +--- +title: Tanstack Start +description: Supabase client for Tanstack Start +--- + +## Installation + + + +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 + + diff --git a/apps/ui-library/public/llms.txt b/apps/ui-library/public/llms.txt index 08e8b0fef88..076d319035c 100644 --- a/apps/ui-library/public/llms.txt +++ b/apps/ui-library/public/llms.txt @@ -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. diff --git a/apps/ui-library/public/r/password-based-auth-tanstack.json b/apps/ui-library/public/r/password-based-auth-tanstack.json new file mode 100644 index 00000000000..d77d3596bcc --- /dev/null +++ b/apps/ui-library/public/r/password-based-auth-tanstack.json @@ -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
\n
\n \n
\n
\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
\n
\n
\n \n \n Sorry, something went wrong.\n \n \n {params?.error ? (\n

Code error: {params.error}

\n ) : (\n

An unspecified error occurred.

\n )}\n
\n
\n
\n
\n
\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

Hello {data.user.email}

\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(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
\n \n \n Login\n Enter your email below to login to your account\n \n \n
\n
\n
\n \n setEmail(e.target.value)}\n />\n
\n
\n
\n \n \n Forgot your password?\n \n
\n setPassword(e.target.value)}\n />\n
\n {error &&

{error}

}\n \n
\n
\n Don't have an account?{' '}\n \n Sign up\n \n
\n
\n
\n
\n
\n )\n}\n", + "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
\n
\n \n
\n
\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
\n
\n
\n \n \n Thank you for signing up!\n Check your email to confirm\n \n \n

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

\n
\n
\n
\n
\n
\n )\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(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
\n \n \n Sign up\n Create a new account\n \n \n
\n
\n
\n \n setEmail(e.target.value)}\n />\n
\n
\n
\n \n
\n setPassword(e.target.value)}\n />\n
\n
\n
\n \n
\n setRepeatPassword(e.target.value)}\n />\n
\n {error &&

{error}

}\n \n
\n
\n Already have an account?{' '}\n \n Login\n \n
\n
\n
\n
\n
\n )\n}\n", + "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
\n
\n \n
\n
\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
\n
\n \n
\n
\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(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
\n \n \n Reset Your Password\n \n Type in your email and we'll send you a link to reset your password\n \n \n \n
\n
\n
\n \n setEmail(e.target.value)}\n />\n
\n {error &&

{error}

}\n \n
\n
\n Already have an account?{' '}\n \n Login\n \n
\n
\n
\n
\n
\n )\n}\n", + "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(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
\n \n \n Reset Your Password\n Please enter your new password below.\n \n \n
\n
\n
\n \n setPassword(e.target.value)}\n />\n
\n {error &&

{error}

}\n \n
\n
\n Already have an account?{' '}\n \n Login\n \n
\n
\n
\n
\n
\n )\n}\n", + "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": "/// \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" + } + ] +} \ No newline at end of file diff --git a/apps/ui-library/registry.json b/apps/ui-library/registry.json index c6635d87d59..911e292cf12 100644 --- a/apps/ui-library/registry.json +++ b/apps/ui-library/registry.json @@ -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", diff --git a/apps/ui-library/registry/blocks.ts b/apps/ui-library/registry/blocks.ts index 6e65db89fb7..7db43270b51 100644 --- a/apps/ui-library/registry/blocks.ts +++ b/apps/ui-library/registry/blocks.ts @@ -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'] diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/forgot-password-form.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/forgot-password-form.tsx new file mode 100644 index 00000000000..d3f3e81190b --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/forgot-password-form.tsx @@ -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(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 ( +
+ + + Reset Your Password + + Type in your email and we'll send you a link to reset your password + + + +
+
+
+ + setEmail(e.target.value)} + /> +
+ {error &&

{error}

} + +
+
+ Already have an account?{' '} + + Login + +
+
+
+
+
+ ) +} diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/login-form.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/login-form.tsx new file mode 100644 index 00000000000..e95417627d2 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/login-form.tsx @@ -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(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 ( +
+ + + Login + Enter your email below to login to your account + + +
+
+
+ + setEmail(e.target.value)} + /> +
+
+
+ + + Forgot your password? + +
+ setPassword(e.target.value)} + /> +
+ {error &&

{error}

} + +
+
+ Don't have an account?{' '} + + Sign up + +
+
+
+
+
+ ) +} diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/sign-up-form.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/sign-up-form.tsx new file mode 100644 index 00000000000..079a3bea5a1 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/sign-up-form.tsx @@ -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(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 ( +
+ + + Sign up + Create a new account + + +
+
+
+ + setEmail(e.target.value)} + /> +
+
+
+ +
+ setPassword(e.target.value)} + /> +
+
+
+ +
+ setRepeatPassword(e.target.value)} + /> +
+ {error &&

{error}

} + +
+
+ Already have an account?{' '} + + Login + +
+
+
+
+
+ ) +} diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/update-password-form.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/update-password-form.tsx new file mode 100644 index 00000000000..a8858306219 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/update-password-form.tsx @@ -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(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 ( +
+ + + Reset Your Password + Please enter your new password below. + + +
+
+
+ + setPassword(e.target.value)} + /> +
+ {error &&

{error}

} + +
+
+ Already have an account?{' '} + + Login + +
+
+
+
+
+ ) +} diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/lib/supabase/fetch-user-server-fn.ts b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/lib/supabase/fetch-user-server-fn.ts new file mode 100644 index 00000000000..f200a6033b8 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/lib/supabase/fetch-user-server-fn.ts @@ -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 +}) diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/registry-item.json b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/registry-item.json new file mode 100644 index 00000000000..36de9cf65cc --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/registry-item.json @@ -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" + } + ] +} diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routeTree.gen.ts b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routeTree.gen.ts new file mode 100644 index 00000000000..40087e7a39c --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routeTree.gen.ts @@ -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() + +/* 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 */ diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/_protected.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/_protected.tsx new file mode 100644 index 00000000000..70ca25fb70c --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/_protected.tsx @@ -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' }) + } + }, +}) diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/_protected/info.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/_protected/info.tsx new file mode 100644 index 00000000000..bbd39020e1f --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/_protected/info.tsx @@ -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

Hello {data.user.email}

+} diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/auth/confirm.ts b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/auth/confirm.ts new file mode 100644 index 00000000000..d19e254a339 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/auth/confirm.ts @@ -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 }), +}) diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/auth/error.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/auth/error.tsx new file mode 100644 index 00000000000..cb7a4669b08 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/auth/error.tsx @@ -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 ( +
+
+
+ + + Sorry, something went wrong. + + + {params?.error ? ( +

Code error: {params.error}

+ ) : ( +

An unspecified error occurred.

+ )} +
+
+
+
+
+ ) +} diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/forgot-password.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/forgot-password.tsx new file mode 100644 index 00000000000..9e6475f68af --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/forgot-password.tsx @@ -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 ( +
+
+ +
+
+ ) +} diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/login.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/login.tsx new file mode 100644 index 00000000000..5680a1a1670 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/login.tsx @@ -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 ( +
+
+ +
+
+ ) +} diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx new file mode 100644 index 00000000000..5a59b834113 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx @@ -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 ( +
+
+
+ + + Thank you for signing up! + Check your email to confirm + + +

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

+
+
+
+
+
+ ) +} diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/sign-up.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/sign-up.tsx new file mode 100644 index 00000000000..661b8ab6cf8 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/sign-up.tsx @@ -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 ( +
+
+ +
+
+ ) +} diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/update-password.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/update-password.tsx new file mode 100644 index 00000000000..9959cb9be34 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/update-password.tsx @@ -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 ( +
+
+ +
+
+ ) +} diff --git a/apps/ui-library/tsconfig.json b/apps/ui-library/tsconfig.json index 9e21bf5ea57..b99c97268c8 100644 --- a/apps/ui-library/tsconfig.json +++ b/apps/ui-library/tsconfig.json @@ -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" + ] }