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
This commit is contained in:
2026-01-19 12:36:35 +01:00
parent bb9a78f755
commit 2bf0ac0f3e
2 changed files with 101 additions and 2 deletions

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { createClient } from '@/lib/supabase/client'; import { createClient } from '@/lib/supabase/client';
import BottleGrid from "@/components/BottleGrid"; import BottleGrid from "@/components/BottleGrid";
import AuthForm from "@/components/AuthForm"; import AuthForm from "@/components/AuthForm";
@@ -11,7 +11,7 @@ import { useI18n } from "@/i18n/I18nContext";
import { useAuth } from "@/context/AuthContext"; import { useAuth } from "@/context/AuthContext";
import { useSession } from "@/context/SessionContext"; import { useSession } from "@/context/SessionContext";
import TastingHub from "@/components/TastingHub"; 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 { BottomNavigation } from '@/components/BottomNavigation';
import ScanAndTasteFlow from '@/components/ScanAndTasteFlow'; import ScanAndTasteFlow from '@/components/ScanAndTasteFlow';
import UserStatusBadge from '@/components/UserStatusBadge'; import UserStatusBadge from '@/components/UserStatusBadge';
@@ -20,10 +20,12 @@ import SplitCard from '@/components/SplitCard';
import HeroBanner from '@/components/HeroBanner'; import HeroBanner from '@/components/HeroBanner';
import QuickActionsGrid from '@/components/QuickActionsGrid'; import QuickActionsGrid from '@/components/QuickActionsGrid';
import DramOfTheDay from '@/components/DramOfTheDay'; import DramOfTheDay from '@/components/DramOfTheDay';
import { checkIsAdmin } from '@/services/track-api-usage';
export default function Home() { export default function Home() {
const supabase = createClient(); const supabase = createClient();
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams();
const [bottles, setBottles] = useState<any[]>([]); const [bottles, setBottles] = useState<any[]>([]);
const { user, isLoading: isAuthLoading } = useAuth(); const { user, isLoading: isAuthLoading } = useAuth();
const [isInternalLoading, setIsInternalLoading] = useState(false); const [isInternalLoading, setIsInternalLoading] = useState(false);

97
src/middleware.ts Normal file
View File

@@ -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).*)",
],
};