Add route for generating shadcn auth block. Add a page for generated block.

This commit is contained in:
Ivan Vasilov committed 2025-09-05 14:44:02 +02:00
1 parent 9d02e288b0
commit dfcbdd6027
27 files changed
+2470 -204

No files matched your search

View File
File renamed without changes.
+1 -1
View File
@@ -44,7 +44,7 @@ const SignInPage: NextPageWithLayout = () => {
<div className="w-full border-t border-strong" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 text-sm bg-studio text-foreground">or</span>
<span className="px-2 text-sm bg-background text-foreground">or</span>
</div>
</div>
<SignInForm />
@@ -0,0 +1,126 @@
import fs from 'fs'
import { template } from 'lodash'
import path from 'path'
import * as prettier from 'prettier'
import { type RegistryItem } from 'shadcn/schema'
const SOCIAL_PROVIDERS = [
{ label: 'Password', value: 'password' },
{ label: 'Google', value: 'google' },
{ label: 'Facebook', value: 'facebook' },
{ label: 'Apple', value: 'apple' },
{ label: 'Azure (Microsoft)', value: 'azure' },
{ label: 'Twitter', value: 'twitter' },
{ label: 'GitHub', value: 'github' },
{ label: 'Gitlab', value: 'gitlab' },
{ label: 'Bitbucket', value: 'bitbucket' },
{ label: 'Discord', value: 'discord' },
{ label: 'Figma', value: 'figma' },
{ label: 'Kakao', value: 'kakao' },
{ label: 'Keycloak', value: 'keycloak' },
{ label: 'LinkedIn', value: 'linkedin' },
{ label: 'Notion', value: 'notion' },
{ label: 'Slack', value: 'slack' },
{ label: 'Spotify', value: 'spotify' },
{ label: 'Twitch', value: 'twitch' },
{ label: 'WorkOS', value: 'workos' },
{ label: 'Zoom', value: 'zoom' },
]
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) {
try {
const slug = (await params).slug.replace('.json', '')
const parts = slug.split('-')
let [framework, ...authTypes] = parts
// Build block names
const passwordBlockName = `password-based-auth-${framework}`
const socialBlockName = `social-auth-${framework}`
const blocksDir = path.join(process.cwd(), 'public', 'r')
const blocks: RegistryItem[] = []
const imports: string[] = []
const components: string[] = []
const commonBlockPath = path.join(blocksDir, `common-auth-nextjs.json`)
const commonBlock: RegistryItem = JSON.parse(fs.readFileSync(commonBlockPath, 'utf-8'))
blocks.push(commonBlock)
if (authTypes.includes('password')) {
// Read the base auth block
const baseBlockPath = path.join(blocksDir, `${passwordBlockName}.json`)
if (!fs.existsSync(baseBlockPath)) {
return Response.json(
{ error: `Base block not found: ${passwordBlockName}` },
{ status: 404 }
)
}
const baseBlock: RegistryItem = JSON.parse(fs.readFileSync(baseBlockPath, 'utf-8'))
blocks.push(baseBlock)
imports.push(
"import { PasswordLoginForm } from '@/registry/default/blocks/password-based-auth-nextjs/components/password-login-form'"
)
components.push('<PasswordLoginForm />')
if (authTypes.length > 1) {
components.push(
`<div className="relative"><div className="absolute inset-0 flex items-center"><div className="w-full border-t border" /></div><div className="relative flex justify-center text-sm"><span className="px-2 text-sm bg-background text-foreground">or</span></div></div>`
)
}
}
for (let authType of authTypes) {
if (authType === 'password') {
// Already handled above
continue
}
const provider = SOCIAL_PROVIDERS.find((p) => p.value === authType)
if (!provider) {
return Response.json({ error: `Unknown auth type: ${authType}` }, { status: 400 })
}
const socialBlockPath = path.join(blocksDir, `${socialBlockName}.json`)
const socialBlock: RegistryItem = JSON.parse(fs.readFileSync(socialBlockPath, 'utf-8'))
blocks.push(socialBlock)
if (imports.find((imp) => imp.includes('SocialLoginButton')) === undefined) {
imports.push(
"import { SocialLoginButton } from '@/registry/default/blocks/social-auth-nextjs/components/social-login-button'"
)
}
components.push(
`<SocialLoginButton label="${provider.label}" provider="${provider.value}" />`
)
}
// Create combined block
const combinedBlock: RegistryItem = {
name: slug,
type: 'registry:block',
title: `Combined Auth with ${authTypes.join(', ')} for ${framework}`,
description: `Combined authentication flow with password-based auth and ${authTypes.join(', ')} social providers for ${framework}`,
registryDependencies: Array.from(
new Set(blocks.flatMap((b) => b.registryDependencies || []))
),
dependencies: Array.from(new Set(blocks.flatMap((b) => b.dependencies || []))),
files: blocks.flatMap((b) => b.files || []),
}
const loginForm = combinedBlock.files?.find((f) => f.path.endsWith('app/auth/login/page.tsx'))
if (loginForm) {
const compiled = template(loginForm.content, { interpolate: /{?\/\*<%=(.+?)%> \*\/}?/g })
const templated = compiled({
imports: imports.join('\n'),
components: components.join(''),
})
const content = await prettier.format(templated, { parser: 'typescript' } as prettier.Options)
loginForm.content = content
}
return Response.json(combinedBlock, { status: 200 })
} catch (error) {
console.error('Error processing block request:', error)
return Response.json({ error: 'Internal server error' }, { status: 500 })
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ export function Command({ name, highlight }: CommandCopyProps) {
}
const baseUrl = getBaseUrl()
const componentPath = `${process.env.NEXT_PUBLIC_BASE_PATH ?? ''}/r/${name}.json`
const componentPath = `${process.env.NEXT_PUBLIC_BASE_PATH ?? ''}/api/blocks/${name}.json`
const commands: Record<PackageManager, string> = {
npm: `npx shadcn@latest add ${baseUrl}${componentPath}`,
@@ -0,0 +1,74 @@
'use client'
import { OpenInV0Button } from '@/components/open-in-v0-button'
import { type Provider } from '@supabase/supabase-js'
import dynamic from 'next/dynamic'
import { useState } from 'react'
import { Label_Shadcn_, Switch } from 'ui'
//The dynamic import is to prevent the command component from being rendered on the server and cause hydration errors
const Command = dynamic(() => import('./command').then((mod) => mod.Command), { ssr: false })
interface BlockItemProps {
name: string
}
const PROVIDERS: { label: string; value: Provider | 'password' }[] = [
{ label: 'Password', value: 'password' },
{ label: 'Google', value: 'google' },
{ label: 'Facebook', value: 'facebook' },
{ label: 'Apple', value: 'apple' },
{ label: 'Azure (Microsoft)', value: 'azure' },
{ label: 'Twitter', value: 'twitter' },
{ label: 'GitHub', value: 'github' },
{ label: 'Gitlab', value: 'gitlab' },
{ label: 'Bitbucket', value: 'bitbucket' },
{ label: 'Discord', value: 'discord' },
{ label: 'Figma', value: 'figma' },
{ label: 'Kakao', value: 'kakao' },
{ label: 'Keycloak', value: 'keycloak' },
{ label: 'LinkedIn', value: 'linkedin' },
{ label: 'Notion', value: 'notion' },
{ label: 'Slack', value: 'slack' },
{ label: 'Spotify', value: 'spotify' },
{ label: 'Twitch', value: 'twitch' },
{ label: 'WorkOS', value: 'workos' },
{ label: 'Zoom', value: 'zoom' },
]
export const GeneratedAuthBlockItem = ({
name = 'social-auth-nextjs',
}: {
name?: string
description?: string
}) => {
const [selected, setSelected] = useState<string[]>([])
const handleChange = (provider: string) => {
setSelected((prev: string[]) =>
prev.includes(provider) ? prev.filter((p) => p !== provider) : [...prev, provider]
)
}
const url = `nextjs-${selected.join('-')}`
return (
<div className="mt-4">
<div className="flex flex-col gap-4 py-4">
{PROVIDERS.map((provider) => (
<Label_Shadcn_ key={provider.value} className="flex items-center">
<Switch
checked={selected.includes(provider.value)}
onCheckedChange={() => handleChange(provider.value)}
className="mr-2"
/>
{provider.label}
</Label_Shadcn_>
))}
</div>
<Command name={url} highlight />
<OpenInV0Button name={name} className="w-fit shrink-0 mt-4" />
</div>
)
}
@@ -15,6 +15,7 @@ import { Callout } from './callout'
import { ComponentPreview } from './component-preview'
import { CopyButton } from './copy-button'
import { DualRealtimeChat } from './dual-realtime-chat'
import { GeneratedAuthBlockItem } from './generated-auth-block-item'
import { RegistryBlock } from './registry-block'
import { StyleWrapper } from './style-wrapper'
import TanStackBeta from './tanstack-beta'
@@ -169,6 +170,7 @@ const components = {
/>
),
BlockItem,
GeneratedAuthBlockItem,
BlockPreview,
DualRealtimeChat,
}
@@ -0,0 +1,44 @@
---
title: Generated Authentication
description: Generated authentication block for Next.js
---
<Callout className="mt-4">
Select which providers to enable below. The generated URL will call the API route for block
generation.
</Callout>
## Installation
Select which social providers you want to be added to the block.
<GeneratedAuthBlockItem
name="generated-auth-nextjs"
description="All needed components for the generated 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.
## Usage
Once you install the block in your Next.js project, you'll get all the necessary pages and components to set up authentication flow with the specified providers.
### Getting started
After installing the block, you'll have the following environment variables in your `.env.local` file:
```env
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_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).
## Further reading
- [Social login](https://supabase.com/docs/guides/auth/social-login)
- [Authentication error codes](https://supabase.com/docs/guides/auth/debugging/error-codes)
@@ -0,0 +1,68 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "common-auth-nextjs",
"type": "registry:block",
"title": "Common Auth flow for Nextjs and Supabase",
"description": "Common Auth flow for Nextjs and Supabase",
"dependencies": [
"@supabase/ssr@latest",
"@supabase/supabase-js@latest"
],
"registryDependencies": [
"button",
"card",
"input",
"label"
],
"files": [
{
"path": "registry/default/blocks/common-auth-nextjs/app/auth/login/page.tsx",
"content": "import {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\n/*<%= imports %> */\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 <div className=\"flex flex-col gap-6\">\n <Card>\n <CardHeader>\n <CardTitle className=\"text-2xl\">Login</CardTitle>\n <CardDescription>Sign in to your account to continue</CardDescription>\n </CardHeader>\n <CardContent className=\"flex flex-col gap-4\">{/*<%= components %> */}</CardContent>\n </Card>\n </div>\n </div>\n </div>\n )\n}\n",
"type": "registry:page",
"target": "app/auth/login/page.tsx"
},
{
"path": "registry/default/blocks/common-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/common-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.getClaims()\n if (error || !data?.claims) {\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.claims.email}</span>\n </p>\n <LogoutButton />\n </div>\n )\n}\n",
"type": "registry:page",
"target": "app/protected/page.tsx"
},
{
"path": "registry/default/blocks/common-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/common-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_PUBLISHABLE_OR_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 // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_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.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\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\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\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_PUBLISHABLE_OR_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"
}
],
"envVars": {
"NEXT_PUBLIC_SUPABASE_URL": "",
"NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": ""
},
"docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`."
}
@@ -15,12 +15,6 @@
"label"
],
"files": [
{
"path": "registry/default/blocks/password-based-auth-nextjs/app/auth/login/page.tsx",
"content": "import { LoginForm } from '@/registry/default/blocks/password-based-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/password-based-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",
@@ -39,14 +33,9 @@
"type": "registry:page",
"target": "app/auth/confirm/route.ts"
},
{
"path": "registry/default/blocks/password-based-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 { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\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 router = useRouter()\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 router.push('/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 href=\"/auth/forgot-password\"\n className=\"ml-auto inline-block text-sm underline-offset-4 hover:underline\"\n >\n Forgot your password?\n </Link>\n </div>\n <Input\n id=\"password\"\n type=\"password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n />\n </div>\n {error && <p className=\"text-sm text-red-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Logging in...' : 'Login'}\n </Button>\n </div>\n <div className=\"mt-4 text-center text-sm\">\n Don&apos;t have an account?{' '}\n <Link href=\"/auth/sign-up\" className=\"underline underline-offset-4\">\n Sign up\n </Link>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-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:\n * - _next/static (static files)\n * - _next/image (image optimization files)\n * - favicon.ico (favicon file)\n * - images - .svg, .png, .jpg, .jpeg, .gif, .webp\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",
"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"
},
@@ -94,6 +83,11 @@
"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/blocks/password-based-auth-nextjs/components/password-login-form.tsx",
"content": "'use client'\n\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { useState } from 'react'\n\nexport function PasswordLoginForm() {\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 router = useRouter()\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 router.push('/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 <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 href=\"/auth/forgot-password\"\n className=\"ml-auto inline-block text-sm underline-offset-4 hover:underline\"\n >\n Forgot your password?\n </Link>\n </div>\n <Input\n id=\"password\"\n type=\"password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n />\n </div>\n {error && <p className=\"text-sm text-red-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Logging in...' : 'Login'}\n </Button>\n </div>\n <div className=\"mt-4 text-center text-sm\">\n Don&apos;t have an account?{' '}\n <Link href=\"/auth/sign-up\" className=\"underline underline-offset-4\">\n Sign up\n </Link>\n </div>\n </form>\n )\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_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n",
File diff suppressed because it is too large. Load diff
@@ -13,12 +13,6 @@
"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",
@@ -37,11 +31,6 @@
"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",
@@ -53,6 +42,11 @@
"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/blocks/social-auth-nextjs/components/social-login-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 { type Provider } from '@supabase/supabase-js'\nimport { useState } from 'react'\n\nexport function SocialLoginButton({ label, provider }: { label: string; provider: Provider }) {\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,\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 <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 ${label}`}\n </Button>\n </div>\n </form>\n )\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_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n",
+59 -18
View File
@@ -3,6 +3,57 @@
"name": "Supabase UI Library",
"homepage": "https://supabase.com/ui",
"items": [
{
"name": "common-auth-nextjs",
"type": "registry:block",
"title": "Common Auth flow for Nextjs and Supabase",
"description": "Common Auth flow for Nextjs and Supabase",
"registryDependencies": ["button", "card", "input", "label"],
"dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"],
"files": [
{
"path": "registry/default/blocks/common-auth-nextjs/app/auth/login/page.tsx",
"type": "registry:page",
"target": "app/auth/login/page.tsx"
},
{
"path": "registry/default/blocks/common-auth-nextjs/app/auth/error/page.tsx",
"type": "registry:page",
"target": "app/auth/error/page.tsx"
},
{
"path": "registry/default/blocks/common-auth-nextjs/app/protected/page.tsx",
"type": "registry:page",
"target": "app/protected/page.tsx"
},
{
"path": "registry/default/blocks/common-auth-nextjs/middleware.ts",
"type": "registry:file",
"target": "middleware.ts"
},
{
"path": "registry/default/blocks/common-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"
}
],
"docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.",
"envVars": {
"NEXT_PUBLIC_SUPABASE_URL": "",
"NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": ""
}
},
{
"name": "password-based-auth-nextjs",
"type": "registry:block",
@@ -11,11 +62,6 @@
"registryDependencies": ["button", "card", "input", "label"],
"dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"],
"files": [
{
"path": "registry/default/blocks/password-based-auth-nextjs/app/auth/login/page.tsx",
"type": "registry:page",
"target": "app/auth/login/page.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/app/auth/error/page.tsx",
"type": "registry:page",
@@ -31,10 +77,6 @@
"type": "registry:page",
"target": "app/auth/confirm/route.ts"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/components/login-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/middleware.ts",
"type": "registry:file",
@@ -76,6 +118,10 @@
"path": "registry/default/blocks/password-based-auth-nextjs/components/logout-button.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/components/password-login-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/client.ts",
"type": "registry:lib"
@@ -299,11 +345,6 @@
"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",
@@ -319,10 +360,6 @@
"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",
@@ -332,6 +369,10 @@
"path": "registry/default/blocks/social-auth-nextjs/components/logout-button.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/social-auth-nextjs/components/social-login-button.tsx",
"type": "registry:component"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/client.ts",
"type": "registry:lib"
+5 -2
View File
@@ -9,6 +9,7 @@ import realtimeAvatarStack from './default/blocks/realtime-avatar-stack/registry
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 commonAuthNextjs from './default/blocks/common-auth-nextjs/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' }
@@ -19,7 +20,7 @@ import socialAuthReactRouter from './default/blocks/social-auth-react-router/reg
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' }
const combine = (component: Registry['items'][number]) => {
const combine = (component: RegistryItem) => {
return clients.flatMap((client) => {
return registryItemAppend(
{
@@ -37,6 +38,8 @@ const tanstackClient = clients.find((client) => client.name === 'supabase-client
const reactRouterClient = clients.find((client) => client.name === 'supabase-client-react-router')
export const blocks = [
registryItemAppend(commonAuthNextjs as RegistryItem, [nextjsClient!]),
registryItemAppend(passwordBasedAuthNextjs as RegistryItem, [nextjsClient!]),
registryItemAppend(passwordBasedAuthReact as RegistryItem, [reactClient!]),
registryItemAppend(passwordBasedAuthReactRouter as RegistryItem, [reactRouterClient!]),
@@ -54,4 +57,4 @@ export const blocks = [
...combine(realtimeChat as RegistryItem),
// infinite query hook is intentionally not combined with the clients since it depends on clients having database types.
infiniteQueryHook as RegistryItem,
] as Registry['items']
] as RegistryItem[]
@@ -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,26 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
/*<%= imports %> */
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">
<div className="flex flex-col gap-6">
<Card>
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>Sign in to your account to continue</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">{/*<%= components %> */}</CardContent>
</Card>
</div>
</div>
</div>
)
}
@@ -0,0 +1,22 @@
import { redirect } from 'next/navigation'
import { LogoutButton } from '@/registry/default/blocks/password-based-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.getClaims()
if (error || !data?.claims) {
redirect('/auth/login')
}
return (
<div className="flex h-svh w-full items-center justify-center gap-2">
<p>
Hello <span>{data.claims.email}</span>
</p>
<LogoutButton />
</div>
)
}
@@ -0,0 +1,17 @@
'use client'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import { useRouter } from 'next/navigation'
export function LogoutButton() {
const router = useRouter()
const logout = async () => {
const supabase = createClient()
await supabase.auth.signOut()
router.push('/example/password-based-auth/auth/login')
}
return <Button onClick={logout}>Logout</Button>
}
@@ -0,0 +1,19 @@
import { updateSession } from '@/registry/default/clients/nextjs/lib/supabase/middleware'
import { type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
@@ -0,0 +1,34 @@
{
"name": "common-auth-nextjs",
"type": "registry:block",
"title": "Common Auth flow for Nextjs and Supabase",
"description": "Common Auth flow for Nextjs and Supabase",
"registryDependencies": ["button", "card", "input", "label"],
"dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"],
"files": [
{
"path": "registry/default/blocks/common-auth-nextjs/app/auth/login/page.tsx",
"type": "registry:page",
"target": "app/auth/login/page.tsx"
},
{
"path": "registry/default/blocks/common-auth-nextjs/app/auth/error/page.tsx",
"type": "registry:page",
"target": "app/auth/error/page.tsx"
},
{
"path": "registry/default/blocks/common-auth-nextjs/app/protected/page.tsx",
"type": "registry:page",
"target": "app/protected/page.tsx"
},
{
"path": "registry/default/blocks/common-auth-nextjs/middleware.ts",
"type": "registry:file",
"target": "middleware.ts"
},
{
"path": "registry/default/blocks/common-auth-nextjs/components/logout-button.tsx",
"type": "registry:component"
}
]
}
@@ -1,10 +1,10 @@
import { LoginForm } from '@/registry/default/blocks/password-based-auth-nextjs/components/login-form'
import { PasswordLoginForm } from '@/registry/default/blocks/password-based-auth-nextjs/components/password-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 />
<PasswordLoginForm />
</div>
</div>
)
@@ -1,102 +0,0 @@
'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 { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import { Link } from '@/registry/default/components/ui/link'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
const supabase = createClient()
setIsLoading(true)
setError(null)
try {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) throw error
// Update this route to redirect to an authenticated route. The user already has an active session.
router.push('/example/password-based-auth/protected')
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>Enter your email below to login to your account</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleLogin}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<Link
href="/auth/forgot-password"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</Link>
</div>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{' '}
<Link href="/auth/sign-up" className="underline underline-offset-4">
Sign up
</Link>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,84 @@
'use client'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import { Link } from '@/registry/default/components/ui/link'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export function PasswordLoginForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
const supabase = createClient()
setIsLoading(true)
setError(null)
try {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) throw error
// Update this route to redirect to an authenticated route. The user already has an active session.
router.push('/example/password-based-auth/protected')
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<form onSubmit={handleLogin}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<Link
href="/auth/forgot-password"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</Link>
</div>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{' '}
<Link href="/auth/sign-up" className="underline underline-offset-4">
Sign up
</Link>
</div>
</form>
)
}
@@ -8,11 +8,10 @@ export async function middleware(request: NextRequest) {
export const config = {
matcher: [
/*
* Match all request paths except:
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - images - .svg, .png, .jpg, .jpeg, .gif, .webp
* Feel free to modify this pattern to include more paths.
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
@@ -6,11 +6,6 @@
"registryDependencies": ["button", "card", "input", "label"],
"dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"],
"files": [
{
"path": "registry/default/blocks/password-based-auth-nextjs/app/auth/login/page.tsx",
"type": "registry:page",
"target": "app/auth/login/page.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/app/auth/error/page.tsx",
"type": "registry:page",
@@ -26,10 +21,6 @@
"type": "registry:page",
"target": "app/auth/confirm/route.ts"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/components/login-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/middleware.ts",
"type": "registry:file",
@@ -70,6 +61,10 @@
{
"path": "registry/default/blocks/password-based-auth-nextjs/components/logout-button.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/components/password-login-form.tsx",
"type": "registry:component"
}
]
}
@@ -1,8 +1,6 @@
'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,
@@ -10,33 +8,9 @@ import {
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { useState } from 'react'
import { SocialLoginButton } from './social-login-button'
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>
@@ -45,14 +19,7 @@ export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRe
<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>
<SocialLoginButton label="GitHub" provider="github" />
</CardContent>
</Card>
</div>
@@ -0,0 +1,43 @@
'use client'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import { type Provider } from '@supabase/supabase-js'
import { useState } from 'react'
export function SocialLoginButton({ label, provider }: { label: string; provider: Provider }) {
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,
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 (
<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 ${label}`}
</Button>
</div>
</form>
)
}
@@ -6,11 +6,6 @@
"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",
@@ -26,10 +21,6 @@
"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",
@@ -38,6 +29,10 @@
{
"path": "registry/default/blocks/social-auth-nextjs/components/logout-button.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/social-auth-nextjs/components/social-login-button.tsx",
"type": "registry:component"
}
]
}