feat: Add social login to the UI Library (#34803)

* Add social login block.

* Regen the registry.

* Fix the nextjs social auth.

* Add social auth blocks for RR, Tanstack and React.

* Minor fixes.

* Add docs.

* Update the docs.

* Minor fixes to the blocks.

* Update the docs.

* Fix various doc issues.

* Fix the redirect in the password-based auth.

* Fix note about supabase clients in docs.

* Use with instead of assert in the registry imports.

* Update all auth blocks to use /protected.

* Update all docs for the password-based auth.

* Add new label to social auth.

* Fix docs issues.

* Light mode fix

* Smol fixes

* Fix the origin in the login route.

* Add social auth to the landing page.

* Regenerate the registry.

---------

Co-authored-by: Terry Sutton <saltcod@gmail.com>
This commit is contained in:
Ivan Vasilov
2025-04-17 14:57:09 +02:00
committed by GitHub
parent b7e9b82a44
commit 2ebf81c601
63 changed files with 2014 additions and 76 deletions
+49 -1
View File
@@ -49,7 +49,55 @@ export const Index: Record<string, any> = {
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"],
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/protected.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: []
}
,
"social-auth-nextjs": {
name: "social-auth-nextjs",
type: "registry:block",
registryDependencies: ["button","card"],
component: React.lazy(() => import("@/registry/default/blocks/social-auth-nextjs/app/auth/login/page.tsx")),
source: "",
files: ["registry/default/blocks/social-auth-nextjs/app/auth/login/page.tsx","registry/default/blocks/social-auth-nextjs/app/auth/error/page.tsx","registry/default/blocks/social-auth-nextjs/app/protected/page.tsx","registry/default/blocks/social-auth-nextjs/app/auth/oauth/route.ts","registry/default/blocks/social-auth-nextjs/components/login-form.tsx","registry/default/blocks/social-auth-nextjs/middleware.ts","registry/default/blocks/social-auth-nextjs/components/logout-button.tsx","registry/default/clients/nextjs/lib/supabase/client.ts","registry/default/clients/nextjs/lib/supabase/middleware.ts","registry/default/clients/nextjs/lib/supabase/server.ts"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"social-auth-react": {
name: "social-auth-react",
type: "registry:block",
registryDependencies: ["button","card"],
component: React.lazy(() => import("@/registry/default/blocks/social-auth-react/components/login-form.tsx")),
source: "",
files: ["registry/default/blocks/social-auth-react/components/login-form.tsx","registry/default/clients/react/lib/supabase/client.ts"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"social-auth-react-router": {
name: "social-auth-react-router",
type: "registry:block",
registryDependencies: ["button","card"],
component: React.lazy(() => import("@/registry/default/blocks/social-auth-react-router/app/routes/auth.error.tsx")),
source: "",
files: ["registry/default/blocks/social-auth-react-router/app/routes/auth.error.tsx","registry/default/blocks/social-auth-react-router/app/routes/auth.oauth.tsx","registry/default/blocks/social-auth-react-router/app/routes/login.tsx","registry/default/blocks/social-auth-react-router/app/routes/logout.tsx","registry/default/blocks/social-auth-react-router/app/routes/protected.tsx","registry/default/blocks/social-auth-react-router/app/routes.ts","registry/default/clients/react-router/lib/supabase/client.ts","registry/default/clients/react-router/lib/supabase/server.ts"],
category: "undefined",
subcategory: "undefined",
chunks: []
}
,
"social-auth-tanstack": {
name: "social-auth-tanstack",
type: "registry:block",
registryDependencies: ["button","card"],
component: React.lazy(() => import("@/registry/default/blocks/social-auth-tanstack/components/login-form.tsx")),
source: "",
files: ["registry/default/blocks/social-auth-tanstack/components/login-form.tsx","registry/default/blocks/social-auth-tanstack/lib/supabase/fetch-user-server-fn.ts","registry/default/blocks/social-auth-tanstack/routes/_protected.tsx","registry/default/blocks/social-auth-tanstack/routes/_protected/protected.tsx","registry/default/blocks/social-auth-tanstack/routes/auth/error.tsx","registry/default/blocks/social-auth-tanstack/routes/auth/oauth.ts","registry/default/blocks/social-auth-tanstack/routes/login.tsx","registry/default/clients/tanstack/lib/supabase/client.ts","registry/default/clients/tanstack/lib/supabase/server.ts"],
category: "undefined",
subcategory: "undefined",
chunks: []
+18
View File
@@ -69,6 +69,24 @@ export default function Home() {
</div>
<HorizontalGridLine />
{/* Social Authentication */}
<div className="col-start-2 col-span-10 md:col-start-3 md:col-span-8 pt-16 pb-6 text-xs uppercase font-mono text-foreground-light tracking-wider relative flex justify-between items-center">
<span>Social Authentication</span>
<Link
className="text-foreground underline decoration-1 decoration-foreground-muted underline-offset-4 transition-colors hover:decoration-brand hover:decoration-2"
href="/docs/nextjs/social-auth"
>
Go to block
</Link>
</div>
<HorizontalGridLine />
<div className="col-start-2 col-span-10 md:col-start-3 md:col-span-8 relative">
<div className="-mt-4">
<BlockPreview name="social-auth/auth/login" />
</div>
</div>
<HorizontalGridLine />
{/* Realtime Cursors */}
<div className="col-start-2 col-span-10 md:col-start-3 md:col-span-8 pt-16 pb-6 text-xs uppercase font-mono text-foreground-light tracking-wider relative flex justify-between items-center">
<span>Realtime Cursors</span>
@@ -0,0 +1,36 @@
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
const SocialAuthDemo = () => {
const isLoading = false
const error = null
return (
<div className="flex flex-col gap-6">
<Card>
<CardHeader>
<CardTitle className="text-2xl">Welcome!</CardTitle>
<CardDescription>Sign in to your account to continue</CardDescription>
</CardHeader>
<CardContent>
<form>
<div className="flex flex-col gap-6">
{error && <p className="text-sm text-destructive-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Continue with Github'}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
export default SocialAuthDemo
@@ -0,0 +1,45 @@
import { ThemeProvider } from '@/app/Providers'
import { Metadata } from 'next'
import { BaseInjector } from './../base-injector'
export const metadata: Metadata = {
title: 'Social Auth Example',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html style={{ height: '100%', overflow: 'hidden' }}>
<head>
<style
dangerouslySetInnerHTML={{
__html: `
html, body, #root, main {
height: 100% !important;
min-height: 100% !important;
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important;
}
`,
}}
/>
</head>
<body style={{ height: '100%', margin: 0, padding: 0, overflow: 'hidden' }}>
<BaseInjector />
<ThemeProvider
themes={['dark', 'light', 'classic-dark']}
defaultTheme="system"
enableSystem
>
<div
className="flex w-full h-full items-center justify-center p-6 md:p-10 preview bg-surface-100"
style={{ minHeight: '100%' }}
>
<div className="z-0 pointer-events-none absolute h-full w-full bg-[radial-gradient(hsla(var(--foreground-default)/0.05)_1px,transparent_1px)] [background-size:16px_16px] [mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
<div className="w-full max-w-sm">{children}</div>
</div>
</ThemeProvider>
</body>
</html>
)
}
+2 -2
View File
@@ -39,10 +39,10 @@ export function Command({ name, highlight }: CommandCopyProps) {
return (
<Tabs_Shadcn_ value={value} onValueChange={setValue} className="w-full">
<div className="w-full group relative rounded-lg bg-surface-100 px-4 py-2 overflow-hidden">
<div className="w-full group relative rounded-lg bg-surface-200 dark:bg-surface-100 px-4 py-2 overflow-hidden">
{highlight && (
<motion.div
className="absolute inset-0 bg-gradient-to-l from-transparent via-white to-transparent opacity-10 z-0"
className="absolute inset-0 bg-gradient-to-l from-transparent via-[#bbb] dark:via-white to-transparent opacity-10 z-0"
initial={{ x: '100%' }}
animate={{ x: '-100%' }}
transition={{
@@ -7,7 +7,7 @@ import React from 'react'
import { useFramework } from '@/context/framework-context'
import { useMobileMenu } from '@/hooks/use-mobile-menu'
import { SidebarNavItem } from '@/types/nav'
import { cn } from 'ui'
import { Badge, cn } from 'ui'
// We extend:
// 1. LinkProps - for Next.js Link component props (prefetch, etc)
@@ -89,7 +89,7 @@ const NavigationItem: React.FC<NavigationItemProps> = ({ item, onClick, ...props
className={cn(
'relative',
'flex',
'items-center',
'items-center justify-between',
'h-6',
'text-sm',
'text-foreground-lighter px-6',
@@ -106,8 +106,13 @@ const NavigationItem: React.FC<NavigationItemProps> = ({ item, onClick, ...props
'absolute left-0 w-1 h-full bg-foreground',
isActive ? 'opacity-100' : 'opacity-0'
)}
></div>
/>
{item.title}
{item.new && (
<Badge variant="brand" className="capitalize">
NEW
</Badge>
)}
</Link>
)
}
+8
View File
@@ -54,6 +54,14 @@ export const componentPages: SidebarNavGroup = {
items: [],
commandItemLabel: 'Password-Based Auth',
},
{
title: 'Social Auth',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
href: '/docs/nextjs/social-auth',
items: [],
new: true,
commandItemLabel: 'Social Auth',
},
{
title: 'Dropzone',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
@@ -14,7 +14,7 @@ description: Password-based authentication block for Next.js
## Folder structure
This block includes the [Supabase client](/ui/docs/nextjs/client). When installing, you can skip overwriting it.
This block includes the [Supabase client](/ui/docs/nextjs/client). If you already have one installed, you can skip overwriting it.
<RegistryBlock itemName="password-based-auth-nextjs" />
@@ -72,7 +72,7 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=
1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
1. Set up the Next.js route that users will visit to reset or update their password. Go to the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings and add the `forgot-password` route to the list of Redirect URLs. It should look something like: `http://example.com/auth/forgot-password`.
1. Update the redirect paths in the `login-form.tsx` and `update-password-form.tsx` components to point to the logged-in routes in your app.
1. Update the redirect paths in `login-form.tsx` and `update-password-form.tsx` components to point to the logged-in routes in your app. Our examples use `/protected`, but you can set this to whatever fits your app.
<Callout type="info">
@@ -0,0 +1,67 @@
---
title: Social Authentication
description: Social authentication block for Next.js
---
<BlockPreview name="social-auth/auth/login" />
<Callout className="mt-4">
The block is using Github provider by default, but can be easily switched by changing a single
parameter.
</Callout>
## Installation
<BlockItem name="social-auth-nextjs" description="All needed components for the social auth flow" />
## Folder structure
This block includes the [Supabase client](/ui/docs/nextjs/client). If you already have one installed, you can skip overwriting it.
<RegistryBlock itemName="social-auth-nextjs" />
## Usage
Once you install the block in your Next.js project, you'll get all the necessary pages and components to set up a social authentication flow.
### Getting started
First, add a `.env` file to your project with the following environment variables:
```env
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
```
- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
### Setting up third party providers
We support a wide variety of social providers that you can use to integrate with your application. The full list is available [here](https://supabase.com/docs/guides/auth/social-login).
This block uses the PKCE flow with GitHub as the provider. To switch providers, just update the `provider` field in the `supabase.auth.signInWithOAuth` call. Enable the provider you want to use under [Auth Providers](https://supabase.com/dashboard/project/_/auth/providers) in the Supabase Dashboard and add the necessary credentials.
### Setting up routes and redirect URLs
1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
1. Update the redirect paths in `login-form.tsx` to point to your apps logged-in routes. Our examples use `/protected`, but you can set this to whatever fits your app.
1. Visit `http://your-site-url/auth/login` to see this component in action.
<Callout type="info">
You can use this block with the Pages router by simply moving the routes from the `app` folder into the `pages` folder and renaming them. Example instead of `app/login/page.tsx`, you'd create a `pages/login.tsx` file.
</Callout>
### Combining social auth with password-based auth
If you want to combine this block with the password-based auth, you need to:
- Copy the `handleSocialLogin` function into the password-based `login-form.tsx` component and bind it to a "Login with ..." button.
- Copy the `@/app/auth/oauth/route.ts` in your app under the same route.
## Further reading
- [Social login](https://supabase.com/docs/guides/auth/social-login)
- [Authentication error codes](https://supabase.com/docs/guides/auth/debugging/error-codes)
@@ -14,7 +14,7 @@ description: Password-based authentication block for React Router
## Folder structure
This block includes the [Supabase client](/ui/docs/react-router/client). When installing, you can skip overwriting it.
This block includes the [Supabase client](/ui/docs/react-router/client). If you already have one installed, you can skip overwriting it.
<RegistryBlock itemName="password-based-auth-react-router" />
@@ -71,8 +71,7 @@ VITE_SUPABASE_ANON_KEY=
1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
1. Set up the route users will visit to reset or update their password. Go to the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings and add the `forgot-password` route to the list of Redirect URLs. It should look something like: `http://example.com/auth/forgot-password`.
1. Update the redirect paths in the `login.tsx` and `update-password.tsx` components to point to the logged-in routes in your app.
1. Update the redirect paths in `login.tsx` and `update-password.tsx` components to point to the logged-in routes in your app. Our examples use `/protected`, but you can set this to whatever fits your app.
## Further reading
@@ -0,0 +1,65 @@
---
title: Social Authentication
description: Social authentication block for React Router
---
<BlockPreview name="social-auth/auth/login" />
<Callout className="mt-4">
The block is using Github provider by default, but can be easily switched by changing a single
parameter.
</Callout>
## Installation
<BlockItem
name="social-auth-react-router"
description="All needed components for the social auth flow"
/>
## Folder structure
This block includes the [Supabase client](/ui/docs/react-router/client). If you already have one installed, you can skip overwriting it.
<RegistryBlock itemName="social-auth-react-router" />
## Usage
Once you install the block in your React Router project, you'll get all the necessary pages and components to set up a social authentication flow.
### Getting started
First, add a `.env` file to your project with the following environment variables:
```env
VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=
```
- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
### Setting up third party providers
We support a wide variety of social providers that you can use to integrate with your application. The full list is available [here](https://supabase.com/docs/guides/auth/social-login).
This block uses the PKCE flow with GitHub as the provider. To switch providers, just update the `provider` field in the `supabase.auth.signInWithOAuth` call. Enable the provider you want to use under [Auth Providers](https://supabase.com/dashboard/project/_/auth/providers) in the Supabase Dashboard and add the necessary credentials.
### Setting up routes and redirect URLs
1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
1. Update the redirect paths in `login-form.tsx` to point to your apps logged-in routes. Our examples use `/protected`, but you can set this to whatever fits your app.
1. Visit `http://your-site-url/login` to see this component in action.
### Combining social auth with password-based auth
If you want to combine this block with the password-based auth, you need to:
- Copy the `@/app/routes/auth.oauth.tsx` in your app under the same route.
- Copy just the action from the social login page into a separate route.
- In the password-based `login.tsx` page, create another form with a "Login with ..." button. The method should be `post` and the `action` should point at the action route from the previous step.
## Further reading
- [Social login](https://supabase.com/docs/guides/auth/social-login)
- [Authentication error codes](https://supabase.com/docs/guides/auth/debugging/error-codes)
@@ -14,6 +14,8 @@ description: Password-based authentication block for React Single Page Applicati
## Folder structure
This block includes the [Supabase client](/ui/docs/react/client). If you already have one installed, you can skip overwriting it.
<RegistryBlock itemName="password-based-auth-react" />
## Usage
@@ -37,7 +39,7 @@ VITE_SUPABASE_ANON_KEY=
1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
1. Set up the route users will visit to reset or update their password. Go to the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings and add the `forgot-password` route to the list of Redirect URLs. It should look something like: `http://example.com/auth/forgot-password`.
1. Update the redirect paths in the `login.tsx` and `update-password.tsx` components to point to the logged-in routes in your app.
1. Update the redirect paths in `login.tsx` and `update-password.tsx` components to point to the logged-in routes in your app. Our examples use `/protected`, but you can set this to whatever fits your app.
1. Add the following code in the authenticated route to redirect to login if the user is unauthenticated.
@@ -0,0 +1,57 @@
---
title: Social Authentication
description: Social authentication block for React Single Page Applications
---
<BlockPreview name="social-auth/auth/login" />
<Callout className="mt-4">
The block is using Github provider by default, but can be easily switched by changing a single
parameter.
</Callout>
## Installation
<BlockItem name="social-auth-react" description="All needed components for the social auth flow" />
## Folder structure
This block includes the [Supabase client](/ui/docs/react/client). If you already have one installed, you can skip overwriting it.
<RegistryBlock itemName="social-auth-react" />
## Usage
Once you install the block in your React project, you'll get all the necessary pages and components to set up a social authentication flow.
### Getting started
First, add a `.env` file to your project with the following environment variables:
```env
VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=
```
- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
### Setting up third party providers
We support a wide variety of social providers that you can use to integrate with your application. The full list is available [here](https://supabase.com/docs/guides/auth/social-login).
This block uses the implicit flow with GitHub as the provider. To switch providers, just update the `provider` field in the `supabase.auth.signInWithOAuth` call. Enable the provider you want to use under [Auth Providers](https://supabase.com/dashboard/project/_/auth/providers) in the Supabase Dashboard and add the necessary credentials.
### Setting up routes and redirect URLs
1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
1. Update the redirect paths in `login-form.tsx` to point to your apps logged-in routes. Our examples use `/protected`, but you can set this to whatever fits your app.
### Combining social auth with password-based auth
If you want to combine this block with the password-based auth, you need to copy the `handleSocialLogin` function into the password-based `login-form.tsx` component and bind it to a button.
## Further reading
- [Social login](https://supabase.com/docs/guides/auth/social-login)
- [Authentication error codes](https://supabase.com/docs/guides/auth/debugging/error-codes)
@@ -11,6 +11,8 @@ description: Password-based authentication block for TanStack Start
## Folder structure
This block includes the [Supabase client](/ui/docs/tanstack/client). If you already have one installed, you can skip overwriting it.
<RegistryBlock itemName="password-based-auth-tanstack" />
## Usage
@@ -66,8 +68,7 @@ VITE_SUPABASE_ANON_KEY=
1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
1. Set up the route users will visit to reset or update their password. Go to the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings and add the `forgot-password` route to the list of Redirect URLs. It should look something like: `http://example.com/auth/forgot-password`.
1. Update the redirect paths in the `login.tsx` and `update-password.tsx` components to point to the logged-in routes in your app.
1. Update the redirect paths in `login.tsx` and `update-password.tsx` components to point to the logged-in routes in your app. 1. Our examples use `/protected`, but you can set this to whatever fits your app.
## Further reading
@@ -0,0 +1,61 @@
---
title: Social Authentication
description: Social authentication block for Tanstack Start
---
<BlockPreview name="social-auth/auth/login" />
<Callout className="mt-4">
The block is using Github provider by default, but can be easily switched by changing a single
parameter.
</Callout>
## Installation
<BlockItem name="social-auth-nextjs" description="All needed components for the social auth flow" />
## Folder structure
This block includes the [Supabase client](/ui/docs/tanstack/client). If you already have one installed, you can skip overwriting it.
<RegistryBlock itemName="social-auth-tanstack" />
## Usage
Once you install the block in your Tanstack Start project, you'll get all the necessary pages and components to set up a social authentication flow.
### Getting started
First, add a `.env` file to your project with the following environment variables:
```env
VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=
```
- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
### Setting up third party providers
We support a wide variety of social providers that you can use to integrate with your application. The full list is available [here](https://supabase.com/docs/guides/auth/social-login).
This block uses the PKCE flow with GitHub as the provider. To switch providers, just update the `provider` field in the `supabase.auth.signInWithOAuth` call. Enable the provider you want to use under [Auth Providers](https://supabase.com/dashboard/project/_/auth/providers) in the Supabase Dashboard and add the necessary credentials.
### Setting up routes and redirect URLs
1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
1. Update the redirect paths in `login-form.tsx` to point to your apps logged-in routes. Our examples use `/protected`, but you can set this to whatever fits your app.
1. Visit `http://your-site-url/login` to see this component in action.
### Combining social auth with password-based auth
If you want to combine this block with the password-based auth, you need to:
- Copy the `handleSocialLogin` function into the password-based `login-form.tsx` component and bind it to a "Login with ..." button.
- Copy the `@/routes/auth/oauth.ts` in your app under the same route.
## Further reading
- [Social login](https://supabase.com/docs/guides/auth/social-login)
- [Authentication error codes](https://supabase.com/docs/guides/auth/debugging/error-codes)
File diff suppressed because one or more lines are too long
@@ -29,7 +29,7 @@
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/app/protected/page.tsx",
"content": "import { redirect } from 'next/navigation'\n\nimport { LogoutButton } from '@/registry/default/blocks/password-based-auth-nextjs/components/logout-button'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'\n\nexport default async function ProtectedPage() {\n const supabase = await createClient()\n\n const { data, error } = await supabase.auth.getUser()\n if (error || !data?.user) {\n redirect('/login')\n }\n\n return (\n <div className=\"flex h-svh w-full items-center justify-center gap-2\">\n <p>\n Hello <span>{data.user.email}</span>\n </p>\n <LogoutButton />\n </div>\n )\n}\n",
"content": "import { redirect } from 'next/navigation'\n\nimport { LogoutButton } from '@/registry/default/blocks/password-based-auth-nextjs/components/logout-button'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'\n\nexport default async function ProtectedPage() {\n const supabase = await createClient()\n\n const { data, error } = await supabase.auth.getUser()\n if (error || !data?.user) {\n redirect('/auth/login')\n }\n\n return (\n <div className=\"flex h-svh w-full items-center justify-center gap-2\">\n <p>\n Hello <span>{data.user.email}</span>\n </p>\n <LogoutButton />\n </div>\n )\n}\n",
"type": "registry:page",
"target": "app/protected/page.tsx"
},
@@ -16,7 +16,7 @@
"files": [
{
"path": "registry/default/blocks/password-based-auth-react/components/login-form.tsx",
"content": "import { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/react/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 { 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 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 location.href = '/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 <a\n href=\"/forgot-password\"\n className=\"ml-auto inline-block text-sm underline-offset-4 hover:underline\"\n >\n Forgot your password?\n </a>\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 <a href=\"/sign-up\" className=\"underline underline-offset-4\">\n Sign up\n </a>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"content": "import { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/react/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 { 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 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 location.href = '/protected'\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n <div className={cn('flex flex-col gap-6', className)} {...props}>\n <Card>\n <CardHeader>\n <CardTitle className=\"text-2xl\">Login</CardTitle>\n <CardDescription>Enter your email below to login to your account</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleLogin}>\n <div className=\"flex flex-col gap-6\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"m@example.com\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n />\n </div>\n <div className=\"grid gap-2\">\n <div className=\"flex items-center\">\n <Label htmlFor=\"password\">Password</Label>\n <a\n href=\"/forgot-password\"\n className=\"ml-auto inline-block text-sm underline-offset-4 hover:underline\"\n >\n Forgot your password?\n </a>\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 <a href=\"/sign-up\" className=\"underline underline-offset-4\">\n Sign up\n </a>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"type": "registry:component"
},
{
@@ -31,7 +31,7 @@
},
{
"path": "registry/default/blocks/password-based-auth-react/components/update-password-form.tsx",
"content": "import { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/react/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 { 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\n const handleForgotPassword = async (e: React.FormEvent) => {\n const supabase = createClient()\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 location.href = '/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 </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"content": "import { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/react/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 { 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\n const handleForgotPassword = async (e: React.FormEvent) => {\n const supabase = createClient()\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 location.href = '/protected'\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n <div className={cn('flex flex-col gap-6', className)} {...props}>\n <Card>\n <CardHeader>\n <CardTitle className=\"text-2xl\">Reset Your Password</CardTitle>\n <CardDescription>Please enter your new password below.</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleForgotPassword}>\n <div className=\"flex flex-col gap-6\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"password\">New password</Label>\n <Input\n id=\"password\"\n type=\"password\"\n placeholder=\"New password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n />\n </div>\n {error && <p className=\"text-sm text-red-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Saving...' : 'Save new password'}\n </Button>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"type": "registry:component"
},
{
@@ -34,10 +34,10 @@
"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",
"path": "registry/default/blocks/password-based-auth-tanstack/routes/_protected/protected.tsx",
"content": "import { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/_protected/protected')({\n component: Info,\n loader: async ({ context }) => {\n return {\n user: context.user!,\n }\n },\n})\n\nfunction Info() {\n const data = Route.useLoaderData()\n\n return <p>Hello {data.user.email}</p>\n}\n",
"type": "registry:file",
"target": "routes/_protected/info.tsx"
"target": "routes/_protected/protected.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/auth/confirm.ts",
@@ -47,7 +47,7 @@
},
{
"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\n const handleLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n if (error) throw error\n // Update this route to redirect to an authenticated route. The user already has an active session.\n await navigate({ to: '/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",
"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\n const handleLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n if (error) throw error\n // Update this route to redirect to an authenticated route. The user already has an active session.\n await navigate({ to: '/protected' })\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n <div className={cn('flex flex-col gap-6', className)} {...props}>\n <Card>\n <CardHeader>\n <CardTitle className=\"text-2xl\">Login</CardTitle>\n <CardDescription>Enter your email below to login to your account</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleLogin}>\n <div className=\"flex flex-col gap-6\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"m@example.com\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n />\n </div>\n <div className=\"grid gap-2\">\n <div className=\"flex items-center\">\n <Label htmlFor=\"password\">Password</Label>\n <Link\n to=\"/forgot-password\"\n className=\"ml-auto inline-block text-sm underline-offset-4 hover:underline\"\n >\n Forgot your password?\n </Link>\n </div>\n <Input\n id=\"password\"\n type=\"password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n />\n </div>\n {error && <p className=\"text-sm text-red-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Logging in...' : 'Login'}\n </Button>\n </div>\n <div className=\"mt-4 text-center text-sm\">\n Don&apos;t have an account?{' '}\n <Link to=\"/sign-up\" className=\"underline underline-offset-4\">\n Sign up\n </Link>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"type": "registry:component"
},
{
@@ -86,7 +86,7 @@
},
{
"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\n const handleForgotPassword = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.updateUser({ password })\n if (error) throw error\n // Update this route to redirect to an authenticated route. The user already has an active session.\n await navigate({ to: '/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 </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"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\n const handleForgotPassword = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.updateUser({ password })\n if (error) throw error\n // Update this route to redirect to an authenticated route. The user already has an active session.\n await navigate({ to: '/protected' })\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n <div className={cn('flex flex-col gap-6', className)} {...props}>\n <Card>\n <CardHeader>\n <CardTitle className=\"text-2xl\">Reset Your Password</CardTitle>\n <CardDescription>Please enter your new password below.</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleForgotPassword}>\n <div className=\"flex flex-col gap-6\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"password\">New password</Label>\n <Input\n id=\"password\"\n type=\"password\"\n placeholder=\"New password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n />\n </div>\n {error && <p className=\"text-sm text-red-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Saving...' : 'Save new password'}\n </Button>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"type": "registry:component"
},
{
@@ -0,0 +1,72 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "social-auth-nextjs",
"type": "registry:block",
"title": "Social Auth flow for Nextjs and Supabase",
"description": "Social Auth flow for Nextjs and Supabase",
"dependencies": [
"@supabase/ssr@latest",
"@supabase/supabase-js@latest"
],
"registryDependencies": [
"button",
"card"
],
"files": [
{
"path": "registry/default/blocks/social-auth-nextjs/app/auth/login/page.tsx",
"content": "import { LoginForm } from '@/registry/default/blocks/social-auth-nextjs/components/login-form'\n\nexport default function Page() {\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:page",
"target": "app/auth/login/page.tsx"
},
{
"path": "registry/default/blocks/social-auth-nextjs/app/auth/error/page.tsx",
"content": "import { Card, CardContent, CardHeader, CardTitle } from '@/registry/default/components/ui/card'\n\nexport default async function Page({ searchParams }: { searchParams: Promise<{ error: string }> }) {\n const params = await searchParams\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:page",
"target": "app/auth/error/page.tsx"
},
{
"path": "registry/default/blocks/social-auth-nextjs/app/protected/page.tsx",
"content": "import { redirect } from 'next/navigation'\n\nimport { LogoutButton } from '@/registry/default/blocks/social-auth-nextjs/components/logout-button'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'\n\nexport default async function ProtectedPage() {\n const supabase = await createClient()\n\n const { data, error } = await supabase.auth.getUser()\n if (error || !data?.user) {\n redirect('/auth/login')\n }\n\n return (\n <div className=\"flex h-svh w-full items-center justify-center gap-2\">\n <p>\n Hello <span>{data.user.email}</span>\n </p>\n <LogoutButton />\n </div>\n )\n}\n",
"type": "registry:page",
"target": "app/protected/page.tsx"
},
{
"path": "registry/default/blocks/social-auth-nextjs/app/auth/oauth/route.ts",
"content": "import { NextResponse } from 'next/server'\n// The client you created from the Server-Side Auth instructions\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'\n\nexport async function GET(request: Request) {\n const { searchParams, origin } = new URL(request.url)\n const code = searchParams.get('code')\n // if \"next\" is in param, use it as the redirect URL\n const next = searchParams.get('next') ?? '/'\n\n if (code) {\n const supabase = await createClient()\n const { error } = await supabase.auth.exchangeCodeForSession(code)\n if (!error) {\n const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer\n const isLocalEnv = process.env.NODE_ENV === 'development'\n if (isLocalEnv) {\n // we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host\n return NextResponse.redirect(`${origin}${next}`)\n } else if (forwardedHost) {\n return NextResponse.redirect(`https://${forwardedHost}${next}`)\n } else {\n return NextResponse.redirect(`${origin}${next}`)\n }\n }\n }\n\n // return the user to an error page with instructions\n return NextResponse.redirect(`${origin}/auth/error`)\n}\n",
"type": "registry:page",
"target": "app/auth/oauth/route.ts"
},
{
"path": "registry/default/blocks/social-auth-nextjs/components/login-form.tsx",
"content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/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 { useState } from 'react'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [error, setError] = useState<string | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n\n const handleSocialLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n options: {\n redirectTo: `${window.location.origin}/auth/oauth?next=/protected`,\n },\n })\n\n if (error) throw error\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\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\">Welcome!</CardTitle>\n <CardDescription>Sign in to your account to continue</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSocialLogin}>\n <div className=\"flex flex-col gap-6\">\n {error && <p className=\"text-sm text-destructive-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Logging in...' : 'Continue with Github'}\n </Button>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/social-auth-nextjs/middleware.ts",
"content": "import { updateSession } from '@/registry/default/clients/nextjs/lib/supabase/middleware'\nimport { type NextRequest } from 'next/server'\n\nexport async function middleware(request: NextRequest) {\n return await updateSession(request)\n}\n\nexport const config = {\n matcher: [\n /*\n * Match all request paths except for the ones starting with:\n * - _next/static (static files)\n * - _next/image (image optimization files)\n * - favicon.ico (favicon file)\n * Feel free to modify this pattern to include more paths.\n */\n '/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',\n ],\n}\n",
"type": "registry:file",
"target": "middleware.ts"
},
{
"path": "registry/default/blocks/social-auth-nextjs/components/logout-button.tsx",
"content": "'use client'\n\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport { useRouter } from 'next/navigation'\n\nexport function LogoutButton() {\n const router = useRouter()\n\n const logout = async () => {\n const supabase = createClient()\n await supabase.auth.signOut()\n router.push('/auth/login')\n }\n\n return <Button onClick={logout}>Logout</Button>\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/client.ts",
"content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!\n )\n}\n",
"type": "registry:lib"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/middleware.ts",
"content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getUser(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: DO NOT REMOVE auth.getUser()\n\n const {\n data: { user },\n } = await supabase.auth.getUser()\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n",
"type": "registry:lib"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/server.ts",
"content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n",
"type": "registry:lib"
}
]
}
@@ -0,0 +1,65 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "social-auth-react-router",
"type": "registry:block",
"title": "Social Auth flow for React Router and Supabase",
"description": "Social Auth flow for React Router and Supabase",
"dependencies": [
"@supabase/ssr@latest",
"@react-router/dev@latest",
"@react-router/fs-routes@latest",
"@supabase/supabase-js@latest"
],
"registryDependencies": [
"button",
"card"
],
"files": [
{
"path": "registry/default/blocks/social-auth-react-router/app/routes/auth.error.tsx",
"content": "import { Card, CardContent, CardHeader, CardTitle } from '@/registry/default/components/ui/card'\nimport { useSearchParams } from 'react-router'\n\nexport default function Page() {\n let [searchParams] = useSearchParams()\n\n return (\n <div className=\"flex min-h-svh w-full items-center justify-center p-6 md:p-10\">\n <div className=\"w-full max-w-sm\">\n <div className=\"flex flex-col gap-6\">\n <Card>\n <CardHeader>\n <CardTitle className=\"text-2xl\">Sorry, something went wrong.</CardTitle>\n </CardHeader>\n <CardContent>\n {searchParams?.get('error') ? (\n <p className=\"text-sm text-muted-foreground\">\n Code error: {searchParams?.get('error')}\n </p>\n ) : (\n <p className=\"text-sm text-muted-foreground\">An unspecified error occurred.</p>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n )\n}\n",
"type": "registry:file",
"target": "app/routes/auth.error.tsx"
},
{
"path": "registry/default/blocks/social-auth-react-router/app/routes/auth.oauth.tsx",
"content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { type LoaderFunctionArgs, redirect } from 'react-router'\n\nexport async function loader({ request }: LoaderFunctionArgs) {\n const requestUrl = new URL(request.url)\n const code = requestUrl.searchParams.get('code')\n const next = requestUrl.searchParams.get('next') || '/'\n if (code) {\n const { supabase, headers } = createClient(request)\n\n const { error } = await supabase.auth.exchangeCodeForSession(code)\n if (!error) {\n return redirect(next, { headers })\n } else {\n return redirect(`/auth/error?error=${error?.message}`)\n }\n }\n // redirect the user to an error page with some instructions\n return redirect(`/auth/error`)\n}\n",
"type": "registry:file",
"target": "app/routes/auth.oauth.tsx"
},
{
"path": "registry/default/blocks/social-auth-react-router/app/routes/login.tsx",
"content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { type ActionFunctionArgs, redirect, useFetcher } from 'react-router'\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n const { supabase } = createClient(request)\n const origin = new URL(request.url).origin\n\n const { data, error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n options: {\n redirectTo: `${origin}/auth/oauth?next=/protected`,\n },\n })\n\n if (data.url) {\n return redirect(data.url)\n }\n\n if (error) {\n return {\n error: error instanceof Error ? error.message : 'An error occurred',\n }\n }\n}\n\nexport default function Login() {\n const fetcher = useFetcher<typeof action>()\n\n const error = fetcher.data?.error\n const loading = fetcher.state === 'submitting'\n\n return (\n <div className=\"flex min-h-svh w-full items-center justify-center p-6 md:p-10\">\n <div className=\"w-full max-w-sm\">\n <div className=\"flex flex-col gap-6\">\n <Card>\n <CardHeader>\n <CardTitle className=\"text-2xl\">Welcome!</CardTitle>\n <CardDescription>Sign in to your account to continue</CardDescription>\n </CardHeader>\n <CardContent>\n <fetcher.Form method=\"post\">\n <div className=\"flex flex-col gap-6\">\n {error && <p className=\"text-sm text-destructive-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={loading}>\n {loading ? 'Logging in...' : 'Continue with Github'}\n </Button>\n </div>\n </fetcher.Form>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n )\n}\n",
"type": "registry:file",
"target": "app/routes/login.tsx"
},
{
"path": "registry/default/blocks/social-auth-react-router/app/routes/logout.tsx",
"content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { type ActionFunctionArgs, redirect } from 'react-router'\n\nexport async function loader({ request }: ActionFunctionArgs) {\n const { supabase, headers } = createClient(request)\n\n const { error } = await supabase.auth.signOut()\n\n if (error) {\n console.error(error)\n return { success: false, error: error.message }\n }\n\n // Redirect to dashboard or home page after successful sign-in\n return redirect('/', { headers })\n}\n",
"type": "registry:file",
"target": "app/routes/logout.tsx"
},
{
"path": "registry/default/blocks/social-auth-react-router/app/routes/protected.tsx",
"content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport { type LoaderFunctionArgs, redirect, useLoaderData } from 'react-router'\n\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n const { supabase } = createClient(request)\n\n const { data, error } = await supabase.auth.getUser()\n if (error || !data?.user) {\n return redirect('/login')\n }\n\n return data\n}\n\nexport default function ProtectedPage() {\n let data = useLoaderData<typeof loader>()\n\n return (\n <div className=\"flex items-center justify-center h-screen gap-2\">\n <p>\n Hello <span className=\"text-primary font-semibold\">{data.user.email}</span>\n </p>\n <a href=\"/logout\">\n <Button>Logout</Button>\n </a>\n </div>\n )\n}\n",
"type": "registry:file",
"target": "app/routes/protected.tsx"
},
{
"path": "registry/default/blocks/social-auth-react-router/app/routes.ts",
"content": "import { type RouteConfig } from '@react-router/dev/routes'\nimport { flatRoutes } from '@react-router/fs-routes'\n\nexport default flatRoutes() satisfies RouteConfig\n",
"type": "registry:file",
"target": "app/routes.ts"
},
{
"path": "registry/default/clients/react-router/lib/supabase/client.ts",
"content": "/// <reference types=\"vite/types/importMeta.d.ts\" />\nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_ANON_KEY!\n )\n}\n",
"type": "registry:lib"
},
{
"path": "registry/default/clients/react-router/lib/supabase/server.ts",
"content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n",
"type": "registry:lib"
}
]
}
@@ -0,0 +1,26 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "social-auth-react",
"type": "registry:block",
"title": "Social Auth flow for React and Supabase",
"description": "Social Auth flow for React and Supabase",
"dependencies": [
"@supabase/supabase-js@latest"
],
"registryDependencies": [
"button",
"card"
],
"files": [
{
"path": "registry/default/blocks/social-auth-react/components/login-form.tsx",
"content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/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 { useState } from 'react'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [error, setError] = useState<string | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n\n const handleSocialLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n })\n\n if (error) throw error\n location.href = '/protected'\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\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\">Welcome!</CardTitle>\n <CardDescription>Sign in to your account to continue</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSocialLogin}>\n <div className=\"flex flex-col gap-6\">\n {error && <p className=\"text-sm text-destructive-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Logging in...' : 'Continue with Github'}\n </Button>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/clients/react/lib/supabase/client.ts",
"content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_ANON_KEY!\n )\n}\n",
"type": "registry:lib"
}
]
}
@@ -0,0 +1,67 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "social-auth-tanstack",
"type": "registry:block",
"title": "Social Auth flow for TanStack and Supabase",
"description": "Social Auth flow for TanStack and Supabase",
"dependencies": [
"@supabase/ssr@latest",
"@supabase/supabase-js@latest"
],
"registryDependencies": [
"button",
"card"
],
"files": [
{
"path": "registry/default/blocks/social-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 { useState } from 'react'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [error, setError] = useState<string | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n\n const handleSocialLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n options: {\n redirectTo: `${window.location.origin}/auth/oauth?next=/protected`,\n },\n })\n\n if (error) throw error\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\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\">Welcome!</CardTitle>\n <CardDescription>Sign in to your account to continue</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSocialLogin}>\n <div className=\"flex flex-col gap-6\">\n {error && <p className=\"text-sm text-destructive-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Logging in...' : 'Continue with Github'}\n </Button>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/social-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'\ntype SSRSafeUser = User & {\n factors: (Factor & { factor_type: 'phone' | 'totp' })[]\n}\n\nexport const fetchUser: () => Promise<SSRSafeUser | null> = createServerFn({\n method: 'GET',\n}).handler(async () => {\n const supabase = createClient()\n const { data, error } = await supabase.auth.getUser()\n\n if (error) {\n return null\n }\n\n return data.user as SSRSafeUser\n})\n",
"type": "registry:lib"
},
{
"path": "registry/default/blocks/social-auth-tanstack/routes/_protected.tsx",
"content": "import { fetchUser } from '@/registry/default/blocks/social-auth-tanstack/lib/supabase/fetch-user-server-fn'\nimport { createFileRoute, redirect } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/_protected')({\n beforeLoad: async () => {\n const user = await fetchUser()\n\n if (!user) {\n throw redirect({ to: '/login' })\n }\n\n return {\n user,\n }\n },\n})\n",
"type": "registry:file",
"target": "routes/_protected.tsx"
},
{
"path": "registry/default/blocks/social-auth-tanstack/routes/_protected/protected.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/protected.tsx"
},
{
"path": "registry/default/blocks/social-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/social-auth-tanstack/routes/auth/oauth.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`, search: { error: 'No request' } })\n }\n\n const searchParams = ctx.data\n const token_hash = searchParams['token_hash'] as string\n const type = searchParams['type'] as EmailOtpType | null\n const next = (searchParams['next'] ?? '/') as string\n\n if (token_hash && type) {\n const supabase = createClient()\n\n const { error } = await supabase.auth.verifyOtp({\n type,\n token_hash,\n })\n console.log(error?.message)\n if (!error) {\n // redirect user to specified redirect URL or root of app\n throw redirect({ href: next })\n } else {\n // redirect the user to an error page with some instructions\n throw redirect({\n to: `/auth/error`,\n search: { error: error?.message },\n })\n }\n }\n\n // redirect the user to an error page with some instructions\n throw redirect({\n to: `/auth/error`,\n search: { error: 'No token hash or type' },\n })\n })\n\nexport const Route = createFileRoute('/auth/confirm')({\n preload: false,\n loader: (opts) => confirmFn({ data: opts.location.search }),\n})\n",
"type": "registry:file",
"target": "routes/auth/oauth.ts"
},
{
"path": "registry/default/blocks/social-auth-tanstack/routes/login.tsx",
"content": "import { LoginForm } from '@/registry/default/blocks/social-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/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"
}
]
}
+179 -2
View File
@@ -212,9 +212,9 @@
"target": "routes/_protected.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/_protected/info.tsx",
"path": "registry/default/blocks/password-based-auth-tanstack/routes/_protected/protected.tsx",
"type": "registry:file",
"target": "routes/_protected/info.tsx"
"target": "routes/_protected/protected.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/auth/confirm.ts",
@@ -271,6 +271,183 @@
}
]
},
{
"name": "social-auth-nextjs",
"type": "registry:block",
"title": "Social Auth flow for Nextjs and Supabase",
"description": "Social Auth flow for Nextjs and Supabase",
"registryDependencies": ["button", "card"],
"dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"],
"files": [
{
"path": "registry/default/blocks/social-auth-nextjs/app/auth/login/page.tsx",
"type": "registry:page",
"target": "app/auth/login/page.tsx"
},
{
"path": "registry/default/blocks/social-auth-nextjs/app/auth/error/page.tsx",
"type": "registry:page",
"target": "app/auth/error/page.tsx"
},
{
"path": "registry/default/blocks/social-auth-nextjs/app/protected/page.tsx",
"type": "registry:page",
"target": "app/protected/page.tsx"
},
{
"path": "registry/default/blocks/social-auth-nextjs/app/auth/oauth/route.ts",
"type": "registry:page",
"target": "app/auth/oauth/route.ts"
},
{
"path": "registry/default/blocks/social-auth-nextjs/components/login-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/social-auth-nextjs/middleware.ts",
"type": "registry:file",
"target": "middleware.ts"
},
{
"path": "registry/default/blocks/social-auth-nextjs/components/logout-button.tsx",
"type": "registry:component"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/client.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/middleware.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/server.ts",
"type": "registry:lib"
}
]
},
{
"name": "social-auth-react",
"type": "registry:block",
"title": "Social Auth flow for React and Supabase",
"description": "Social Auth flow for React and Supabase",
"registryDependencies": ["button", "card"],
"files": [
{
"path": "registry/default/blocks/social-auth-react/components/login-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/clients/react/lib/supabase/client.ts",
"type": "registry:lib"
}
],
"dependencies": ["@supabase/supabase-js@latest"]
},
{
"name": "social-auth-react-router",
"type": "registry:block",
"title": "Social Auth flow for React Router and Supabase",
"description": "Social Auth flow for React Router and Supabase",
"registryDependencies": ["button", "card"],
"dependencies": [
"@supabase/ssr@latest",
"@react-router/dev@latest",
"@react-router/fs-routes@latest",
"@supabase/supabase-js@latest"
],
"files": [
{
"path": "registry/default/blocks/social-auth-react-router/app/routes/auth.error.tsx",
"type": "registry:file",
"target": "app/routes/auth.error.tsx"
},
{
"path": "registry/default/blocks/social-auth-react-router/app/routes/auth.oauth.tsx",
"type": "registry:file",
"target": "app/routes/auth.oauth.tsx"
},
{
"path": "registry/default/blocks/social-auth-react-router/app/routes/login.tsx",
"type": "registry:file",
"target": "app/routes/login.tsx"
},
{
"path": "registry/default/blocks/social-auth-react-router/app/routes/logout.tsx",
"type": "registry:file",
"target": "app/routes/logout.tsx"
},
{
"path": "registry/default/blocks/social-auth-react-router/app/routes/protected.tsx",
"type": "registry:file",
"target": "app/routes/protected.tsx"
},
{
"path": "registry/default/blocks/social-auth-react-router/app/routes.ts",
"type": "registry:file",
"target": "app/routes.ts"
},
{
"path": "registry/default/clients/react-router/lib/supabase/client.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/react-router/lib/supabase/server.ts",
"type": "registry:lib"
}
]
},
{
"name": "social-auth-tanstack",
"type": "registry:block",
"title": "Social Auth flow for TanStack and Supabase",
"description": "Social Auth flow for TanStack and Supabase",
"registryDependencies": ["button", "card"],
"dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"],
"files": [
{
"path": "registry/default/blocks/social-auth-tanstack/components/login-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/social-auth-tanstack/lib/supabase/fetch-user-server-fn.ts",
"type": "registry:lib"
},
{
"path": "registry/default/blocks/social-auth-tanstack/routes/_protected.tsx",
"type": "registry:file",
"target": "routes/_protected.tsx"
},
{
"path": "registry/default/blocks/social-auth-tanstack/routes/_protected/protected.tsx",
"type": "registry:file",
"target": "routes/_protected/protected.tsx"
},
{
"path": "registry/default/blocks/social-auth-tanstack/routes/auth/error.tsx",
"type": "registry:file",
"target": "routes/auth/error.tsx"
},
{
"path": "registry/default/blocks/social-auth-tanstack/routes/auth/oauth.ts",
"type": "registry:file",
"target": "routes/auth/oauth.ts"
},
{
"path": "registry/default/blocks/social-auth-tanstack/routes/login.tsx",
"type": "registry:file",
"target": "routes/login.tsx"
},
{
"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",
+21 -9
View File
@@ -1,15 +1,21 @@
import { type Registry, type RegistryItem } from 'shadcn/registry'
import { clients } from './clients'
import currentUserAvatar from './default/blocks/current-user-avatar/registry-item.json' assert { type: 'json' }
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 passwordBasedAuthReactRouter from './default/blocks/password-based-auth-react-router/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 realtimeAvatarStack from './default/blocks/realtime-avatar-stack/registry-item.json' assert { type: 'json' }
import currentUserAvatar from './default/blocks/current-user-avatar/registry-item.json' with { type: 'json' }
import dropzone from './default/blocks/dropzone/registry-item.json' with { type: 'json' }
import passwordBasedAuthNextjs from './default/blocks/password-based-auth-nextjs/registry-item.json' with { type: 'json' }
import passwordBasedAuthReactRouter from './default/blocks/password-based-auth-react-router/registry-item.json' with { type: 'json' }
import passwordBasedAuthReact from './default/blocks/password-based-auth-react/registry-item.json' with { type: 'json' }
import passwordBasedAuthTanstack from './default/blocks/password-based-auth-tanstack/registry-item.json' with { type: 'json' }
import realtimeChat from './default/blocks/realtime-chat/registry-item.json' assert { type: 'json' }
import realtimeCursor from './default/blocks/realtime-cursor/registry-item.json' assert { type: 'json' }
import socialAuthNextjs from './default/blocks/social-auth-nextjs/registry-item.json' with { type: 'json' }
import socialAuthReactRouter from './default/blocks/social-auth-react-router/registry-item.json' with { type: 'json' }
import socialAuthReact from './default/blocks/social-auth-react/registry-item.json' with { type: 'json' }
import socialAuthTanstack from './default/blocks/social-auth-tanstack/registry-item.json' with { type: 'json' }
import realtimeAvatarStack from './default/blocks/realtime-avatar-stack/registry-item.json' with { type: 'json' }
import realtimeChat from './default/blocks/realtime-chat/registry-item.json' with { type: 'json' }
import realtimeCursor from './default/blocks/realtime-cursor/registry-item.json' with { type: 'json' }
import { registryItemAppend } from './utils'
const combine = (component: Registry['items'][number]) => {
@@ -34,6 +40,12 @@ export const blocks = [
registryItemAppend(passwordBasedAuthReact as RegistryItem, [reactClient!]),
registryItemAppend(passwordBasedAuthReactRouter as RegistryItem, [reactRouterClient!]),
registryItemAppend(passwordBasedAuthTanstack as RegistryItem, [tanstackClient!]),
registryItemAppend(socialAuthNextjs as RegistryItem, [nextjsClient!]),
registryItemAppend(socialAuthReact as RegistryItem, [reactClient!]),
registryItemAppend(socialAuthReactRouter as RegistryItem, [reactRouterClient!]),
registryItemAppend(socialAuthTanstack as RegistryItem, [tanstackClient!]),
...combine(dropzone as RegistryItem),
...combine(realtimeCursor as RegistryItem),
...combine(currentUserAvatar as RegistryItem),
+4 -4
View File
@@ -1,7 +1,7 @@
import { type Registry } from 'shadcn/registry'
import nextjs from './default/clients/nextjs/registry-item.json' assert { type: 'json' }
import reactRouter from './default/clients/react-router/registry-item.json' assert { type: 'json' }
import react from './default/clients/react/registry-item.json' assert { type: 'json' }
import tanstack from './default/clients/tanstack/registry-item.json' assert { type: 'json' }
import nextjs from './default/clients/nextjs/registry-item.json' with { type: 'json' }
import reactRouter from './default/clients/react-router/registry-item.json' with { type: 'json' }
import react from './default/clients/react/registry-item.json' with { type: 'json' }
import tanstack from './default/clients/tanstack/registry-item.json' with { type: 'json' }
export const clients = [nextjs, react, reactRouter, tanstack] as Registry['items']
@@ -8,7 +8,7 @@ export default async function ProtectedPage() {
const { data, error } = await supabase.auth.getUser()
if (error || !data?.user) {
redirect('/login')
redirect('/auth/login')
}
return (
@@ -31,7 +31,7 @@ export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRe
})
if (error) throw error
// Update this route to redirect to an authenticated route. The user already has an active session.
location.href = '/info'
location.href = '/protected'
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
@@ -27,7 +27,7 @@ export function UpdatePasswordForm({ className, ...props }: React.ComponentProps
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.
location.href = '/info'
location.href = '/protected'
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
@@ -20,10 +20,6 @@
{
"path": "registry/default/blocks/password-based-auth-react/components/update-password-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/clients/react/lib/supabase/client.ts",
"type": "registry:lib"
}
],
"dependencies": ["@supabase/supabase-js@latest"]
@@ -33,7 +33,7 @@ export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRe
})
if (error) throw error
// Update this route to redirect to an authenticated route. The user already has an active session.
await navigate({ to: '/info' })
await navigate({ to: '/protected' })
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
@@ -29,7 +29,7 @@ export function UpdatePasswordForm({ className, ...props }: React.ComponentProps
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' })
await navigate({ to: '/protected' })
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
@@ -22,9 +22,9 @@
"target": "routes/_protected.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/_protected/info.tsx",
"path": "registry/default/blocks/password-based-auth-tanstack/routes/_protected/protected.tsx",
"type": "registry:file",
"target": "routes/_protected/info.tsx"
"target": "routes/_protected/protected.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/auth/confirm.ts",
@@ -1,5 +1,3 @@
// SUPABASE NOTE: THIS FILE WAS ADDED TO SATISFY TANSTACK BUILD CONFIGURATION. IT IS NOT INCLUDED IN THE BLOCK.
/* eslint-disable */
// @ts-nocheck
@@ -14,7 +12,7 @@
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 ProtectedProtectedImport } from './routes/_protected/protected'
import { Route as AuthConfirmImport } from './routes/auth/confirm'
import { Route as AuthErrorImport } from './routes/auth/error'
import { Route as ForgotPasswordImport } from './routes/forgot-password'
@@ -79,9 +77,9 @@ const AuthConfirmRoute = AuthConfirmImport.update({
getParentRoute: () => rootRoute,
} as any)
const ProtectedInfoRoute = ProtectedInfoImport.update({
id: '/info',
path: '/info',
const ProtectedProtectedRoute = ProtectedProtectedImport.update({
id: '/protected',
path: '/protected',
getParentRoute: () => ProtectedRoute,
} as any)
@@ -138,11 +136,11 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof UpdatePasswordImport
parentRoute: typeof rootRoute
}
'/_protected/info': {
id: '/_protected/info'
path: '/info'
fullPath: '/info'
preLoaderRoute: typeof ProtectedInfoImport
'/_protected/protected': {
id: '/_protected/protected'
path: '/protected'
fullPath: '/protected'
preLoaderRoute: typeof ProtectedProtectedImport
parentRoute: typeof ProtectedImport
}
'/auth/confirm': {
@@ -165,11 +163,11 @@ declare module '@tanstack/react-router' {
// Create and export the route tree
interface ProtectedRouteChildren {
ProtectedInfoRoute: typeof ProtectedInfoRoute
ProtectedProtectedRoute: typeof ProtectedProtectedRoute
}
const ProtectedRouteChildren: ProtectedRouteChildren = {
ProtectedInfoRoute: ProtectedInfoRoute,
ProtectedProtectedRoute: ProtectedProtectedRoute,
}
const ProtectedRouteWithChildren = ProtectedRoute._addFileChildren(ProtectedRouteChildren)
@@ -182,7 +180,7 @@ export interface FileRoutesByFullPath {
'/sign-up': typeof SignUpRoute
'/sign-up-success': typeof SignUpSuccessRoute
'/update-password': typeof UpdatePasswordRoute
'/info': typeof ProtectedInfoRoute
'/protected': typeof ProtectedProtectedRoute
'/auth/confirm': typeof AuthConfirmRoute
'/auth/error': typeof AuthErrorRoute
}
@@ -195,7 +193,7 @@ export interface FileRoutesByTo {
'/sign-up': typeof SignUpRoute
'/sign-up-success': typeof SignUpSuccessRoute
'/update-password': typeof UpdatePasswordRoute
'/info': typeof ProtectedInfoRoute
'/protected': typeof ProtectedProtectedRoute
'/auth/confirm': typeof AuthConfirmRoute
'/auth/error': typeof AuthErrorRoute
}
@@ -209,7 +207,7 @@ export interface FileRoutesById {
'/sign-up': typeof SignUpRoute
'/sign-up-success': typeof SignUpSuccessRoute
'/update-password': typeof UpdatePasswordRoute
'/_protected/info': typeof ProtectedInfoRoute
'/_protected/protected': typeof ProtectedProtectedRoute
'/auth/confirm': typeof AuthConfirmRoute
'/auth/error': typeof AuthErrorRoute
}
@@ -224,7 +222,7 @@ export interface FileRouteTypes {
| '/sign-up'
| '/sign-up-success'
| '/update-password'
| '/info'
| '/protected'
| '/auth/confirm'
| '/auth/error'
fileRoutesByTo: FileRoutesByTo
@@ -236,7 +234,7 @@ export interface FileRouteTypes {
| '/sign-up'
| '/sign-up-success'
| '/update-password'
| '/info'
| '/protected'
| '/auth/confirm'
| '/auth/error'
id:
@@ -248,7 +246,7 @@ export interface FileRouteTypes {
| '/sign-up'
| '/sign-up-success'
| '/update-password'
| '/_protected/info'
| '/_protected/protected'
| '/auth/confirm'
| '/auth/error'
fileRoutesById: FileRoutesById
@@ -305,7 +303,7 @@ export const routeTree = rootRoute
"/_protected": {
"filePath": "_protected.tsx",
"children": [
"/_protected/info"
"/_protected/protected"
]
},
"/forgot-password": {
@@ -323,8 +321,8 @@ export const routeTree = rootRoute
"/update-password": {
"filePath": "update-password.tsx"
},
"/_protected/info": {
"filePath": "_protected/info.tsx",
"/_protected/protected": {
"filePath": "_protected/protected.tsx",
"parent": "/_protected"
},
"/auth/confirm": {
@@ -0,0 +1,16 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_protected/protected')({
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,26 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/registry/default/components/ui/card'
export default async function Page({ searchParams }: { searchParams: Promise<{ error: string }> }) {
const params = await searchParams
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,11 @@
import { LoginForm } from '@/registry/default/blocks/social-auth-nextjs/components/login-form'
export default function Page() {
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,30 @@
import { NextResponse } from 'next/server'
// The client you created from the Server-Side Auth instructions
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
// if "next" is in param, use it as the redirect URL
const next = searchParams.get('next') ?? '/'
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer
const isLocalEnv = process.env.NODE_ENV === 'development'
if (isLocalEnv) {
// we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
return NextResponse.redirect(`${origin}${next}`)
} else if (forwardedHost) {
return NextResponse.redirect(`https://${forwardedHost}${next}`)
} else {
return NextResponse.redirect(`${origin}${next}`)
}
}
}
// return the user to an error page with instructions
return NextResponse.redirect(`${origin}/auth/error`)
}
@@ -0,0 +1,22 @@
import { redirect } from 'next/navigation'
import { LogoutButton } from '@/registry/default/blocks/social-auth-nextjs/components/logout-button'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'
export default async function ProtectedPage() {
const supabase = await createClient()
const { data, error } = await supabase.auth.getUser()
if (error || !data?.user) {
redirect('/auth/login')
}
return (
<div className="flex h-svh w-full items-center justify-center gap-2">
<p>
Hello <span>{data.user.email}</span>
</p>
<LogoutButton />
</div>
)
}
@@ -0,0 +1,60 @@
'use client'
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { useState } from 'react'
export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const handleSocialLogin = async (e: React.FormEvent) => {
e.preventDefault()
const supabase = createClient()
setIsLoading(true)
setError(null)
try {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${window.location.origin}/auth/oauth?next=/protected`,
},
})
if (error) throw error
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Welcome!</CardTitle>
<CardDescription>Sign in to your account to continue</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSocialLogin}>
<div className="flex flex-col gap-6">
{error && <p className="text-sm text-destructive-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Continue with Github'}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,17 @@
'use client'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import { useRouter } from 'next/navigation'
export function LogoutButton() {
const router = useRouter()
const logout = async () => {
const supabase = createClient()
await supabase.auth.signOut()
router.push('/example/password-based-auth/auth/login')
}
return <Button onClick={logout}>Logout</Button>
}
@@ -0,0 +1,19 @@
import { updateSession } from '@/registry/default/clients/nextjs/lib/supabase/middleware'
import { type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
@@ -0,0 +1,43 @@
{
"name": "social-auth-nextjs",
"type": "registry:block",
"title": "Social Auth flow for Nextjs and Supabase",
"description": "Social Auth flow for Nextjs and Supabase",
"registryDependencies": ["button", "card"],
"dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"],
"files": [
{
"path": "registry/default/blocks/social-auth-nextjs/app/auth/login/page.tsx",
"type": "registry:page",
"target": "app/auth/login/page.tsx"
},
{
"path": "registry/default/blocks/social-auth-nextjs/app/auth/error/page.tsx",
"type": "registry:page",
"target": "app/auth/error/page.tsx"
},
{
"path": "registry/default/blocks/social-auth-nextjs/app/protected/page.tsx",
"type": "registry:page",
"target": "app/protected/page.tsx"
},
{
"path": "registry/default/blocks/social-auth-nextjs/app/auth/oauth/route.ts",
"type": "registry:page",
"target": "app/auth/oauth/route.ts"
},
{
"path": "registry/default/blocks/social-auth-nextjs/components/login-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/social-auth-nextjs/middleware.ts",
"type": "registry:file",
"target": "middleware.ts"
},
{
"path": "registry/default/blocks/social-auth-nextjs/components/logout-button.tsx",
"type": "registry:component"
}
]
}
@@ -0,0 +1,4 @@
import { type RouteConfig } from '@react-router/dev/routes'
import { flatRoutes } from '@react-router/fs-routes'
export default flatRoutes() satisfies RouteConfig
@@ -0,0 +1,29 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/registry/default/components/ui/card'
import { useSearchParams } from 'react-router'
export default function Page() {
let [searchParams] = useSearchParams()
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>
{searchParams?.get('error') ? (
<p className="text-sm text-muted-foreground">
Code error: {searchParams?.get('error')}
</p>
) : (
<p className="text-sm text-muted-foreground">An unspecified error occurred.</p>
)}
</CardContent>
</Card>
</div>
</div>
</div>
)
}
@@ -0,0 +1,20 @@
import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'
import { type LoaderFunctionArgs, redirect } from 'react-router'
export async function loader({ request }: LoaderFunctionArgs) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
const next = requestUrl.searchParams.get('next') || '/'
if (code) {
const { supabase, headers } = createClient(request)
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return redirect(next, { headers })
} else {
return redirect(`/auth/error?error=${error?.message}`)
}
}
// redirect the user to an error page with some instructions
return redirect(`/auth/error`)
}
@@ -0,0 +1,64 @@
import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { type ActionFunctionArgs, redirect, useFetcher } from 'react-router'
export const action = async ({ request }: ActionFunctionArgs) => {
const { supabase } = createClient(request)
const origin = new URL(request.url).origin
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${origin}/auth/oauth?next=/protected`,
},
})
if (data.url) {
return redirect(data.url)
}
if (error) {
return {
error: error instanceof Error ? error.message : 'An error occurred',
}
}
}
export default function Login() {
const fetcher = useFetcher<typeof action>()
const error = fetcher.data?.error
const loading = fetcher.state === 'submitting'
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">Welcome!</CardTitle>
<CardDescription>Sign in to your account to continue</CardDescription>
</CardHeader>
<CardContent>
<fetcher.Form method="post">
<div className="flex flex-col gap-6">
{error && <p className="text-sm text-destructive-500">{error}</p>}
<Button type="submit" className="w-full" disabled={loading}>
{loading ? 'Logging in...' : 'Continue with Github'}
</Button>
</div>
</fetcher.Form>
</CardContent>
</Card>
</div>
</div>
</div>
)
}
@@ -0,0 +1,16 @@
import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'
import { type ActionFunctionArgs, redirect } from 'react-router'
export async function loader({ request }: ActionFunctionArgs) {
const { supabase, headers } = createClient(request)
const { error } = await supabase.auth.signOut()
if (error) {
console.error(error)
return { success: false, error: error.message }
}
// Redirect to dashboard or home page after successful sign-in
return redirect('/', { headers })
}
@@ -0,0 +1,29 @@
import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'
import { Button } from '@/registry/default/components/ui/button'
import { type LoaderFunctionArgs, redirect, useLoaderData } from 'react-router'
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { supabase } = createClient(request)
const { data, error } = await supabase.auth.getUser()
if (error || !data?.user) {
return redirect('/login')
}
return data
}
export default function ProtectedPage() {
let data = useLoaderData<typeof loader>()
return (
<div className="flex items-center justify-center h-screen gap-2">
<p>
Hello <span className="text-primary font-semibold">{data.user.email}</span>
</p>
<a href="/logout">
<Button>Logout</Button>
</a>
</div>
)
}
@@ -0,0 +1,45 @@
{
"name": "social-auth-react-router",
"type": "registry:block",
"title": "Social Auth flow for React Router and Supabase",
"description": "Social Auth flow for React Router and Supabase",
"registryDependencies": ["button", "card"],
"dependencies": [
"@supabase/ssr@latest",
"@react-router/dev@latest",
"@react-router/fs-routes@latest"
],
"files": [
{
"path": "registry/default/blocks/social-auth-react-router/app/routes/auth.error.tsx",
"type": "registry:file",
"target": "app/routes/auth.error.tsx"
},
{
"path": "registry/default/blocks/social-auth-react-router/app/routes/auth.oauth.tsx",
"type": "registry:file",
"target": "app/routes/auth.oauth.tsx"
},
{
"path": "registry/default/blocks/social-auth-react-router/app/routes/login.tsx",
"type": "registry:file",
"target": "app/routes/login.tsx"
},
{
"path": "registry/default/blocks/social-auth-react-router/app/routes/logout.tsx",
"type": "registry:file",
"target": "app/routes/logout.tsx"
},
{
"path": "registry/default/blocks/social-auth-react-router/app/routes/protected.tsx",
"type": "registry:file",
"target": "app/routes/protected.tsx"
},
{
"path": "registry/default/blocks/social-auth-react-router/app/routes.ts",
"type": "registry:file",
"target": "app/routes.ts"
}
]
}
@@ -0,0 +1,58 @@
'use client'
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { useState } from 'react'
export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const handleSocialLogin = async (e: React.FormEvent) => {
e.preventDefault()
const supabase = createClient()
setIsLoading(true)
setError(null)
try {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'github',
})
if (error) throw error
location.href = '/protected'
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Welcome!</CardTitle>
<CardDescription>Sign in to your account to continue</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSocialLogin}>
<div className="flex flex-col gap-6">
{error && <p className="text-sm text-destructive-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Continue with Github'}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,14 @@
{
"name": "social-auth-react",
"type": "registry:block",
"title": "Social Auth flow for React and Supabase",
"description": "Social Auth flow for React and Supabase",
"registryDependencies": ["button", "card"],
"files": [
{
"path": "registry/default/blocks/social-auth-react/components/login-form.tsx",
"type": "registry:component"
}
],
"dependencies": ["@supabase/supabase-js@latest"]
}
@@ -0,0 +1,58 @@
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 { useState } from 'react'
export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const handleSocialLogin = async (e: React.FormEvent) => {
e.preventDefault()
const supabase = createClient()
setIsLoading(true)
setError(null)
try {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${window.location.origin}/auth/oauth?next=/protected`,
},
})
if (error) throw error
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Welcome!</CardTitle>
<CardDescription>Sign in to your account to continue</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSocialLogin}>
<div className="flex flex-col gap-6">
{error && <p className="text-sm text-destructive-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Continue with Github'}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,19 @@
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: () => Promise<SSRSafeUser | null> = 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,43 @@
{
"name": "social-auth-tanstack",
"type": "registry:block",
"title": "Social Auth flow for TanStack and Supabase",
"description": "Social Auth flow for TanStack and Supabase",
"registryDependencies": ["button", "card"],
"dependencies": ["@supabase/ssr@latest"],
"files": [
{
"path": "registry/default/blocks/social-auth-tanstack/components/login-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/social-auth-tanstack/lib/supabase/fetch-user-server-fn.ts",
"type": "registry:lib"
},
{
"path": "registry/default/blocks/social-auth-tanstack/routes/_protected.tsx",
"type": "registry:file",
"target": "routes/_protected.tsx"
},
{
"path": "registry/default/blocks/social-auth-tanstack/routes/_protected/protected.tsx",
"type": "registry:file",
"target": "routes/_protected/protected.tsx"
},
{
"path": "registry/default/blocks/social-auth-tanstack/routes/auth/error.tsx",
"type": "registry:file",
"target": "routes/auth/error.tsx"
},
{
"path": "registry/default/blocks/social-auth-tanstack/routes/auth/oauth.ts",
"type": "registry:file",
"target": "routes/auth/oauth.ts"
},
{
"path": "registry/default/blocks/social-auth-tanstack/routes/login.tsx",
"type": "registry:file",
"target": "routes/login.tsx"
}
]
}
@@ -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,16 @@
import { fetchUser } from '@/registry/default/blocks/social-auth-tanstack/lib/supabase/fetch-user-server-fn'
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_protected')({
beforeLoad: async () => {
const user = await fetchUser()
if (!user) {
throw redirect({ to: '/login' })
}
return {
user,
}
},
})
@@ -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,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`, search: { 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`,
search: { error: error?.message },
})
}
}
// redirect the user to an error page with some instructions
throw redirect({
to: `/auth/error`,
search: { error: 'No token hash or type' },
})
})
export const Route = createFileRoute('/auth/confirm')({
preload: false,
loader: (opts) => confirmFn({ data: opts.location.search }),
})
@@ -0,0 +1,16 @@
import { LoginForm } from '@/registry/default/blocks/social-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>
)
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { examples } from '@/registry/examples'
import type { RegistryItem } from 'shadcn/registry'
import { blocks } from './blocks'
import { clients } from './clients'
import aiEditorRules from './default/ai-editor-rules/registry-item.json' assert { type: 'json' }
import aiEditorRules from './default/ai-editor-rules/registry-item.json' with { type: 'json' }
export const registry = {
name: 'Supabase UI Library',
+1
View File
@@ -4,6 +4,7 @@ export interface NavItem {
href?: string
disabled?: boolean
external?: boolean
new?: boolean
icon?: any // to do: clean up later | keyof typeof Icons
label?: string
supportedFrameworks?: supportedFrameworks[]