From 6cfe58e96d3f01c033d4ac2ebbe4da59b28081d9 Mon Sep 17 00:00:00 2001 From: Michele Date: Sat, 31 Jan 2026 13:36:36 +0100 Subject: [PATCH] feat(01-05): add middleware for session refresh and route protection - Create updateSession helper for Supabase session management - Add main middleware with protected and auth route handling - Configure matcher to exclude static files for performance - Session refresh on every request prevents random logouts Co-Authored-By: Claude Opus 4.5 --- middleware.ts | 49 ++++++++++++++++++++++++++++++++++ src/lib/supabase/middleware.ts | 37 +++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 middleware.ts create mode 100644 src/lib/supabase/middleware.ts diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..1b4214b --- /dev/null +++ b/middleware.ts @@ -0,0 +1,49 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { updateSession } from '@/lib/supabase/middleware' + +// Routes that require authentication +const protectedRoutes = ['/dashboard', '/settings', '/subscription'] + +// Routes that should redirect to dashboard if already authenticated +const authRoutes = ['/login', '/register'] + +export async function middleware(request: NextRequest) { + const { supabaseResponse, user } = await updateSession(request) + const { pathname } = request.nextUrl + + // Check if trying to access protected route without auth + const isProtectedRoute = protectedRoutes.some(route => + pathname.startsWith(route) + ) + + if (isProtectedRoute && !user) { + const redirectUrl = new URL('/login', request.url) + // Save the original URL to redirect back after login + redirectUrl.searchParams.set('redirectTo', pathname) + return NextResponse.redirect(redirectUrl) + } + + // Check if trying to access auth routes while already authenticated + const isAuthRoute = authRoutes.some(route => + pathname.startsWith(route) + ) + + if (isAuthRoute && user) { + return NextResponse.redirect(new URL('/dashboard', request.url)) + } + + return supabaseResponse +} + +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * - public folder files + */ + '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', + ], +} diff --git a/src/lib/supabase/middleware.ts b/src/lib/supabase/middleware.ts new file mode 100644 index 0000000..9b80647 --- /dev/null +++ b/src/lib/supabase/middleware.ts @@ -0,0 +1,37 @@ +import { createServerClient } from '@supabase/ssr' +import { NextResponse, type NextRequest } from 'next/server' + +export async function updateSession(request: NextRequest) { + let supabaseResponse = NextResponse.next({ + request, + }) + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_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) + ) + }, + }, + } + ) + + // IMPORTANT: Do not remove this line + // Refreshing the auth token is crucial for keeping the session alive + const { data: { user } } = await supabase.auth.getUser() + + return { supabaseResponse, user } +}