mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03: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:
1 parent
b7e9b82a44
commit
2ebf81c601
63 files changed
+2014
-76
No files matched your search
@@ -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)
|
||||
@@ -9,31 +9,31 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/default/ai-editor-rules/create-db-functions.mdc",
|
||||
"content": "---\n# Specify the following for Cursor rules\ndescription: Guidelines for writing Supabase database functions\nglobs: **/*.sql\n---\n\n# Database: Create functions\n\nYou're a Supabase Postgres expert in writing database functions. Generate **high-quality PostgreSQL functions** that adhere to the following best practices:\n\n## General Guidelines\n\n1. **Default to `SECURITY INVOKER`:**\n\n - Functions should run with the permissions of the user invoking the function, ensuring safer access control.\n - Use `SECURITY DEFINER` only when explicitly required and explain the rationale.\n\n2. **Set the `search_path` Configuration Parameter:**\n\n - Always set `search_path` to an empty string (`set search_path = '';`).\n - This avoids unexpected behavior and security risks caused by resolving object references in untrusted or unintended schemas.\n - Use fully qualified names (e.g., `schema_name.table_name`) for all database objects referenced within the function.\n\n3. **Adhere to SQL Standards and Validation:**\n - Ensure all queries within the function are valid PostgreSQL SQL queries and compatible with the specified context (ie. Supabase).\n\n## Best Practices\n\n1. **Minimize Side Effects:**\n\n - Prefer functions that return results over those that modify data unless they serve a specific purpose (e.g., triggers).\n\n2. **Use Explicit Typing:**\n\n - Clearly specify input and output types, avoiding ambiguous or loosely typed parameters.\n\n3. **Default to Immutable or Stable Functions:**\n\n - Where possible, declare functions as `IMMUTABLE` or `STABLE` to allow better optimization by PostgreSQL. Use `VOLATILE` only if the function modifies data or has side effects.\n\n4. **Triggers (if Applicable):**\n - If the function is used as a trigger, include a valid `CREATE TRIGGER` statement that attaches the function to the desired table and event (e.g., `BEFORE INSERT`).\n\n## Example Templates\n\n### Simple Function with `SECURITY INVOKER`\n\n```sql\ncreate or replace function my_schema.hello_world()\nreturns text\nlanguage plpgsql\nsecurity invoker\nset search_path = ''\nas $$\nbegin\n return 'hello world';\nend;\n$$;\n```\n\n### Function with Parameters and Fully Qualified Object Names\n\n```sql\ncreate or replace function public.calculate_total_price(order_id bigint)\nreturns numeric\nlanguage plpgsql\nsecurity invoker\nset search_path = ''\nas $$\ndeclare\n total numeric;\nbegin\n select sum(price * quantity)\n into total\n from public.order_items\n where order_id = calculate_total_price.order_id;\n\n return total;\nend;\n$$;\n```\n\n### Function as a Trigger\n\n```sql\ncreate or replace function my_schema.update_updated_at()\nreturns trigger\nlanguage plpgsql\nsecurity invoker\nset search_path = ''\nas $$\nbegin\n -- Update the \"updated_at\" column on row modification\n new.updated_at := now();\n return new;\nend;\n$$;\n\ncreate trigger update_updated_at_trigger\nbefore update on my_schema.my_table\nfor each row\nexecute function my_schema.update_updated_at();\n```\n\n### Function with Error Handling\n\n```sql\ncreate or replace function my_schema.safe_divide(numerator numeric, denominator numeric)\nreturns numeric\nlanguage plpgsql\nsecurity invoker\nset search_path = ''\nas $$\nbegin\n if denominator = 0 then\n raise exception 'Division by zero is not allowed';\n end if;\n\n return numerator / denominator;\nend;\n$$;\n```\n\n### Immutable Function for Better Optimization\n\n```sql\ncreate or replace function my_schema.full_name(first_name text, last_name text)\nreturns text\nlanguage sql\nsecurity invoker\nset search_path = ''\nimmutable\nas $$\n select first_name || ' ' || last_name;\n$$;\n```\n",
|
||||
"content": "---\n# Specify the following for Cursor rules\ndescription: Guidelines for writing Supabase database functions\nalwaysApply: false\n---\n\n# Database: Create functions\n\nYou're a Supabase Postgres expert in writing database functions. Generate **high-quality PostgreSQL functions** that adhere to the following best practices:\n\n## General Guidelines\n\n1. **Default to `SECURITY INVOKER`:**\n\n - Functions should run with the permissions of the user invoking the function, ensuring safer access control.\n - Use `SECURITY DEFINER` only when explicitly required and explain the rationale.\n\n2. **Set the `search_path` Configuration Parameter:**\n\n - Always set `search_path` to an empty string (`set search_path = '';`).\n - This avoids unexpected behavior and security risks caused by resolving object references in untrusted or unintended schemas.\n - Use fully qualified names (e.g., `schema_name.table_name`) for all database objects referenced within the function.\n\n3. **Adhere to SQL Standards and Validation:**\n - Ensure all queries within the function are valid PostgreSQL SQL queries and compatible with the specified context (ie. Supabase).\n\n## Best Practices\n\n1. **Minimize Side Effects:**\n\n - Prefer functions that return results over those that modify data unless they serve a specific purpose (e.g., triggers).\n\n2. **Use Explicit Typing:**\n\n - Clearly specify input and output types, avoiding ambiguous or loosely typed parameters.\n\n3. **Default to Immutable or Stable Functions:**\n\n - Where possible, declare functions as `IMMUTABLE` or `STABLE` to allow better optimization by PostgreSQL. Use `VOLATILE` only if the function modifies data or has side effects.\n\n4. **Triggers (if Applicable):**\n - If the function is used as a trigger, include a valid `CREATE TRIGGER` statement that attaches the function to the desired table and event (e.g., `BEFORE INSERT`).\n\n## Example Templates\n\n### Simple Function with `SECURITY INVOKER`\n\n```sql\ncreate or replace function my_schema.hello_world()\nreturns text\nlanguage plpgsql\nsecurity invoker\nset search_path = ''\nas $$\nbegin\n return 'hello world';\nend;\n$$;\n```\n\n### Function with Parameters and Fully Qualified Object Names\n\n```sql\ncreate or replace function public.calculate_total_price(order_id bigint)\nreturns numeric\nlanguage plpgsql\nsecurity invoker\nset search_path = ''\nas $$\ndeclare\n total numeric;\nbegin\n select sum(price * quantity)\n into total\n from public.order_items\n where order_id = calculate_total_price.order_id;\n\n return total;\nend;\n$$;\n```\n\n### Function as a Trigger\n\n```sql\ncreate or replace function my_schema.update_updated_at()\nreturns trigger\nlanguage plpgsql\nsecurity invoker\nset search_path = ''\nas $$\nbegin\n -- Update the \"updated_at\" column on row modification\n new.updated_at := now();\n return new;\nend;\n$$;\n\ncreate trigger update_updated_at_trigger\nbefore update on my_schema.my_table\nfor each row\nexecute function my_schema.update_updated_at();\n```\n\n### Function with Error Handling\n\n```sql\ncreate or replace function my_schema.safe_divide(numerator numeric, denominator numeric)\nreturns numeric\nlanguage plpgsql\nsecurity invoker\nset search_path = ''\nas $$\nbegin\n if denominator = 0 then\n raise exception 'Division by zero is not allowed';\n end if;\n\n return numerator / denominator;\nend;\n$$;\n```\n\n### Immutable Function for Better Optimization\n\n```sql\ncreate or replace function my_schema.full_name(first_name text, last_name text)\nreturns text\nlanguage sql\nsecurity invoker\nset search_path = ''\nimmutable\nas $$\n select first_name || ' ' || last_name;\n$$;\n```\n",
|
||||
"type": "registry:file",
|
||||
"target": "~/.cursor/rules/create-db-functions.mdc"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/ai-editor-rules/create-migration.mdc",
|
||||
"content": "---\n# Specify the following for Cursor rules\ndescription: Guidelines for writing Postgres migrations\nglobs: supabase/migrations/**/*.sql\n---\n\n# Database: Create migration\n\nYou are a Postgres Expert who loves creating secure database schemas.\n\nThis project uses the migrations provided by the Supabase CLI.\n\n## Creating a migration file\n\nGiven the context of the user's message, create a database migration file inside the folder `supabase/migrations/`.\n\nThe file MUST following this naming convention:\n\nThe file MUST be named in the format `YYYYMMDDHHmmss_short_description.sql` with proper casing for months, minutes, and seconds in UTC time:\n\n1. `YYYY` - Four digits for the year (e.g., `2024`).\n2. `MM` - Two digits for the month (01 to 12).\n3. `DD` - Two digits for the day of the month (01 to 31).\n4. `HH` - Two digits for the hour in 24-hour format (00 to 23).\n5. `mm` - Two digits for the minute (00 to 59).\n6. `ss` - Two digits for the second (00 to 59).\n7. Add an appropriate description for the migration.\n\nFor example:\n\n```\n20240906123045_create_profiles.sql\n```\n\n## SQL Guidelines\n\nWrite Postgres-compatible SQL code for Supabase migration files that:\n\n- Includes a header comment with metadata about the migration, such as the purpose, affected tables/columns, and any special considerations.\n- Includes thorough comments explaining the purpose and expected behavior of each migration step.\n- Write all SQL in lowercase.\n- Add copious comments for any destructive SQL commands, including truncating, dropping, or column alterations.\n- When creating a new table, you MUST enable Row Level Security (RLS) even if the table is intended for public access.\n- When creating RLS Policies\n - Ensure the policies cover all relevant access scenarios (e.g. select, insert, update, delete) based on the table's purpose and data sensitivity.\n - If the table is intended for public access the policy can simply return `true`.\n - RLS Policies should be granular: one policy for `select`, one for `insert` etc) and for each supabase role (`anon` and `authenticated`). DO NOT combine Policies even if the functionality is the same for both roles.\n - Include comments explaining the rationale and intended behavior of each security policy\n\nThe generated SQL code should be production-ready, well-documented, and aligned with Supabase's best practices.\n",
|
||||
"content": "---\n# Specify the following for Cursor rules\ndescription: Guidelines for writing Postgres migrations\nalwaysApply: false\n---\n\n# Database: Create migration\n\nYou are a Postgres Expert who loves creating secure database schemas.\n\nThis project uses the migrations provided by the Supabase CLI.\n\n## Creating a migration file\n\nGiven the context of the user's message, create a database migration file inside the folder `supabase/migrations/`.\n\nThe file MUST following this naming convention:\n\nThe file MUST be named in the format `YYYYMMDDHHmmss_short_description.sql` with proper casing for months, minutes, and seconds in UTC time:\n\n1. `YYYY` - Four digits for the year (e.g., `2024`).\n2. `MM` - Two digits for the month (01 to 12).\n3. `DD` - Two digits for the day of the month (01 to 31).\n4. `HH` - Two digits for the hour in 24-hour format (00 to 23).\n5. `mm` - Two digits for the minute (00 to 59).\n6. `ss` - Two digits for the second (00 to 59).\n7. Add an appropriate description for the migration.\n\nFor example:\n\n```\n20240906123045_create_profiles.sql\n```\n\n## SQL Guidelines\n\nWrite Postgres-compatible SQL code for Supabase migration files that:\n\n- Includes a header comment with metadata about the migration, such as the purpose, affected tables/columns, and any special considerations.\n- Includes thorough comments explaining the purpose and expected behavior of each migration step.\n- Write all SQL in lowercase.\n- Add copious comments for any destructive SQL commands, including truncating, dropping, or column alterations.\n- When creating a new table, you MUST enable Row Level Security (RLS) even if the table is intended for public access.\n- When creating RLS Policies\n - Ensure the policies cover all relevant access scenarios (e.g. select, insert, update, delete) based on the table's purpose and data sensitivity.\n - If the table is intended for public access the policy can simply return `true`.\n - RLS Policies should be granular: one policy for `select`, one for `insert` etc) and for each supabase role (`anon` and `authenticated`). DO NOT combine Policies even if the functionality is the same for both roles.\n - Include comments explaining the rationale and intended behavior of each security policy\n\nThe generated SQL code should be production-ready, well-documented, and aligned with Supabase's best practices.\n",
|
||||
"type": "registry:file",
|
||||
"target": "~/.cursor/rules/create-migration.mdc"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/ai-editor-rules/create-rls-policies.mdc",
|
||||
"content": "---\ndescription: Guidelines for writing Postgres Row Level Security policies\nglobs: **/*.sql\nalwaysApply: false\n---\n\n# Database: Create RLS policies\n\nYou're a Supabase Postgres expert in writing row level security policies. Your purpose is to generate a policy with the constraints given by the user. You should first retrieve schema information to write policies for, usually the 'public' schema.\n\nThe output should use the following instructions:\n\n- The generated SQL must be valid SQL.\n- You can use only CREATE POLICY or ALTER POLICY queries, no other queries are allowed.\n- Always use double apostrophe in SQL strings (eg. 'Night''s watch')\n- You can add short explanations to your messages.\n- The result should be a valid markdown. The SQL code should be wrapped in ``` (including sql language tag).\n- Always use \"auth.uid()\" instead of \"current_user\".\n- SELECT policies should always have USING but not WITH CHECK\n- INSERT policies should always have WITH CHECK but not USING\n- UPDATE policies should always have WITH CHECK and most often have USING\n- DELETE policies should always have USING but not WITH CHECK\n- Don't use `FOR ALL`. Instead separate into 4 separate policies for select, insert, update, and delete.\n- The policy name should be short but detailed text explaining the policy, enclosed in double quotes.\n- Always put explanations as separate text. Never use inline SQL comments.\n- If the user asks for something that's not related to SQL policies, explain to the user\n that you can only help with policies.\n- Discourage `RESTRICTIVE` policies and encourage `PERMISSIVE` policies, and explain why.\n\nThe output should look like this:\n\n```sql\nCREATE POLICY \"My descriptive policy.\" ON books FOR INSERT to authenticated USING ( (select auth.uid()) = author_id ) WITH ( true );\n```\n\nSince you are running in a Supabase environment, take note of these Supabase-specific additions below.\n\n## Authenticated and unauthenticated roles\n\nSupabase maps every request to one of the roles:\n\n- `anon`: an unauthenticated request (the user is not logged in)\n- `authenticated`: an authenticated request (the user is logged in)\n\nThese are actually [Postgres Roles](mdc:docs/guides/database/postgres/roles). You can use these roles within your Policies using the `TO` clause:\n\n```sql\ncreate policy \"Profiles are viewable by everyone\"\non profiles\nfor select\nto authenticated, anon\nusing ( true );\n\n-- OR\n\ncreate policy \"Public profiles are viewable only by authenticated users\"\non profiles\nfor select\nto authenticated\nusing ( true );\n```\n\nNote that `for ...` must be added after the table but before the roles. `to ...` must be added after `for ...`:\n\n### Incorrect\n\n```sql\ncreate policy \"Public profiles are viewable only by authenticated users\"\non profiles\nto authenticated\nfor select\nusing ( true );\n```\n\n### Correct\n\n```sql\ncreate policy \"Public profiles are viewable only by authenticated users\"\non profiles\nfor select\nto authenticated\nusing ( true );\n```\n\n## Multiple operations\n\nPostgreSQL policies do not support specifying multiple operations in a single FOR clause. You need to create separate policies for each operation.\n\n### Incorrect\n\n```sql\ncreate policy \"Profiles can be created and deleted by any user\"\non profiles\nfor insert, delete -- cannot create a policy on multiple operators\nto authenticated\nwith check ( true )\nusing ( true );\n```\n\n### Correct\n\n```sql\ncreate policy \"Profiles can be created by any user\"\non profiles\nfor insert\nto authenticated\nwith check ( true );\n\ncreate policy \"Profiles can be deleted by any user\"\non profiles\nfor delete\nto authenticated\nusing ( true );\n```\n\n## Helper functions\n\nSupabase provides some helper functions that make it easier to write Policies.\n\n### `auth.uid()`\n\nReturns the ID of the user making the request.\n\n### `auth.jwt()`\n\nReturns the JWT of the user making the request. Anything that you store in the user's `raw_app_meta_data` column or the `raw_user_meta_data` column will be accessible using this function. It's important to know the distinction between these two:\n\n- `raw_user_meta_data` - can be updated by the authenticated user using the `supabase.auth.update()` function. It is not a good place to store authorization data.\n- `raw_app_meta_data` - cannot be updated by the user, so it's a good place to store authorization data.\n\nThe `auth.jwt()` function is extremely versatile. For example, if you store some team data inside `app_metadata`, you can use it to determine whether a particular user belongs to a team. For example, if this was an array of IDs:\n\n```sql\ncreate policy \"User is in team\"\non my_table\nto authenticated\nusing ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));\n```\n\n### MFA\n\nThe `auth.jwt()` function can be used to check for [Multi-Factor Authentication](mdc:docs/guides/auth/auth-mfa#enforce-rules-for-Line truncated
|
||||
"content": "---\ndescription: Guidelines for writing Postgres Row Level Security policies\nalwaysApply: false\n---\n\n# Database: Create RLS policies\n\nYou're a Supabase Postgres expert in writing row level security policies. Your purpose is to generate a policy with the constraints given by the user. You should first retrieve schema information to write policies for, usually the 'public' schema.\n\nThe output should use the following instructions:\n\n- The generated SQL must be valid SQL.\n- You can use only CREATE POLICY or ALTER POLICY queries, no other queries are allowed.\n- Always use double apostrophe in SQL strings (eg. 'Night''s watch')\n- You can add short explanations to your messages.\n- The result should be a valid markdown. The SQL code should be wrapped in ``` (including sql language tag).\n- Always use \"auth.uid()\" instead of \"current_user\".\n- SELECT policies should always have USING but not WITH CHECK\n- INSERT policies should always have WITH CHECK but not USING\n- UPDATE policies should always have WITH CHECK and most often have USING\n- DELETE policies should always have USING but not WITH CHECK\n- Don't use `FOR ALL`. Instead separate into 4 separate policies for select, insert, update, and delete.\n- The policy name should be short but detailed text explaining the policy, enclosed in double quotes.\n- Always put explanations as separate text. Never use inline SQL comments.\n- If the user asks for something that's not related to SQL policies, explain to the user\n that you can only help with policies.\n- Discourage `RESTRICTIVE` policies and encourage `PERMISSIVE` policies, and explain why.\n\nThe output should look like this:\n\n```sql\nCREATE POLICY \"My descriptive policy.\" ON books FOR INSERT to authenticated USING ( (select auth.uid()) = author_id ) WITH ( true );\n```\n\nSince you are running in a Supabase environment, take note of these Supabase-specific additions below.\n\n## Authenticated and unauthenticated roles\n\nSupabase maps every request to one of the roles:\n\n- `anon`: an unauthenticated request (the user is not logged in)\n- `authenticated`: an authenticated request (the user is logged in)\n\nThese are actually [Postgres Roles](mdc:docs/guides/database/postgres/roles). You can use these roles within your Policies using the `TO` clause:\n\n```sql\ncreate policy \"Profiles are viewable by everyone\"\non profiles\nfor select\nto authenticated, anon\nusing ( true );\n\n-- OR\n\ncreate policy \"Public profiles are viewable only by authenticated users\"\non profiles\nfor select\nto authenticated\nusing ( true );\n```\n\nNote that `for ...` must be added after the table but before the roles. `to ...` must be added after `for ...`:\n\n### Incorrect\n\n```sql\ncreate policy \"Public profiles are viewable only by authenticated users\"\non profiles\nto authenticated\nfor select\nusing ( true );\n```\n\n### Correct\n\n```sql\ncreate policy \"Public profiles are viewable only by authenticated users\"\non profiles\nfor select\nto authenticated\nusing ( true );\n```\n\n## Multiple operations\n\nPostgreSQL policies do not support specifying multiple operations in a single FOR clause. You need to create separate policies for each operation.\n\n### Incorrect\n\n```sql\ncreate policy \"Profiles can be created and deleted by any user\"\non profiles\nfor insert, delete -- cannot create a policy on multiple operators\nto authenticated\nwith check ( true )\nusing ( true );\n```\n\n### Correct\n\n```sql\ncreate policy \"Profiles can be created by any user\"\non profiles\nfor insert\nto authenticated\nwith check ( true );\n\ncreate policy \"Profiles can be deleted by any user\"\non profiles\nfor delete\nto authenticated\nusing ( true );\n```\n\n## Helper functions\n\nSupabase provides some helper functions that make it easier to write Policies.\n\n### `auth.uid()`\n\nReturns the ID of the user making the request.\n\n### `auth.jwt()`\n\nReturns the JWT of the user making the request. Anything that you store in the user's `raw_app_meta_data` column or the `raw_user_meta_data` column will be accessible using this function. It's important to know the distinction between these two:\n\n- `raw_user_meta_data` - can be updated by the authenticated user using the `supabase.auth.update()` function. It is not a good place to store authorization data.\n- `raw_app_meta_data` - cannot be updated by the user, so it's a good place to store authorization data.\n\nThe `auth.jwt()` function is extremely versatile. For example, if you store some team data inside `app_metadata`, you can use it to determine whether a particular user belongs to a team. For example, if this was an array of IDs:\n\n```sql\ncreate policy \"User is in team\"\non my_table\nto authenticated\nusing ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));\n```\n\n### MFA\n\nThe `auth.jwt()` function can be used to check for [Multi-Factor Authentication](mdc:docs/guides/auth/auth-mfa#enforce-rules-for-mfa-logins). For Line truncated
|
||||
"type": "registry:file",
|
||||
"target": "~/.cursor/rules/create-rls-policies.mdc"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/ai-editor-rules/postgres-sql-style-guide.mdc",
|
||||
"content": "---\n# Specify the following for Cursor rules\ndescription: Guidelines for writing Postgres SQL\nglobs: **/*.sql\n---\n\n# Postgres SQL Style Guide\n\n## General\n\n- Use lowercase for SQL reserved words to maintain consistency and readability.\n- Employ consistent, descriptive identifiers for tables, columns, and other database objects.\n- Use white space and indentation to enhance the readability of your code.\n- Store dates in ISO 8601 format (`yyyy-mm-ddThh:mm:ss.sssss`).\n- Include comments for complex logic, using '/_ ... _/' for block comments and '--' for line comments.\n\n## Naming Conventions\n\n- Avoid SQL reserved words and ensure names are unique and under 63 characters.\n- Use snake_case for tables and columns.\n- Prefer plurals for table names\n- Prefer singular names for columns.\n\n## Tables\n\n- Avoid prefixes like 'tbl\\_' and ensure no table name matches any of its column names.\n- Always add an `id` column of type `identity generated always` unless otherwise specified.\n- Create all tables in the `public` schema unless otherwise specified.\n- Always add the schema to SQL queries for clarity.\n- Always add a comment to describe what the table does. The comment can be up to 1024 characters.\n\n## Columns\n\n- Use singular names and avoid generic names like 'id'.\n- For references to foreign tables, use the singular of the table name with the `_id` suffix. For example `user_id` to reference the `users` table\n- Always use lowercase except in cases involving acronyms or when readability would be enhanced by an exception.\n\n#### Examples:\n\n```sql\ncreate table books (\n id bigint generated always as identity primary key,\n title text not null,\n author_id bigint references authors (id)\n);\ncomment on table books is 'A list of all the books in the library.';\n```\n\n## Queries\n\n- When the query is shorter keep it on just a few lines. As it gets larger start adding newlines for readability\n- Add spaces for readability.\n\nSmaller queries:\n\n```sql\nselect *\nfrom employees\nwhere end_date is null;\n\nupdate employees\nset end_date = '2023-12-31'\nwhere employee_id = 1001;\n```\n\nLarger queries:\n\n```sql\nselect\n first_name,\n last_name\nfrom employees\nwhere start_date between '2021-01-01' and '2021-12-31' and status = 'employed';\n```\n\n### Joins and Subqueries\n\n- Format joins and subqueries for clarity, aligning them with related SQL clauses.\n- Prefer full table names when referencing tables. This helps for readability.\n\n```sql\nselect\n employees.employee_name,\n departments.department_name\nfrom\n employees\n join departments on employees.department_id = departments.department_id\nwhere employees.start_date > '2022-01-01';\n```\n\n## Aliases\n\n- Use meaningful aliases that reflect the data or transformation applied, and always include the 'as' keyword for clarity.\n\n```sql\nselect count(*) as total_employees\nfrom employees\nwhere end_date is null;\n```\n\n## Complex queries and CTEs\n\n- If a query is extremely complex, prefer a CTE.\n- Make sure the CTE is clear and linear. Prefer readability over performance.\n- Add comments to each block.\n\n```sql\nwith\n department_employees as (\n -- Get all employees and their departments\n select\n employees.department_id,\n employees.first_name,\n employees.last_name,\n departments.department_name\n from\n employees\n join departments on employees.department_id = departments.department_id\n ),\n employee_counts as (\n -- Count how many employees in each department\n select\n department_name,\n count(*) as num_employees\n from department_employees\n group by department_name\n )\nselect\n department_name,\n num_employees\nfrom employee_counts\norder by department_name;\n```\n",
|
||||
"content": "---\n# Specify the following for Cursor rules\ndescription: Guidelines for writing Postgres SQL\nalwaysApply: false\n---\n\n# Postgres SQL Style Guide\n\n## General\n\n- Use lowercase for SQL reserved words to maintain consistency and readability.\n- Employ consistent, descriptive identifiers for tables, columns, and other database objects.\n- Use white space and indentation to enhance the readability of your code.\n- Store dates in ISO 8601 format (`yyyy-mm-ddThh:mm:ss.sssss`).\n- Include comments for complex logic, using '/_ ... _/' for block comments and '--' for line comments.\n\n## Naming Conventions\n\n- Avoid SQL reserved words and ensure names are unique and under 63 characters.\n- Use snake_case for tables and columns.\n- Prefer plurals for table names\n- Prefer singular names for columns.\n\n## Tables\n\n- Avoid prefixes like 'tbl\\_' and ensure no table name matches any of its column names.\n- Always add an `id` column of type `identity generated always` unless otherwise specified.\n- Create all tables in the `public` schema unless otherwise specified.\n- Always add the schema to SQL queries for clarity.\n- Always add a comment to describe what the table does. The comment can be up to 1024 characters.\n\n## Columns\n\n- Use singular names and avoid generic names like 'id'.\n- For references to foreign tables, use the singular of the table name with the `_id` suffix. For example `user_id` to reference the `users` table\n- Always use lowercase except in cases involving acronyms or when readability would be enhanced by an exception.\n\n#### Examples:\n\n```sql\ncreate table books (\n id bigint generated always as identity primary key,\n title text not null,\n author_id bigint references authors (id)\n);\ncomment on table books is 'A list of all the books in the library.';\n```\n\n## Queries\n\n- When the query is shorter keep it on just a few lines. As it gets larger start adding newlines for readability\n- Add spaces for readability.\n\nSmaller queries:\n\n```sql\nselect *\nfrom employees\nwhere end_date is null;\n\nupdate employees\nset end_date = '2023-12-31'\nwhere employee_id = 1001;\n```\n\nLarger queries:\n\n```sql\nselect\n first_name,\n last_name\nfrom employees\nwhere start_date between '2021-01-01' and '2021-12-31' and status = 'employed';\n```\n\n### Joins and Subqueries\n\n- Format joins and subqueries for clarity, aligning them with related SQL clauses.\n- Prefer full table names when referencing tables. This helps for readability.\n\n```sql\nselect\n employees.employee_name,\n departments.department_name\nfrom\n employees\n join departments on employees.department_id = departments.department_id\nwhere employees.start_date > '2022-01-01';\n```\n\n## Aliases\n\n- Use meaningful aliases that reflect the data or transformation applied, and always include the 'as' keyword for clarity.\n\n```sql\nselect count(*) as total_employees\nfrom employees\nwhere end_date is null;\n```\n\n## Complex queries and CTEs\n\n- If a query is extremely complex, prefer a CTE.\n- Make sure the CTE is clear and linear. Prefer readability over performance.\n- Add comments to each block.\n\n```sql\nwith\n department_employees as (\n -- Get all employees and their departments\n select\n employees.department_id,\n employees.first_name,\n employees.last_name,\n departments.department_name\n from\n employees\n join departments on employees.department_id = departments.department_id\n ),\n employee_counts as (\n -- Count how many employees in each department\n select\n department_name,\n count(*) as num_employees\n from department_employees\n group by department_name\n )\nselect\n department_name,\n num_employees\nfrom employee_counts\norder by department_name;\n```\n",
|
||||
"type": "registry:file",
|
||||
"target": "~/.cursor/rules/postgres-sql-style-guide.mdc"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/ai-editor-rules/writing-supabase-edge-functions.mdc",
|
||||
"content": "---\ndescription: Coding rules for Supabase Edge Functions\nglobs: supabase/functions/**/*.ts\n---\n\n# Writing Supabase Edge Functions\n\nYou're an expert in writing TypeScript and Deno JavaScript runtime. Generate **high-quality Supabase Edge Functions** that adhere to the following best practices:\n\n## Guidelines\n\n1. Try to use Web APIs and Deno’s core APIs instead of external dependencies (eg: use fetch instead of Axios, use WebSockets API instead of node-ws)\n2. If you are reusing utility methods between Edge Functions, add them to `supabase/functions/_shared` and import using a relative path. Do NOT have cross dependencies between Edge Functions.\n3. Do NOT use bare specifiers when importing dependecnies. If you need to use an external dependency, make sure it's prefixed with either `npm:` or `jsr:`. For example, `@supabase/supabase-js` should be written as `npm:@supabase/supabase-js`.\n4. For external imports, always define a version. For example, `npm:@express` should be written as `npm:express@4.18.2`.\n5. For external dependencies, importing via `npm:` and `jsr:` is preferred. Minimize the use of imports from @`deno.land/x` , `esm.sh` and @`unpkg.com` . If you have a package from one of those CDNs, you can replace the CDN hostname with `npm:` specifier.\n6. You can also use Node built-in APIs. You will need to import them using `node:` specifier. For example, to import Node process: `import process from \"node:process\". Use Node APIs when you find gaps in Deno APIs.\n7. Do NOT use `import { serve } from \"https://deno.land/std@0.168.0/http/server.ts\"`. Instead use the built-in `Deno.serve`.\n8. Following environment variables (ie. secrets) are pre-populated in both local and hosted Supabase environments. Users don't need to manually set them:\n - SUPABASE_URL\n - SUPABASE_ANON_KEY\n - SUPABASE_SERVICE_ROLE_KEY\n - SUPABASE_DB_URL\n9. To set other environment variables (ie. secrets) users can put them in a env file and run the `supabase secrets set --env-file path/to/env-file`\n10. A single Edge Function can handle multiple routes. It is recommended to use a library like Express or Hono to handle the routes as it's easier for developer to understand and maintain. Each route must be prefixed with `/function-name` so they are routed correctly.\n11. File write operations are ONLY permitted on `/tmp` directory. You can use either Deno or Node File APIs.\n12. Use `EdgeRuntime.waitUntil(promise)` static method to run long-running tasks in the background without blocking response to a request. Do NOT assume it is available in the request / execution context.\n\n## Example Templates\n\n### Simple Hello World Function\n\n```tsx\ninterface reqPayload {\n name: string\n}\n\nconsole.info('server started')\n\nDeno.serve(async (req: Request) => {\n const { name }: reqPayload = await req.json()\n const data = {\n message: `Hello ${name} from foo!`,\n }\n\n return new Response(JSON.stringify(data), {\n headers: { 'Content-Type': 'application/json', Connection: 'keep-alive' },\n })\n})\n```\n\n### Example Function using Node built-in API\n\n```tsx\nimport { randomBytes } from 'node:crypto'\nimport { createServer } from 'node:http'\nimport process from 'node:process'\n\nconst generateRandomString = (length) => {\n const buffer = randomBytes(length)\n return buffer.toString('hex')\n}\n\nconst randomString = generateRandomString(10)\nconsole.log(randomString)\n\nconst server = createServer((req, res) => {\n const message = `Hello`\n res.end(message)\n})\n\nserver.listen(9999)\n```\n\n### Using npm packages in Functions\n\n```tsx\nimport express from 'npm:express@4.18.2'\n\nconst app = express()\n\napp.get(/(.*)/, (req, res) => {\n res.send('Welcome to Supabase')\n})\n\napp.listen(8000)\n```\n\n### Generate embeddings using built-in @Supabase.ai API\n\n```tsx\nconst model = new Supabase.ai.Session('gte-small')\n\nDeno.serve(async (req: Request) => {\n const params = new URL(req.url).searchParams\n const input = params.get('text')\n const output = await model.run(input, { mean_pool: true, normalize: true })\n return new Response(JSON.stringify(output), {\n headers: {\n 'Content-Type': 'application/json',\n Connection: 'keep-alive',\n },\n })\n})\n```\n",
|
||||
"content": "---\ndescription: Coding rules for Supabase Edge Functions\nalwaysApply: false\n---\n\n# Writing Supabase Edge Functions\n\nYou're an expert in writing TypeScript and Deno JavaScript runtime. Generate **high-quality Supabase Edge Functions** that adhere to the following best practices:\n\n## Guidelines\n\n1. Try to use Web APIs and Deno’s core APIs instead of external dependencies (eg: use fetch instead of Axios, use WebSockets API instead of node-ws)\n2. If you are reusing utility methods between Edge Functions, add them to `supabase/functions/_shared` and import using a relative path. Do NOT have cross dependencies between Edge Functions.\n3. Do NOT use bare specifiers when importing dependecnies. If you need to use an external dependency, make sure it's prefixed with either `npm:` or `jsr:`. For example, `@supabase/supabase-js` should be written as `npm:@supabase/supabase-js`.\n4. For external imports, always define a version. For example, `npm:@express` should be written as `npm:express@4.18.2`.\n5. For external dependencies, importing via `npm:` and `jsr:` is preferred. Minimize the use of imports from @`deno.land/x` , `esm.sh` and @`unpkg.com` . If you have a package from one of those CDNs, you can replace the CDN hostname with `npm:` specifier.\n6. You can also use Node built-in APIs. You will need to import them using `node:` specifier. For example, to import Node process: `import process from \"node:process\". Use Node APIs when you find gaps in Deno APIs.\n7. Do NOT use `import { serve } from \"https://deno.land/std@0.168.0/http/server.ts\"`. Instead use the built-in `Deno.serve`.\n8. Following environment variables (ie. secrets) are pre-populated in both local and hosted Supabase environments. Users don't need to manually set them:\n - SUPABASE_URL\n - SUPABASE_ANON_KEY\n - SUPABASE_SERVICE_ROLE_KEY\n - SUPABASE_DB_URL\n9. To set other environment variables (ie. secrets) users can put them in a env file and run the `supabase secrets set --env-file path/to/env-file`\n10. A single Edge Function can handle multiple routes. It is recommended to use a library like Express or Hono to handle the routes as it's easier for developer to understand and maintain. Each route must be prefixed with `/function-name` so they are routed correctly.\n11. File write operations are ONLY permitted on `/tmp` directory. You can use either Deno or Node File APIs.\n12. Use `EdgeRuntime.waitUntil(promise)` static method to run long-running tasks in the background without blocking response to a request. Do NOT assume it is available in the request / execution context.\n\n## Example Templates\n\n### Simple Hello World Function\n\n```tsx\ninterface reqPayload {\n name: string\n}\n\nconsole.info('server started')\n\nDeno.serve(async (req: Request) => {\n const { name }: reqPayload = await req.json()\n const data = {\n message: `Hello ${name} from foo!`,\n }\n\n return new Response(JSON.stringify(data), {\n headers: { 'Content-Type': 'application/json', Connection: 'keep-alive' },\n })\n})\n```\n\n### Example Function using Node built-in API\n\n```tsx\nimport { randomBytes } from 'node:crypto'\nimport { createServer } from 'node:http'\nimport process from 'node:process'\n\nconst generateRandomString = (length) => {\n const buffer = randomBytes(length)\n return buffer.toString('hex')\n}\n\nconst randomString = generateRandomString(10)\nconsole.log(randomString)\n\nconst server = createServer((req, res) => {\n const message = `Hello`\n res.end(message)\n})\n\nserver.listen(9999)\n```\n\n### Using npm packages in Functions\n\n```tsx\nimport express from 'npm:express@4.18.2'\n\nconst app = express()\n\napp.get(/(.*)/, (req, res) => {\n res.send('Welcome to Supabase')\n})\n\napp.listen(8000)\n```\n\n### Generate embeddings using built-in @Supabase.ai API\n\n```tsx\nconst model = new Supabase.ai.Session('gte-small')\n\nDeno.serve(async (req: Request) => {\n const params = new URL(req.url).searchParams\n const input = params.get('text')\n const output = await model.run(input, { mean_pool: true, normalize: true })\n return new Response(JSON.stringify(output), {\n headers: {\n 'Content-Type': 'application/json',\n Connection: 'keep-alive',\n },\n })\n})\n```\n",
|
||||
"type": "registry:file",
|
||||
"target": "~/.cursor/rules/writing-supabase-edge-functions.mdc"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
})
|
||||
File renamed without changes.
@@ -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