mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 08:56:46 -04:00
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:
@@ -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: []
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 app’s 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 app’s 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 app’s 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 app’s 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'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'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'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'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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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']
|
||||
|
||||
+1
-1
@@ -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 (
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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"]
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+2
-2
@@ -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",
|
||||
|
||||
+20
-22
@@ -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": {
|
||||
|
||||
+16
@@ -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>
|
||||
)
|
||||
}
|
||||
+17
@@ -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
|
||||
+29
@@ -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>
|
||||
)
|
||||
}
|
||||
+20
@@ -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>
|
||||
)
|
||||
}
|
||||
+16
@@ -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 })
|
||||
}
|
||||
+29
@@ -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"]
|
||||
}
|
||||
+58
@@ -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>
|
||||
)
|
||||
}
|
||||
+19
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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[]
|
||||
|
||||
Reference in New Issue
Block a user