-
Notifications
You must be signed in to change notification settings - Fork 44.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(builder): Add Supabase Auth, session and sign in UI #7655
Merged
kcze
merged 9 commits into
master
from
kpczerwinski/open-1562-update-builder-to-support-auth-ui
Aug 2, 2024
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
04573e2
Password auth
kcze 36d00dd
Google auth
kcze fd5c8e8
Update
kcze 0823bf2
Add SupabaseProvider
kcze 076516f
Add GitHub and Discord auth
kcze 0172dbf
Update profile dropdown
kcze f8a9aa8
Use `AUTH_CALLBACK_URL` env var
kcze f202ab4
Merge branch 'master' into kpczerwinski/open-1562-update-builder-to-s…
kcze ea14c68
Update style
kcze File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,12 @@ | ||
AGPT_SERVER_URL=http://localhost:8000/api | ||
|
||
## Supabase credentials | ||
## YOU ONLY NEED THEM IF YOU WANT TO USE SUPABASE USER AUTHENTICATION | ||
## If you're using self-hosted version then you most likely don't need to set this | ||
# NEXT_PUBLIC_SUPABASE_URL=your-project-url | ||
# NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key | ||
|
||
## OAuth Callback URL | ||
## This should be {domain}/auth/callback | ||
## Only used if you're using Supabase and OAuth | ||
AUTH_CALLBACK_URL=http://localhost:3000/auth/callback |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
"use client"; | ||
|
||
import { useEffect, useState } from 'react'; | ||
|
||
export default function AuthErrorPage() { | ||
const [errorType, setErrorType] = useState<string | null>(null); | ||
const [errorCode, setErrorCode] = useState<string | null>(null); | ||
const [errorDescription, setErrorDescription] = useState<string | null>(null); | ||
|
||
useEffect(() => { | ||
// This code only runs on the client side | ||
if (typeof window !== 'undefined') { | ||
const hash = window.location.hash.substring(1); // Remove the leading '#' | ||
const params = new URLSearchParams(hash); | ||
|
||
setErrorType(params.get('error')); | ||
setErrorCode(params.get('error_code')); | ||
setErrorDescription(params.get('error_description')?.replace(/\+/g, ' ') ?? null); // Replace '+' with space | ||
} | ||
}, []); | ||
|
||
if (!errorType && !errorCode && !errorDescription) { | ||
return <div>Loading...</div>; | ||
} | ||
|
||
return ( | ||
<div> | ||
<h1>Authentication Error</h1> | ||
{errorType && <p>Error Type: {errorType}</p>} | ||
{errorCode && <p>Error Code: {errorCode}</p>} | ||
{errorDescription && <p>Error Description: {errorDescription}</p>} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { NextResponse } from 'next/server' | ||
import { createServerClient } from '@/lib/supabase/server' | ||
|
||
// Handle the callback to complete the user session login | ||
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') ?? '/profile' | ||
|
||
if (code) { | ||
const supabase = createServerClient() | ||
|
||
if (!supabase) { | ||
return NextResponse.redirect(`${origin}/error`) | ||
} | ||
|
||
const { data, error } = await supabase.auth.exchangeCodeForSession(code) | ||
// data.session?.refresh_token is available if you need to store it for later use | ||
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/auth-code-error`) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { type EmailOtpType } from '@supabase/supabase-js' | ||
import { type NextRequest } from 'next/server' | ||
|
||
import { redirect } from 'next/navigation' | ||
import { createServerClient } from '@/lib/supabase/server' | ||
|
||
// Email confirmation route | ||
export async function GET(request: NextRequest) { | ||
const { searchParams } = new URL(request.url) | ||
const token_hash = searchParams.get('token_hash') | ||
const type = searchParams.get('type') as EmailOtpType | null | ||
const next = searchParams.get('next') ?? '/' | ||
|
||
if (token_hash && type) { | ||
const supabase = createServerClient() | ||
|
||
if (!supabase) { | ||
redirect('/error') | ||
} | ||
|
||
const { error } = await supabase.auth.verifyOtp({ | ||
type, | ||
token_hash, | ||
}) | ||
if (!error) { | ||
// redirect user to specified redirect URL or root of app | ||
redirect(next) | ||
} | ||
} | ||
|
||
// redirect the user to an error page with some instructions | ||
redirect('/error') | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function ErrorPage() { | ||
return <p>Sorry, something went wrong</p> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
'use server' | ||
import { revalidatePath } from 'next/cache' | ||
import { redirect } from 'next/navigation' | ||
import { createServerClient } from '@/lib/supabase/server' | ||
import { z } from 'zod' | ||
|
||
const loginFormSchema = z.object({ | ||
email: z.string().email().min(2).max(64), | ||
password: z.string().min(6).max(64), | ||
}) | ||
|
||
export async function login(values: z.infer<typeof loginFormSchema>) { | ||
const supabase = createServerClient() | ||
|
||
if (!supabase) { | ||
redirect('/error') | ||
} | ||
|
||
// We are sure that the values are of the correct type because zod validates the form | ||
const { data, error } = await supabase.auth.signInWithPassword(values) | ||
|
||
if (error) { | ||
return error.message | ||
} | ||
|
||
if (data.session) { | ||
await supabase.auth.setSession(data.session); | ||
} | ||
|
||
revalidatePath('/', 'layout') | ||
redirect('/profile') | ||
} | ||
|
||
export async function signup(values: z.infer<typeof loginFormSchema>) { | ||
const supabase = createServerClient() | ||
|
||
if (!supabase) { | ||
redirect('/error') | ||
} | ||
|
||
// We are sure that the values are of the correct type because zod validates the form | ||
const { data, error } = await supabase.auth.signUp(values) | ||
|
||
if (error) { | ||
return error.message | ||
} | ||
|
||
if (data.session) { | ||
await supabase.auth.setSession(data.session); | ||
} | ||
|
||
revalidatePath('/', 'layout') | ||
redirect('/profile') | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
"use client"; | ||
import useUser from '@/hooks/useUser'; | ||
import { login, signup } from './actions' | ||
import { Button } from '@/components/ui/button'; | ||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; | ||
import { useForm } from 'react-hook-form'; | ||
import { Input } from '@/components/ui/input'; | ||
import { z } from "zod" | ||
import { zodResolver } from "@hookform/resolvers/zod" | ||
import { PasswordInput } from '@/components/PasswordInput'; | ||
import { FaGoogle, FaGithub, FaDiscord, FaSpinner } from "react-icons/fa"; | ||
import { useState } from 'react'; | ||
import { useSupabase } from '@/components/SupabaseProvider'; | ||
import { useRouter } from 'next/navigation'; | ||
|
||
const loginFormSchema = z.object({ | ||
email: z.string().email().min(2).max(64), | ||
password: z.string().min(6).max(64), | ||
}) | ||
|
||
export default function LoginPage() { | ||
const { supabase, isLoading: isSupabaseLoading } = useSupabase(); | ||
const { user, isLoading: isUserLoading } = useUser(); | ||
const [feedback, setFeedback] = useState<string | null>(null); | ||
const router = useRouter(); | ||
const [isLoading, setIsLoading] = useState(false); | ||
|
||
const form = useForm<z.infer<typeof loginFormSchema>>({ | ||
resolver: zodResolver(loginFormSchema), | ||
defaultValues: { | ||
email: "", | ||
password: "", | ||
}, | ||
}) | ||
|
||
if (user) { | ||
console.log('User exists, redirecting to profile') | ||
router.push('/profile') | ||
} | ||
|
||
if (isUserLoading || isSupabaseLoading || user) { | ||
return ( | ||
<div className="flex justify-center items-center h-[80vh]"> | ||
<FaSpinner className="mr-2 h-16 w-16 animate-spin"/> | ||
</div> | ||
); | ||
} | ||
|
||
if (!supabase) { | ||
return <div>User accounts are disabled because Supabase client is unavailable</div> | ||
} | ||
|
||
async function handleSignInWithProvider(provider: 'google' | 'github' | 'discord') { | ||
const { data, error } = await supabase!.auth.signInWithOAuth({ | ||
provider: provider, | ||
options: { | ||
redirectTo: process.env.AUTH_CALLBACK_URL ?? `http://localhost:3000/auth/callback`, | ||
// Get Google provider_refresh_token | ||
// queryParams: { | ||
// access_type: 'offline', | ||
// prompt: 'consent', | ||
// }, | ||
}, | ||
}) | ||
|
||
if (!error) { | ||
setFeedback(null) | ||
return | ||
} | ||
setFeedback(error.message) | ||
} | ||
|
||
const onLogin = async (data: z.infer<typeof loginFormSchema>) => { | ||
setIsLoading(true) | ||
const error = await login(data) | ||
setIsLoading(false) | ||
if (error) { | ||
setFeedback(error) | ||
return | ||
} | ||
setFeedback(null) | ||
} | ||
|
||
const onSignup = async (data: z.infer<typeof loginFormSchema>) => { | ||
if (await form.trigger()) { | ||
setIsLoading(true) | ||
const error = await signup(data) | ||
setIsLoading(false) | ||
if (error) { | ||
setFeedback(error) | ||
return | ||
} | ||
setFeedback(null) | ||
} | ||
} | ||
|
||
return ( | ||
<div className="flex items-center justify-center h-[80vh]"> | ||
<div className="w-full max-w-md p-8 rounded-lg shadow-md space-y-6"> | ||
<div className='mb-6 space-y-2'> | ||
<Button className="w-full" onClick={() => handleSignInWithProvider('google')} variant="outline" type="button" disabled={isLoading}> | ||
<FaGoogle className="mr-2 h-4 w-4" /> | ||
Sign in with Google | ||
</Button> | ||
<Button className="w-full" onClick={() => handleSignInWithProvider('github')} variant="outline" type="button" disabled={isLoading}> | ||
<FaGithub className="mr-2 h-4 w-4" /> | ||
Sign in with GitHub | ||
</Button> | ||
<Button className="w-full" onClick={() => handleSignInWithProvider('discord')} variant="outline" type="button" disabled={isLoading}> | ||
<FaDiscord className="mr-2 h-4 w-4" /> | ||
Sign in with Discord | ||
</Button> | ||
</div> | ||
<Form {...form}> | ||
<form onSubmit={form.handleSubmit(onLogin)}> | ||
<FormField | ||
control={form.control} | ||
name="email" | ||
render={({ field }) => ( | ||
<FormItem className='mb-4'> | ||
<FormLabel>Email</FormLabel> | ||
<FormControl> | ||
<Input placeholder="[email protected]" {...field} /> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
<FormField | ||
control={form.control} | ||
name="password" | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel>Password</FormLabel> | ||
<FormControl> | ||
<PasswordInput placeholder="password" {...field} /> | ||
</FormControl> | ||
<FormDescription> | ||
Password needs to be at least 6 characters long | ||
</FormDescription> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
<div className='flex w-full space-x-4 mt-6 mb-6'> | ||
<Button className='w-1/2 flex justify-center' type="submit" disabled={isLoading}> | ||
Log in | ||
</Button> | ||
<Button | ||
className='w-1/2 flex justify-center' | ||
variant='outline' | ||
type="button" | ||
onClick={form.handleSubmit(onSignup)} | ||
disabled={isLoading} | ||
> | ||
Sign up | ||
</Button> | ||
</div> | ||
</form> | ||
<p className='text-red-500 text-sm'>{feedback}</p> | ||
<p className='text-primary text-center text-sm'> | ||
By continuing you agree to everything | ||
</p> | ||
</Form> | ||
</div> | ||
</div> | ||
) | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it possible to default these to our instance safely
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so