Skip to content

Commit 0b357f2

Browse files
feat: integrate Supabase authentication and database client into the web application
1 parent b2b0c17 commit 0b357f2

10 files changed

Lines changed: 670 additions & 114 deletions

File tree

apps/backend/prisma/schema.prisma

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ generator client {
33
}
44

55
datasource db {
6-
provider = "postgresql"
7-
url = env("SUPABASE_DATABASE_URL")
6+
provider = "postgresql"
7+
url = env("SUPABASE_DATABASE_URL")
8+
directUrl = env("DIRECT_URL")
89
}
910

1011
enum GroupRole {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { createClient } from '@/utils/supabase/server';
2+
import { cookies } from 'next/headers';
3+
4+
export default async function Page() {
5+
const cookieStore = await cookies();
6+
const supabase = createClient(cookieStore);
7+
8+
// Note: This expects a 'todos' table to exist in your Supabase project.
9+
const { data: todos, error } = await supabase.from('todos').select();
10+
11+
if (error) {
12+
return (
13+
<div className="p-8">
14+
<h1 className="text-2xl font-bold text-red-500">Supabase Connection Error</h1>
15+
<pre className="mt-4 p-4 bg-gray-100 rounded">{JSON.stringify(error, null, 2)}</pre>
16+
</div>
17+
);
18+
}
19+
20+
return (
21+
<div className="p-8">
22+
<h1 className="text-2xl font-bold mb-4">Supabase Connection Success</h1>
23+
<p className="mb-4 text-gray-600">Successfully connected to: {process.env.NEXT_PUBLIC_SUPABASE_URL}</p>
24+
25+
<h2 className="text-xl font-semibold mb-2">Todos:</h2>
26+
{todos && todos.length > 0 ? (
27+
<ul className="list-disc pl-5">
28+
{todos.map((todo: any) => (
29+
<li key={todo.id}>{todo.name}</li>
30+
))}
31+
</ul>
32+
) : (
33+
<p className="text-gray-500 italic">No todos found or 'todos' table is empty.</p>
34+
)}
35+
</div>
36+
);
37+
}

apps/web/middleware.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { NextRequest, NextResponse } from 'next/server';
22
import { authCookies, accessCookieOptions, refreshCookieOptions } from './src/lib/authCookies';
33
import { getBackendBaseUrl } from './src/lib/env';
44
import { isJwtExpiringSoon } from './src/lib/jwt';
5+
import { createClient } from './utils/supabase/middleware';
56

67
function isProtectedPath(pathname: string): boolean {
78
return pathname === '/dashboard' || pathname.startsWith('/dashboard/');
89
}
910

1011
function isAuthPage(pathname: string): boolean {
11-
return pathname === '/login' || pathname === '/register';
12+
return pathname === '/login' || pathname === '/register' || pathname === '/supabase-test';
1213
}
1314

1415
function parseCookieValue(setCookieHeader: string | null, cookieName: string): string | null {
@@ -84,12 +85,23 @@ export async function middleware(req: NextRequest) {
8485
const pathname = req.nextUrl.pathname;
8586
const accessToken = req.cookies.get(authCookies.accessToken)?.value ?? null;
8687

88+
// 1. Handle Supabase session refreshing
89+
let res = NextResponse.next({
90+
request: {
91+
headers: req.headers,
92+
},
93+
});
94+
95+
const supabase = createClient(req, res);
96+
// This refreshes the session if needed
97+
await supabase.auth.getUser();
98+
8799
if (isAuthPage(pathname) && accessToken) {
88100
return NextResponse.redirect(new URL('/dashboard', req.url));
89101
}
90102

91103
if (!isProtectedPath(pathname)) {
92-
return NextResponse.next();
104+
return res;
93105
}
94106

95107
if (!accessToken) {
@@ -99,20 +111,19 @@ export async function middleware(req: NextRequest) {
99111
}
100112

101113
if (!isJwtExpiringSoon(accessToken, 60_000)) {
102-
return NextResponse.next();
114+
return res;
103115
}
104116

105117
const refreshed = await tryRefreshTokens(req);
106118
if (!refreshed) {
107-
return NextResponse.next();
119+
return res;
108120
}
109121

110-
const res = NextResponse.next();
111122
res.cookies.set(authCookies.accessToken, refreshed.accessToken, accessCookieOptions);
112123
res.cookies.set(authCookies.refreshToken, refreshed.refreshToken, refreshCookieOptions);
113124
return res;
114125
}
115126

116127
export const config = {
117-
matcher: ['/dashboard/:path*', '/login', '/register'],
128+
matcher: ['/dashboard/:path*', '/login', '/register', '/supabase-test'],
118129
};

apps/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
},
1212
"dependencies": {
1313
"@fairshare/shared-types": "workspace:*",
14+
"@supabase/ssr": "^0.9.0",
15+
"@supabase/supabase-js": "^2.100.1",
1416
"@types/gsap": "^3.0.0",
1517
"framer-motion": "^12.35.0",
1618
"gsap": "^3.14.2",

apps/web/tsconfig.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@
1616
"isolatedModules": true,
1717
"jsx": "preserve",
1818
"incremental": true,
19+
"forceConsistentCasingInFileNames": true,
20+
"baseUrl": ".",
1921
"plugins": [
2022
{
2123
"name": "next"
2224
}
23-
]
25+
],
26+
"paths": {
27+
"@/*": ["./*"]
28+
}
2429
},
2530
"include": [
2631
"**/*.ts",

apps/web/utils/supabase/client.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { createBrowserClient } from "@supabase/ssr";
2+
import { type SupabaseClient } from "@supabase/supabase-js";
3+
4+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
5+
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
6+
7+
export const createClient = (): SupabaseClient =>
8+
createBrowserClient(
9+
supabaseUrl!,
10+
supabaseKey!,
11+
);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { createServerClient } from "@supabase/ssr";
2+
import { type SupabaseClient } from "@supabase/supabase-js";
3+
import { type NextRequest, NextResponse } from "next/server";
4+
5+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
6+
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
7+
8+
export const createClient = (request: NextRequest, response: NextResponse): SupabaseClient => {
9+
return createServerClient(
10+
supabaseUrl!,
11+
supabaseKey!,
12+
{
13+
cookies: {
14+
getAll() {
15+
return request.cookies.getAll()
16+
},
17+
setAll(cookiesToSet) {
18+
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
19+
cookiesToSet.forEach(({ name, value, options }) =>
20+
response.cookies.set(name, value, options)
21+
)
22+
},
23+
},
24+
},
25+
);
26+
};

apps/web/utils/supabase/server.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { createServerClient } from "@supabase/ssr";
2+
import { type SupabaseClient } from "@supabase/supabase-js";
3+
import { cookies } from "next/headers";
4+
5+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
6+
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
7+
8+
export const createClient = (cookieStore: Awaited<ReturnType<typeof cookies>>): SupabaseClient => {
9+
return createServerClient(
10+
supabaseUrl!,
11+
supabaseKey!,
12+
{
13+
cookies: {
14+
getAll() {
15+
return cookieStore.getAll()
16+
},
17+
setAll(cookiesToSet) {
18+
try {
19+
cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options))
20+
} catch {
21+
// The `setAll` method was called from a Server Component.
22+
// This can be ignored if you have middleware refreshing
23+
// user sessions.
24+
}
25+
},
26+
},
27+
},
28+
);
29+
};

0 commit comments

Comments
 (0)