Implement auth middleware for API routes (P0.2)

Add authentication infrastructure for protected routes:
- withAuth() wrapper for API route handlers (src/lib/auth-middleware.ts)
- Next.js middleware for page protection (src/middleware.ts)

withAuth() loads auth from cookies, validates session, and passes
user context to handlers. Returns 401 for unauthenticated requests.

Page middleware redirects unauthenticated users to /login, while
allowing public routes (/login), API routes (handled separately),
and static assets through.

Tests: 18 new tests (6 for withAuth, 12 for page middleware)
Total test count: 60 tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-10 18:43:19 +00:00
parent caed11a445
commit 76a46439b3
5 changed files with 463 additions and 9 deletions

75
src/middleware.ts Normal file
View File

@@ -0,0 +1,75 @@
// ABOUTME: Next.js middleware for page route protection.
// ABOUTME: Redirects unauthenticated users to login for protected pages.
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
/**
* Public paths that don't require authentication.
*/
const PUBLIC_PATHS = ["/login"];
/**
* Paths to skip middleware entirely (API routes, static assets).
*/
const SKIP_PATHS = ["/api", "/_next", "/favicon.ico"];
/**
* Checks if a request path should skip middleware.
*/
function shouldSkipMiddleware(pathname: string): boolean {
return SKIP_PATHS.some((path) => pathname.startsWith(path));
}
/**
* Checks if a request path is public (doesn't require auth).
*/
function isPublicPath(pathname: string): boolean {
return PUBLIC_PATHS.some((path) => pathname === path);
}
/**
* Next.js middleware function for page protection.
* Checks for pb_auth cookie and redirects to login if missing.
*/
export async function middleware(request: NextRequest): Promise<NextResponse> {
const { pathname } = request.nextUrl;
// Skip middleware for API routes and static assets
if (shouldSkipMiddleware(pathname)) {
return NextResponse.next();
}
// Allow public paths without authentication
if (isPublicPath(pathname)) {
return NextResponse.next();
}
// Check for authentication cookie
const authCookie = request.cookies.get("pb_auth");
// If no valid auth cookie, redirect to login
if (!authCookie || !authCookie.value) {
const loginUrl = new URL("/login", request.nextUrl.origin);
return NextResponse.redirect(loginUrl);
}
// User has auth cookie, allow through
return NextResponse.next();
}
/**
* Matcher configuration for Next.js middleware.
* Matches all paths except static files and public assets.
*/
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public folder
*/
"/((?!_next/static|_next/image|favicon.ico|public).*)",
],
};