Loading lessons...
Project: Protected SaaS Portal with Middleware & Cookies
Project: Protected SaaS Portal with Middleware & Cookies
In this capstone project, you will build a production-grade SaaS Portal featuring Edge Middleware route protection, Secure HttpOnly Cookie management, and Custom Error & 404 Boundaries.
Complete SaaS Architecture
src/
├── middleware.ts -> Protects /portal/* routes & checks session cookie
├── app/
│ ├── layout.tsx -> Root Layout
│ ├── not-found.tsx -> Custom 404 page
│ ├── (auth)/
│ │ └── login/page.tsx -> Login Page with Server Action
│ └── portal/
│ ├── layout.tsx -> Protected SaaS Portal Layout
│ ├── page.tsx -> SaaS Overview Page
│ └── error.tsx -> Client Error Boundary
└── lib/
└── session.ts -> Secure Cookie Helper
1. Root Middleware Authentication Guard (middleware.ts)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('saas_token')?.value;
const { pathname } = request.nextUrl;
// Protect all /portal routes!
if (pathname.startsWith('/portal') && !token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(loginUrl);
}
// Redirect authenticated user away from login page
if (pathname === '/login' && token) {
return NextResponse.redirect(new URL('/portal', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/portal/:path*', '/login'],
};
2. Session Cookie Actions (lib/session.ts)
'use server';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
export async function loginServerAction(formData: FormData) {
const email = formData.get('email') as string;
const password = formData.get('password') as string;
if (email === 'user@saas.com' && password === 'password123') {
const cookieStore = await cookies();
cookieStore.set('saas_token', 'jwt_session_token_xyz987', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24, // 24 Hours
});
redirect('/portal');
}
return { error: 'Invalid credentials' };
}
export async function logoutServerAction() {
const cookieStore = await cookies();
cookieStore.delete('saas_token');
redirect('/login');
}
3. Protected Portal Layout (app/portal/layout.tsx)
import { cookies } from 'next/headers';
import { logoutServerAction } from '@/lib/session';
export default async function PortalLayout({ children }: { children: React.ReactNode }) {
const cookieStore = await cookies();
const token = cookieStore.get('saas_token')?.value;
return (
<div className="flex min-h-screen">
<aside className="w-64 bg-slate-900 text-white p-6 flex flex-col justify-between">
<div>
<h2 className="text-xl font-bold">SaaS Portal</h2>
<nav className="mt-6 space-y-2">
<a href="/portal" className="block py-2 hover:text-indigo-400">Dashboard</a>
<a href="/portal/billing" className="block py-2 hover:text-indigo-400">Billing</a>
</nav>
</div>
<form action={logoutServerAction}>
<button type="submit" className="w-full bg-red-600 text-white py-2 rounded">
Log Out
</button>
</form>
</aside>
<main className="flex-1 p-8 bg-slate-50">{children}</main>
</div>
);
}
TL;DR
- Secure SaaS portals by protecting routes at the Edge with
middleware.ts. - Store authentication tokens inside secure
httpOnly: truecookies usingcookies(). - Wrap protected areas in shared nested layouts (
app/portal/layout.tsx). - Provide dedicated
error.tsxboundaries inside protected sections to isolate crashes gracefully.