// 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 PocketBase from "pocketbase"; import type { User } from "@/types"; import { logger } from "./logger"; import { createPocketBaseClient, getCurrentUser, isAuthenticated, loadAuthFromCookies, } from "./pocketbase"; /** * Route handler function type that receives the authenticated user and PocketBase client. */ export type AuthenticatedHandler = ( request: NextRequest, user: User, pb: PocketBase, context?: { params?: T }, ) => Promise; /** * Higher-order function that wraps an API route handler with authentication. * Loads auth from cookies, validates the session, and passes the user and * authenticated PocketBase client 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, pb) => { * const data = await pb.collection("users").getOne(user.id); * return NextResponse.json({ email: user.email }); * }); * ``` */ /** * Extracts client IP address from request headers. * Checks x-forwarded-for and x-real-ip headers, returns "unknown" if neither present. */ function getClientIp(request: NextRequest): string { const forwardedFor = request.headers.get("x-forwarded-for"); if (forwardedFor) { // x-forwarded-for can contain multiple IPs; first one is the client return forwardedFor.split(",")[0].trim(); } const realIp = request.headers.get("x-real-ip"); if (realIp) { return realIp; } return "unknown"; } export function withAuth( handler: AuthenticatedHandler, ): (request: NextRequest, context?: { params?: T }) => Promise { return async ( request: NextRequest, context?: { params?: T }, ): Promise => { try { // Create a fresh PocketBase client for this request const pb = createPocketBaseClient(); // Load auth state from cookies const cookieStore = await cookies(); loadAuthFromCookies(pb, cookieStore); // Get client IP for logging const ip = getClientIp(request); // Check if the user is authenticated if (!isAuthenticated(pb)) { logger.warn({ reason: "not_authenticated", ip }, "Auth failure"); return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // Get the current user const user = getCurrentUser(pb); if (!user) { logger.warn({ reason: "user_not_found", ip }, "Auth failure"); return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // Call the original handler with the user context and authenticated pb client return await handler(request, user, pb, context); } catch (error) { logger.error({ err: error }, "Auth middleware error"); return NextResponse.json( { error: "Internal server error" }, { status: 500 }, ); } }; }