Replace console.error with pino structured logger across API routes and add key event logging per observability spec: - Auth failure (warn): reason - Period logged (info): userId, date - Override toggled (info): userId, override, enabled - Decision calculated (info): userId, decision, reason - Error events (error): err object with stack trace Files updated: - auth-middleware.ts: Added structured logging for auth failures - cycle/period/route.ts: Added Period logged event + error logging - calendar/[userId]/[token].ics/route.ts: Replaced console.error - overrides/route.ts: Added Override toggled events - today/route.ts: Added Decision calculated event Tests: 720 passing (added 3 new structured logging tests) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
80 lines
2.4 KiB
TypeScript
80 lines
2.4 KiB
TypeScript
// 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 { logger } from "./logger";
|
|
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)) {
|
|
logger.warn({ reason: "not_authenticated" }, "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" }, "Auth failure");
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
// Call the original handler with the user context
|
|
return await handler(request, user, context);
|
|
} catch (error) {
|
|
logger.error({ err: error }, "Auth middleware error");
|
|
return NextResponse.json(
|
|
{ error: "Internal server error" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
};
|
|
}
|