diff --git a/src/app/auth/confirm/route.ts b/src/app/auth/confirm/route.ts new file mode 100644 index 0000000..ddf4506 --- /dev/null +++ b/src/app/auth/confirm/route.ts @@ -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) + } + } + + // redirect the user to an error page with some instructions + redirect('/auth/auth-code-error') +} \ No newline at end of file diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..7693748 --- /dev/null +++ b/src/app/login/page.tsx @@ -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(null); + const [responseData, setResponseData] = useState(null); + const [loading, setLoading] = useState(false); + + const handleSignIn = async (event: React.FormEvent) => { + 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); + }; + + const handleSignUp = async (event: React.FormEvent) => { + 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 ( +
+

Sign In

+
+ + setSignInEmail(event.target.value)} + required + /> + + + setSignInPassword(event.target.value)} + required + /> + + +
+ +

Sign Up

+
+ + setSignUpEmail(event.target.value)} + required + /> + + + setSignUpPassword(event.target.value)} + required + /> + + +
+ + {message ?

{message}

: null} + {responseData ? ( +
{JSON.stringify(responseData, null, 2)}
+ ) : null} +
+ ); +} diff --git a/src/lib/client/supabase/auth.ts b/src/lib/client/supabase/auth.ts index 55a8471..5b43fb8 100644 --- a/src/lib/client/supabase/auth.ts +++ b/src/lib/client/supabase/auth.ts @@ -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 }); + return { data, error }; +} + +export async function signInWithEmail(email: string, password: string) { + const supabase = createClient(); + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + return { data, error }; +} diff --git a/src/lib/client/supabase/client.ts b/src/lib/client/supabase/client.ts new file mode 100644 index 0000000..e50bdf6 --- /dev/null +++ b/src/lib/client/supabase/client.ts @@ -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!, + ); +} diff --git a/src/lib/client/supabase/proxy.ts b/src/lib/client/supabase/proxy.ts new file mode 100644 index 0000000..d7bbf00 --- /dev/null +++ b/src/lib/client/supabase/proxy.ts @@ -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; +} diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 0000000..503ab12 --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,20 @@ +import { updateSession } from "@/lib/supabase/proxy"; +import { type NextRequest } from "next/server"; + +export async function proxy(request: NextRequest) { + return await updateSession(request); +} + +export const config = { + matcher: [ + /* + * Match all request paths except: + * - _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)$).*)", + ], +};