Skip to content

Commit a159184

Browse files
committed
feat(auth): implement authentication flow with supabase integration
1 parent 05dd695 commit a159184

File tree

10 files changed

+208
-0
lines changed

10 files changed

+208
-0
lines changed

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
NEXT_PUBLIC_SUPABASE_URL=<url>
2+
NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon_key>

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
"@heroui/theme": "2.4.6",
1515
"@hookform/resolvers": "5.0.1",
1616
"@react-spring/web": "9.7.5",
17+
"@supabase/ssr": "^0.6.1",
18+
"@supabase/supabase-js": "^2.49.4",
1719
"framer-motion": "^12.6.3",
1820
"lucide-react": "0.475.0",
1921
"next": "15.2.5",

src/actions/auth.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"use server";
2+
3+
import { revalidatePath } from "next/cache";
4+
import { redirect } from "next/navigation";
5+
import { createClient } from "@/lib/supabase/server";
6+
7+
export async function login(formData: FormData) {
8+
const supabase = await createClient();
9+
10+
const data = {
11+
email: formData.get("email") as string,
12+
password: formData.get("password") as string,
13+
};
14+
15+
const { error } = await supabase.auth.signInWithPassword(data);
16+
17+
if (error) {
18+
redirect("/error");
19+
}
20+
21+
revalidatePath("/", "layout");
22+
redirect("/");
23+
}
24+
25+
export async function signup(formData: FormData) {
26+
const supabase = await createClient();
27+
28+
const data = {
29+
email: formData.get("email") as string,
30+
password: formData.get("password") as string,
31+
};
32+
33+
const { error } = await supabase.auth.signUp(data);
34+
35+
if (error) {
36+
redirect("/error");
37+
}
38+
39+
revalidatePath("/", "layout");
40+
redirect("/");
41+
}

src/app/(private)/dashboard/page.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { redirect } from "next/navigation";
2+
import { createClient } from "@/lib/supabase/server";
3+
4+
export default async function DashboardPage() {
5+
const supabase = await createClient();
6+
7+
const { data, error } = await supabase.auth.getUser();
8+
if (error || !data?.user) {
9+
redirect("/auth/login");
10+
}
11+
12+
return (
13+
<main>
14+
<div>Dashboard</div>
15+
<p>Hello {data.user.email}</p>
16+
</main>
17+
);
18+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { type EmailOtpType } from "@supabase/supabase-js";
2+
import { type NextRequest } from "next/server";
3+
4+
import { redirect } from "next/navigation";
5+
import { createClient } from "@/lib/supabase/server";
6+
7+
export async function GET(request: NextRequest) {
8+
const { searchParams } = new URL(request.url);
9+
const token_hash = searchParams.get("token_hash");
10+
const type = searchParams.get("type") as EmailOtpType | null;
11+
const next = searchParams.get("next") ?? "/";
12+
13+
if (token_hash && type) {
14+
const supabase = await createClient();
15+
16+
const { error } = await supabase.auth.verifyOtp({
17+
type,
18+
token_hash,
19+
});
20+
if (!error) {
21+
redirect(next);
22+
}
23+
}
24+
25+
redirect("/error");
26+
}

src/app/(public)/auth/login/page.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { login, signup } from "@/actions/auth";
2+
3+
export default function LoginPage() {
4+
return (
5+
<form>
6+
<label htmlFor="email">Email:</label>
7+
<input id="email" name="email" type="email" required />
8+
<label htmlFor="password">Password:</label>
9+
<input id="password" name="password" type="password" required />
10+
<button formAction={login}>Log in</button>
11+
<button formAction={signup}>Sign up</button>
12+
</form>
13+
);
14+
}

src/lib/supabase/client.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createBrowserClient } from "@supabase/ssr";
2+
3+
export function createClient() {
4+
return createBrowserClient(
5+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
6+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
7+
);
8+
}

src/lib/supabase/middleware.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { createServerClient } from "@supabase/ssr";
2+
import { NextResponse, type NextRequest } from "next/server";
3+
4+
const publicRoutes = ["/", "/auth/login"];
5+
6+
export async function updateSession(request: NextRequest) {
7+
let supabaseResponse = NextResponse.next({
8+
request,
9+
});
10+
11+
const supabase = createServerClient(
12+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
13+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
14+
{
15+
cookies: {
16+
getAll() {
17+
return request.cookies.getAll();
18+
},
19+
setAll(cookiesToSet) {
20+
cookiesToSet.forEach(({ name, value, options }) =>
21+
request.cookies.set(name, value),
22+
);
23+
supabaseResponse = NextResponse.next({
24+
request,
25+
});
26+
cookiesToSet.forEach(({ name, value, options }) =>
27+
supabaseResponse.cookies.set(name, value, options),
28+
);
29+
},
30+
},
31+
},
32+
);
33+
34+
const {
35+
data: { user },
36+
} = await supabase.auth.getUser();
37+
38+
if (
39+
!user &&
40+
!request.nextUrl.pathname.startsWith("/auth/login") &&
41+
!request.nextUrl.pathname.startsWith("/auth/confirm") &&
42+
!publicRoutes.includes(request.nextUrl.pathname)
43+
) {
44+
const url = request.nextUrl.clone();
45+
url.pathname = "/auth/login";
46+
return NextResponse.redirect(url);
47+
}
48+
49+
return supabaseResponse;
50+
}

src/lib/supabase/server.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { cookies } from "next/headers";
2+
import { createServerClient } from "@supabase/ssr";
3+
4+
export async function createClient() {
5+
const cookieStore = await cookies();
6+
7+
return createServerClient(
8+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
9+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
10+
{
11+
cookies: {
12+
getAll() {
13+
return cookieStore.getAll();
14+
},
15+
setAll(cookiesToSet) {
16+
try {
17+
cookiesToSet.forEach(({ name, value, options }) =>
18+
cookieStore.set(name, value, options),
19+
);
20+
} catch {
21+
console.error("Error setting cookies");
22+
}
23+
},
24+
},
25+
},
26+
);
27+
}

src/middleware.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { type NextRequest } from "next/server";
2+
3+
import { updateSession } from "@/lib/supabase/middleware";
4+
5+
export async function middleware(request: NextRequest) {
6+
return await updateSession(request);
7+
}
8+
9+
export const config = {
10+
matcher: [
11+
/*
12+
* Match all request paths except for the ones starting with:
13+
* - _next/static (static files)
14+
* - _next/image (image optimization files)
15+
* - favicon.ico (favicon file)
16+
* Feel free to modify this pattern to include more paths.
17+
*/
18+
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
19+
],
20+
};

0 commit comments

Comments
 (0)