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:
@@ -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
97
src/middleware.ts
Normal 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).*)",
|
||||||
|
],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user