Lesson 26 +10 XP

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: true cookies using cookies().
  • Wrap protected areas in shared nested layouts (app/portal/layout.tsx).
  • Provide dedicated error.tsx boundaries inside protected sections to isolate crashes gracefully.