Skip to content

Commit 83ea047

Browse files
committed
fix: restore middleware.ts and add robust error handling for Supabase session issues
1 parent e459fb1 commit 83ea047

File tree

4 files changed

+145
-183
lines changed

4 files changed

+145
-183
lines changed

app/admin/layout.tsx

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { createServerClient } from '@supabase/ssr'
2-
import { cookies } from 'next/headers'
1+
import { createClient } from '@/utils/supabase/server'
32
import { redirect } from 'next/navigation'
43
import { Sidebar } from '@/components/admin/Sidebar'
54

@@ -8,22 +7,15 @@ export default async function AdminLayout({
87
}: {
98
children: React.ReactNode
109
}) {
11-
const cookieStore = await cookies()
12-
const supabase = createServerClient(
13-
process.env.NEXT_PUBLIC_SUPABASE_URL!,
14-
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
15-
{
16-
cookies: {
17-
get(name: string) {
18-
return cookieStore.get(name)?.value
19-
},
20-
},
21-
}
22-
)
10+
const supabase = await createClient()
2311

24-
const {
25-
data: { user },
26-
} = await supabase.auth.getUser()
12+
let user = null;
13+
try {
14+
const { data: { user: foundUser } } = await supabase.auth.getUser()
15+
user = foundUser;
16+
} catch {
17+
// Fall through to redirect
18+
}
2719

2820
if (!user) {
2921
redirect('/login')

app/layout.tsx

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { CartProvider } from "@/context/CartContext";
55
import { Navbar } from "@/components/Navbar";
66
import { ShoppingBagDrawer } from "@/components/ShoppingBagDrawer";
77
import { Analytics } from "@vercel/analytics/next";
8-
import { createServerClient } from "@supabase/ssr";
9-
import { cookies } from "next/headers";
8+
import { createClient } from "@/utils/supabase/server";
9+
import { Footer } from "@/components/Footer";
10+
import { Toaster } from 'sonner';
1011

1112
const playfair = Playfair_Display({
1213
variable: "--font-playfair",
@@ -85,34 +86,22 @@ export const metadata: Metadata = {
8586
},
8687
};
8788

88-
import { Footer } from "@/components/Footer";
89-
90-
import { Toaster } from 'sonner';
9189

9290
export default async function RootLayout({
9391
children,
9492
}: Readonly<{
9593
children: React.ReactNode;
9694
}>) {
97-
// Fetch user server-side for SSR-safe session handling
98-
const cookieStore = await cookies();
99-
100-
const supabase = createServerClient(
101-
process.env.NEXT_PUBLIC_SUPABASE_URL!,
102-
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
103-
{
104-
cookies: {
105-
get(name: string) {
106-
return cookieStore.get(name)?.value;
107-
},
108-
},
109-
}
110-
);
95+
const supabase = await createClient();
11196

112-
// Fetch user server-side (optional - available for future use)
113-
const {
114-
data: { user },
115-
} = await supabase.auth.getUser();
97+
// Fetch user server-side safely
98+
let user = null;
99+
try {
100+
const { data: { user: foundUser } } = await supabase.auth.getUser();
101+
user = foundUser;
102+
} catch (err) {
103+
console.error("Auth session sync failed:", err);
104+
}
116105

117106
return (
118107
<html lang="en" className="dark">

middleware.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { createServerClient } from '@supabase/ssr'
2+
import { NextResponse, type NextRequest } from 'next/server'
3+
4+
/**
5+
* LMXEngine Middleware
6+
* Unified security and session management layer.
7+
*/
8+
export default async function middleware(request: NextRequest) {
9+
let response = NextResponse.next({
10+
request: {
11+
headers: request.headers,
12+
},
13+
})
14+
15+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
16+
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
17+
18+
if (!supabaseUrl || !supabaseAnonKey) {
19+
return response;
20+
}
21+
22+
const supabase = createServerClient(
23+
supabaseUrl,
24+
supabaseAnonKey,
25+
{
26+
cookies: {
27+
getAll() {
28+
return request.cookies.getAll()
29+
},
30+
setAll(cookiesToSet) {
31+
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
32+
response = NextResponse.next({
33+
request: {
34+
headers: request.headers,
35+
},
36+
})
37+
cookiesToSet.forEach(({ name, value, options }) =>
38+
response.cookies.set(name, value, options)
39+
)
40+
},
41+
},
42+
}
43+
)
44+
45+
// Using getUser() is the secure way to check auth, but we handle the specific "refresh_token_not_found" error.
46+
const { data: { user }, error } = await supabase.auth.getUser()
47+
48+
// 1. Handle Refresh Token Error / Missing Session
49+
if (error && error.code === 'refresh_token_not_found') {
50+
// Clear cookies and bounce to login if trying to access protected routes
51+
const isProtected = request.nextUrl.pathname.startsWith('/admin') || request.nextUrl.pathname.startsWith('/checkout');
52+
if (isProtected) {
53+
const redirectResponse = NextResponse.redirect(new URL('/login', request.url))
54+
// Attempt to clear session cookies
55+
redirectResponse.cookies.delete('sb-access-token')
56+
redirectResponse.cookies.delete('sb-refresh-token')
57+
return redirectResponse
58+
}
59+
}
60+
61+
// 2. Admin Portal Protection
62+
if (request.nextUrl.pathname.startsWith('/admin')) {
63+
if (!user) {
64+
return NextResponse.redirect(new URL('/login', request.url))
65+
}
66+
67+
const { data: profile } = await supabase
68+
.from('profiles')
69+
.select('role')
70+
.eq('id', user.id)
71+
.single()
72+
73+
if (!profile || profile.role !== 'admin') {
74+
return NextResponse.redirect(new URL('/', request.url))
75+
}
76+
}
77+
78+
// 3. Checkout Flow Protection
79+
if (request.nextUrl.pathname.startsWith('/checkout')) {
80+
if (!user) {
81+
return NextResponse.redirect(new URL(`/login?next=${request.nextUrl.pathname}`, request.url))
82+
}
83+
}
84+
85+
// 4. Login Redirect Logic
86+
if (user && request.nextUrl.pathname.startsWith('/login')) {
87+
return NextResponse.redirect(new URL('/', request.url))
88+
}
89+
90+
// 5. Kill Switch (Store Enabled Check)
91+
if (!request.nextUrl.pathname.startsWith('/admin') &&
92+
!request.nextUrl.pathname.startsWith('/api') &&
93+
!request.nextUrl.pathname.startsWith('/login') &&
94+
!request.nextUrl.pathname.startsWith('/_next') &&
95+
!request.nextUrl.pathname.startsWith('/maintenance')) {
96+
97+
// Use service role or public check for settings?
98+
// profiles RLS is based on is_admin(), so for public we need to ensure site_settings is public.
99+
const { data: settings } = await supabase
100+
.from('site_settings')
101+
.select('setting_value')
102+
.eq('setting_key', 'store_enabled')
103+
.single();
104+
105+
if (settings && settings.setting_value === false) {
106+
return NextResponse.redirect(new URL('/maintenance', request.url));
107+
}
108+
}
109+
110+
return response
111+
}
112+
113+
export const config = {
114+
matcher: [
115+
/*
116+
* Match all request paths except for the ones starting with:
117+
* - _next/static (static files)
118+
* - _next/image (image optimization files)
119+
* - favicon.ico (favicon file)
120+
* - api/webhooks (Stripe needs public access)
121+
*/
122+
'/((?!_next/static|_next/image|favicon.ico|api/webhooks|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
123+
],
124+
}

proxy.ts

Lines changed: 0 additions & 143 deletions
This file was deleted.

0 commit comments

Comments
 (0)