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

View File

@@ -0,0 +1,76 @@
// ABOUTME: Authentication middleware wrapper for protected API routes.
// ABOUTME: Provides withAuth HOF that validates session and injects user context.
import { cookies } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import type { User } from "@/types";
import {
createPocketBaseClient,
getCurrentUser,
isAuthenticated,
loadAuthFromCookies,
} from "./pocketbase";
/**
* Route handler function type that receives the authenticated user.
*/
export type AuthenticatedHandler<T = unknown> = (
request: NextRequest,
user: User,
context?: { params?: T },
) => Promise<NextResponse>;
/**
* Higher-order function that wraps an API route handler with authentication.
* Loads auth from cookies, validates the session, and passes the user to the handler.
*
* @param handler - The route handler that requires authentication
* @returns A wrapped handler that checks auth before calling the original handler
*
* @example
* ```ts
* export const GET = withAuth(async (request, user) => {
* return NextResponse.json({ email: user.email });
* });
* ```
*/
export function withAuth<T = unknown>(
handler: AuthenticatedHandler<T>,
): (request: NextRequest, context?: { params?: T }) => Promise<NextResponse> {
return async (
request: NextRequest,
context?: { params?: T },
): Promise<NextResponse> => {
try {
// Create a fresh PocketBase client for this request
const pb = createPocketBaseClient();
// Load auth state from cookies
const cookieStore = await cookies();
loadAuthFromCookies(pb, cookieStore);
// Check if the user is authenticated
if (!isAuthenticated(pb)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Get the current user
const user = getCurrentUser(pb);
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Call the original handler with the user context
return await handler(request, user, context);
} catch (error) {
console.error("Auth middleware error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
};
}