From 2bf0ac0f3ef11cf72933996a7261e41a7a0e857a Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 19 Jan 2026 12:36:35 +0100 Subject: [PATCH] chore: Implement route protection and security enhancements - Add src/middleware.ts for global route proection - Whitelist public routes (/, /auth/*, /splits/[slug]) - Add redirect logic to Home page for returning users - Fix minor lint issues in Home page --- src/app/page.tsx | 6 ++- src/middleware.ts | 97 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 src/middleware.ts diff --git a/src/app/page.tsx b/src/app/page.tsx index dc36c4d..9d72157 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { createClient } from '@/lib/supabase/client'; import BottleGrid from "@/components/BottleGrid"; import AuthForm from "@/components/AuthForm"; @@ -11,7 +11,7 @@ import { useI18n } from "@/i18n/I18nContext"; import { useAuth } from "@/context/AuthContext"; import { useSession } from "@/context/SessionContext"; import TastingHub from "@/components/TastingHub"; -import { Sparkles, Loader2, Search, SlidersHorizontal } from "lucide-react"; +import { Sparkles, Loader2, Search, SlidersHorizontal, Settings, CircleUser } from "lucide-react"; import { BottomNavigation } from '@/components/BottomNavigation'; import ScanAndTasteFlow from '@/components/ScanAndTasteFlow'; import UserStatusBadge from '@/components/UserStatusBadge'; @@ -20,10 +20,12 @@ import SplitCard from '@/components/SplitCard'; import HeroBanner from '@/components/HeroBanner'; import QuickActionsGrid from '@/components/QuickActionsGrid'; import DramOfTheDay from '@/components/DramOfTheDay'; +import { checkIsAdmin } from '@/services/track-api-usage'; export default function Home() { const supabase = createClient(); const router = useRouter(); + const searchParams = useSearchParams(); const [bottles, setBottles] = useState([]); const { user, isLoading: isAuthLoading } = useAuth(); const [isInternalLoading, setIsInternalLoading] = useState(false); diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..19dd388 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,97 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { createServerClient } from "@supabase/ssr"; + +export async function middleware(request: NextRequest) { + let response = NextResponse.next({ + request: { + headers: request.headers, + }, + }); + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll(); + }, + setAll(cookiesToSet: any[]) { + cookiesToSet.forEach(({ name, value, options }) => + request.cookies.set(name, value) + ); + response = NextResponse.next({ + request: { + headers: request.headers, + }, + }); + cookiesToSet.forEach(({ name, value, options }) => + response.cookies.set(name, value, options) + ); + }, + }, + } + ); + + // Refresh session if expired - required for Server Components + const { + data: { user }, + } = await supabase.auth.getUser(); + + const path = request.nextUrl.pathname; + + // 1. Define Public Routes (Whitelist) + const isPublic = + path === "/" || + path.startsWith("/auth") || // Auth callbacks + path.startsWith("/api") || // API routes (often handle their own auth or are public) + path === "/manifest.webmanifest" || + path === "/sw.js" || + path === "/offline" || + path.startsWith("/icons") || + path.startsWith("/_next"); // Static assets + + // 2. Specialized Logic for /splits + // - Public: /splits/[slug] + // - Protected: /splits/create, /splits/manage + const isSplitsPublic = + path.startsWith("/splits/") && + !path.startsWith("/splits/create") && + !path.startsWith("/splits/manage"); + + if (isPublic || isSplitsPublic) { + return response; + } + + // 3. Protected Routes + // If no user, redirect to Home (which acts as Login) + if (!user) { + const redirectUrl = request.nextUrl.clone(); + redirectUrl.pathname = "/"; + // Add redirect param so Client can show Login Modal if needed? + // Or just let them land on Home. + // Ideally we'd persist the return URL: + redirectUrl.searchParams.set("redirect_to", path); + return NextResponse.redirect(redirectUrl); + } + + // 4. Admin Protection (Optional Layer in Middleware, but Page also checks) + // We rely on Page component for Admin role check to avoid DB hit in middleware if possible, + // OR we can trust getUser() + Page logic. + // Middleware mainly ensures *Authentication*. Authorization is Page level. + + return response; +} + +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) + * Feel free to modify this pattern to include more paths. + */ + "/((?!_next/static|_next/image|favicon.ico).*)", + ], +};