-
Notifications
You must be signed in to change notification settings - Fork 0
Added user authentication with email and password #46
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { type EmailOtpType } from '@supabase/supabase-js' | ||
| import { type NextRequest } from 'next/server' | ||
|
|
||
| import { createClient } from '@/lib/client/supabase/server' | ||
| import { redirect } from 'next/navigation' | ||
|
|
||
| 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 = await createClient() | ||
|
|
||
| const { error } = await supabase.auth.verifyOtp({ | ||
| type, | ||
| token_hash, | ||
| }) | ||
| if (!error) { | ||
| // redirect user to specified redirect URL or root of app | ||
| redirect(next) | ||
|
Comment on lines
+11
to
+22
|
||
| } | ||
| } | ||
|
|
||
| // redirect the user to an error page with some instructions | ||
| redirect('/auth/auth-code-error') | ||
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| // temp ugly auth page for testing | ||
| "use client"; | ||
|
|
||
| import { useState } from "react"; | ||
| import { signInWithEmail, signUpWithEmail } from "@/lib/client/supabase/auth"; | ||
|
|
||
| export default function LoginPage() { | ||
| const [signInEmail, setSignInEmail] = useState(""); | ||
| const [signInPassword, setSignInPassword] = useState(""); | ||
| const [signUpEmail, setSignUpEmail] = useState(""); | ||
| const [signUpPassword, setSignUpPassword] = useState(""); | ||
| const [message, setMessage] = useState<string | null>(null); | ||
| const [responseData, setResponseData] = useState<unknown>(null); | ||
| const [loading, setLoading] = useState(false); | ||
|
|
||
| const handleSignIn = async (event: React.FormEvent<HTMLFormElement>) => { | ||
| event.preventDefault(); | ||
| setLoading(true); | ||
| setMessage(null); | ||
| setResponseData(null); | ||
| const { data, error } = await signInWithEmail( | ||
| signInEmail, | ||
| signInPassword | ||
| ); | ||
| setMessage(error ? error.message : "Signed in successfully."); | ||
| setResponseData(data); | ||
| setLoading(false); | ||
| }; | ||
LeandroHamaguchi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const handleSignUp = async (event: React.FormEvent<HTMLFormElement>) => { | ||
| event.preventDefault(); | ||
| setLoading(true); | ||
| setMessage(null); | ||
| setResponseData(null); | ||
| const { data, error } = await signUpWithEmail(signUpEmail, signUpPassword); | ||
| setMessage( | ||
| error ? error.message : "Check your email to confirm your account." | ||
| ); | ||
| setResponseData(data); | ||
| setLoading(false); | ||
| }; | ||
|
|
||
| return ( | ||
| <main> | ||
| <h1>Sign In</h1> | ||
| <form onSubmit={handleSignIn}> | ||
| <label htmlFor="signin-email">Email</label> | ||
| <input | ||
| id="signin-email" | ||
| type="email" | ||
| value={signInEmail} | ||
| onChange={(event) => setSignInEmail(event.target.value)} | ||
| required | ||
| /> | ||
|
|
||
| <label htmlFor="signin-password">Password</label> | ||
| <input | ||
| id="signin-password" | ||
| type="password" | ||
| value={signInPassword} | ||
| onChange={(event) => setSignInPassword(event.target.value)} | ||
| required | ||
| /> | ||
|
|
||
| <button type="submit" disabled={loading}> | ||
| Sign In | ||
| </button> | ||
| </form> | ||
|
|
||
| <h1>Sign Up</h1> | ||
| <form onSubmit={handleSignUp}> | ||
| <label htmlFor="signup-email">Email</label> | ||
| <input | ||
| id="signup-email" | ||
| type="email" | ||
| value={signUpEmail} | ||
| onChange={(event) => setSignUpEmail(event.target.value)} | ||
| required | ||
| /> | ||
|
|
||
| <label htmlFor="signup-password">Password</label> | ||
| <input | ||
| id="signup-password" | ||
| type="password" | ||
| value={signUpPassword} | ||
| onChange={(event) => setSignUpPassword(event.target.value)} | ||
| required | ||
| /> | ||
|
|
||
| <button type="submit" disabled={loading}> | ||
| Sign Up | ||
| </button> | ||
| </form> | ||
|
|
||
| {message ? <p>{message}</p> : null} | ||
| {responseData ? ( | ||
| <pre>{JSON.stringify(responseData, null, 2)}</pre> | ||
| ) : null} | ||
| </main> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1 +1,16 @@ | ||||||||||||
| // To be developed: Auth related functions for Supabase client | ||||||||||||
| import { createClient } from "./client"; | ||||||||||||
|
|
||||||||||||
| export async function signUpWithEmail(email: string, password: string) { | ||||||||||||
| const supabase = createClient(); | ||||||||||||
| const { data, error } = await supabase.auth.signUp({ email, password }); | ||||||||||||
|
||||||||||||
| const { data, error } = await supabase.auth.signUp({ email, password }); | |
| const { data, error } = await supabase.auth.signUp( | |
| { email, password }, | |
| { emailRedirectTo: `${window.location.origin}/auth/confirm` } | |
| ); |
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.
Does the confirmation link work properly?
LeandroHamaguchi marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Nov 22, 2025
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.
Consider adding email format validation before calling the Supabase API. While the HTML input type="email" provides basic validation, additional checks in the auth functions would provide defense-in-depth and ensure consistent validation if these functions are called from other contexts.
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.
We can ignore this for now for the same reason that we ignore the password checks. Performing these strength requirements checks on the frontend would provide faster feedback to the user.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { createBrowserClient } from "@supabase/ssr"; | ||
|
|
||
| export function createClient() { | ||
| return createBrowserClient( | ||
| process.env.NEXT_PUBLIC_SUPABASE_URL!, | ||
| process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, | ||
| ); | ||
| } |
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should use a different name for this file since it's the same as src/proxy.ts |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { createServerClient } from "@supabase/ssr"; | ||
| import { NextResponse, type NextRequest } from "next/server"; | ||
|
|
||
| export async function updateSession(request: NextRequest) { | ||
| let supabaseResponse = NextResponse.next({ | ||
| request, | ||
| }); | ||
|
|
||
| // With Fluid compute, don't put this client in a global environment | ||
| // variable. Always create a new one on each request. | ||
| const supabase = createServerClient( | ||
| process.env.NEXT_PUBLIC_SUPABASE_URL!, | ||
| process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, | ||
| { | ||
| cookies: { | ||
| getAll() { | ||
| return request.cookies.getAll(); | ||
| }, | ||
| setAll(cookiesToSet) { | ||
| cookiesToSet.forEach(({ name, value }) => | ||
| request.cookies.set(name, value), | ||
| ); | ||
| supabaseResponse = NextResponse.next({ | ||
| request, | ||
| }); | ||
| cookiesToSet.forEach(({ name, value, options }) => | ||
| supabaseResponse.cookies.set(name, value, options), | ||
| ); | ||
| }, | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| // Do not run code between createServerClient and | ||
| // supabase.auth.getClaims(). A simple mistake could make it very hard to debug | ||
| // issues with users being randomly logged out. | ||
|
|
||
| // IMPORTANT: If you remove getClaims() and you use server-side rendering | ||
| // with the Supabase client, your users may be randomly logged out. | ||
| const { data } = await supabase.auth.getClaims(); | ||
| const user = data?.claims; | ||
|
|
||
| if ( | ||
| request.nextUrl.pathname !== "/" && | ||
| !user && | ||
| !request.nextUrl.pathname.startsWith("/login") && | ||
| !request.nextUrl.pathname.startsWith("/auth") | ||
| ) { | ||
| // no user, potentially respond by redirecting the user to the login page | ||
| const url = request.nextUrl.clone(); | ||
| url.pathname = "/auth/login"; | ||
| return NextResponse.redirect(url); | ||
| } | ||
|
|
||
| // IMPORTANT: You *must* return the supabaseResponse object as it is. | ||
| // If you're creating a new response object with NextResponse.next() make sure to: | ||
| // 1. Pass the request in it, like so: | ||
| // const myNewResponse = NextResponse.next({ request }) | ||
| // 2. Copy over the cookies, like so: | ||
| // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll()) | ||
| // 3. Change the myNewResponse object to fit your needs, but avoid changing | ||
| // the cookies! | ||
| // 4. Finally: | ||
| // return myNewResponse | ||
| // If this is not done, you may be causing the browser and server to go out | ||
| // of sync and terminate the user's session prematurely! | ||
|
|
||
| return supabaseResponse; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,20 @@ | ||||||
| import { updateSession } from "@/lib/supabase/proxy"; | ||||||
|
||||||
| import { updateSession } from "@/lib/supabase/proxy"; | |
| import { updateSession } from "@/lib/client/supabase/proxy"; |
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.
Double check this
Copilot
AI
Nov 22, 2025
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.
This file should be named middleware.ts rather than proxy.ts and placed at the root of src/ directory. Next.js expects middleware to be exported from a file named middleware.ts at the root of the src directory (or project root). The exported function should also be named middleware instead of proxy.
| export async function proxy(request: NextRequest) { | |
| export async function middleware(request: NextRequest) { |
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.
Does the name proxy.ts for this file work? Because for the Next.js version (15.5.6) we're using it asks for the function to be called middleware. The newest version (16) of Next.js requires proxy.ts instead of middleware.ts, but in the version we're using this isn't used yet.
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.
Inconsistent quote style. This file uses single quotes while other files in the codebase use double quotes (e.g.,
src/lib/client/supabase/client.ts,src/lib/client/supabase/auth.ts). Consider using double quotes for consistency with the rest of the codebase.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 think this is also the errors Vercel points out. Make sure you're up-to-date with the latest main branch! There's code in the main branch that runs formatting/linting automatically for you before every commit.