Move auth protection to middleware and server layout

Replace client-side session checks (useSession + router.replace) with
a two-layer server-side approach: Next.js middleware performs a fast
cookie presence check for all protected routes, and the (pages) layout
does a full server-side session validation before rendering.

Move login/register into a dedicated (auth) route group with its own
minimal layout so they sit outside the authenticated shell. Lift the
Toaster to the root layout so it remains available across all route
groups.
This commit is contained in:
2026-03-02 14:38:54 -05:00
parent c47fe97733
commit f94ceaf233
9 changed files with 39 additions and 23 deletions
+7
View File
@@ -0,0 +1,7 @@
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return <main className="min-h-screen">{children}</main>;
}
+3 -10
View File
@@ -8,7 +8,6 @@ import {
import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { DashboardSkeleton } from "@/components/skeletons";
import { StatsSummary } from "@/components/stats-summary";
@@ -72,8 +71,7 @@ const sectionVariants = {
};
export default function DashboardPage() {
const { data: session, isPending } = useSession();
const router = useRouter();
const { data: session } = useSession();
const [continueWatching, setContinueWatching] = useState<
ContinueWatchingItem[]
>([]);
@@ -98,15 +96,10 @@ export default function DashboardPage() {
}, []);
useEffect(() => {
if (isPending) return;
if (!session?.user) {
router.replace("/login");
return;
}
fetchFeeds();
}, [session, isPending, router, fetchFeeds]);
}, [fetchFeeds]);
if (isPending || loading) {
if (loading) {
return <DashboardSkeleton />;
}
+7 -5
View File
@@ -1,17 +1,20 @@
"use client";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { CommandPalette } from "@/components/command-palette";
import { KeyboardHelpDialog } from "@/components/keyboard-help-dialog";
import { KeyboardProvider } from "@/components/keyboard-provider";
import { MobileTabBar } from "@/components/mobile-tab-bar";
import { NavBar } from "@/components/nav-bar";
import { Toaster } from "@/components/ui/sonner";
import { auth } from "@/lib/auth/server";
export default function PagesLayout({
export default async function PagesLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) redirect("/login");
return (
<KeyboardProvider>
<div className="min-h-screen overflow-x-hidden pb-14 sm:pb-0">
@@ -25,7 +28,6 @@ export default function PagesLayout({
<MobileTabBar />
<CommandPalette />
<KeyboardHelpDialog />
<Toaster position="bottom-right" />
</KeyboardProvider>
);
}
+2 -6
View File
@@ -354,17 +354,13 @@ export default function SettingsPage() {
useEffect(() => {
if (isPending) return;
if (!session?.user) {
router.replace("/login");
return;
}
fetchWebhooks();
if (session.user.role === "admin") {
if (session?.user?.role === "admin") {
fetchSettings();
} else {
setLoadingSettings(false);
}
}, [session, isPending, router, fetchSettings, fetchWebhooks]);
}, [session, isPending, fetchSettings, fetchWebhooks]);
async function handleToggleRegistration(checked: boolean) {
setToggling(true);
+2 -2
View File
@@ -65,7 +65,7 @@
--secondary-foreground: oklch(0.85 0.02 80);
--muted: oklch(0.22 0.008 55);
--muted-foreground: oklch(0.63 0.02 80);
--accent: oklch(0.25 0.010 55);
--accent: oklch(0.25 0.01 55);
--accent-foreground: oklch(0.93 0.015 80);
--destructive: oklch(0.65 0.2 25);
--border: oklch(1 0.015 65 / 10%);
@@ -175,7 +175,7 @@ body::before {
[data-slot="skeleton"] {
animation: warm-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
background: oklch(0.25 0.010 55);
background: oklch(0.25 0.01 55);
}
/* Scrollbar styling */
+2
View File
@@ -3,6 +3,7 @@ import { DM_Sans, DM_Serif_Display, Geist_Mono } from "next/font/google";
import { TooltipProvider } from "@/components/ui/tooltip";
import "./globals.css";
import { Toaster } from "@/components/ui/sonner";
const dmSans = DM_Sans({
variable: "--font-dm-sans",
@@ -36,6 +37,7 @@ export default function RootLayout({
className={`${dmSans.variable} ${dmSerif.variable} ${geistMono.variable} font-sans antialiased`}
>
<TooltipProvider>{children}</TooltipProvider>
<Toaster position="bottom-right" />
</body>
</html>
);
+16
View File
@@ -0,0 +1,16 @@
import { getSessionCookie } from "better-auth/cookies";
import { type NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const sessionCookie = getSessionCookie(request);
if (!sessionCookie) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard", "/explore", "/settings", "/setup", "/titles/:path*"],
};