diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts index f1849b8..89284b0 100644 --- a/src/app/api/auth/logout/route.ts +++ b/src/app/api/auth/logout/route.ts @@ -12,7 +12,7 @@ import { logger } from "@/lib/logger"; * Clears the user's authentication session by deleting the pb_auth cookie. * Returns a success response with redirect URL. */ -export async function POST(): Promise { +export async function POST(_request: Request): Promise { try { const cookieStore = await cookies(); diff --git a/src/app/api/calendar/regenerate-token/route.test.ts b/src/app/api/calendar/regenerate-token/route.test.ts index 6a69c2e..99866b3 100644 --- a/src/app/api/calendar/regenerate-token/route.test.ts +++ b/src/app/api/calendar/regenerate-token/route.test.ts @@ -12,14 +12,12 @@ let currentMockUser: User | null = null; // Track PocketBase update calls const mockPbUpdate = vi.fn().mockResolvedValue({}); -// Mock PocketBase -vi.mock("@/lib/pocketbase", () => ({ - createPocketBaseClient: vi.fn(() => ({ - collection: vi.fn(() => ({ - update: mockPbUpdate, - })), +// Create mock PocketBase client +const mockPb = { + collection: vi.fn(() => ({ + update: mockPbUpdate, })), -})); +}; // Mock the auth-middleware module vi.mock("@/lib/auth-middleware", () => ({ @@ -28,7 +26,7 @@ vi.mock("@/lib/auth-middleware", () => ({ if (!currentMockUser) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - return handler(request, currentMockUser); + return handler(request, currentMockUser, mockPb); }; }), })); diff --git a/src/app/api/calendar/regenerate-token/route.ts b/src/app/api/calendar/regenerate-token/route.ts index 6ce4769..dcfea1a 100644 --- a/src/app/api/calendar/regenerate-token/route.ts +++ b/src/app/api/calendar/regenerate-token/route.ts @@ -5,7 +5,6 @@ import { randomBytes } from "node:crypto"; import { NextResponse } from "next/server"; import { withAuth } from "@/lib/auth-middleware"; -import { createPocketBaseClient } from "@/lib/pocketbase"; /** * Generates a cryptographically secure random 32-character alphanumeric token. @@ -17,12 +16,11 @@ function generateToken(): string { return randomBytes(32).toString("hex").slice(0, 32); } -export const POST = withAuth(async (_request, user) => { +export const POST = withAuth(async (_request, user, pb) => { // Generate new random token const newToken = generateToken(); // Update user record with new token - const pb = createPocketBaseClient(); await pb.collection("users").update(user.id, { calendarToken: newToken, }); diff --git a/src/app/api/cycle/period/route.test.ts b/src/app/api/cycle/period/route.test.ts index 74ee215..6ae28ef 100644 --- a/src/app/api/cycle/period/route.test.ts +++ b/src/app/api/cycle/period/route.test.ts @@ -13,17 +13,13 @@ let currentMockUser: User | null = null; const mockPbUpdate = vi.fn(); const mockPbCreate = vi.fn(); -vi.mock("@/lib/pocketbase", () => ({ - createPocketBaseClient: vi.fn(() => ({ - collection: vi.fn((_name: string) => ({ - update: mockPbUpdate, - create: mockPbCreate, - })), +// Create mock PocketBase client +const mockPb = { + collection: vi.fn((_name: string) => ({ + update: mockPbUpdate, + create: mockPbCreate, })), - loadAuthFromCookies: vi.fn(), - isAuthenticated: vi.fn(() => currentMockUser !== null), - getCurrentUser: vi.fn(() => currentMockUser), -})); +}; // Mock the auth-middleware module vi.mock("@/lib/auth-middleware", () => ({ @@ -32,7 +28,7 @@ vi.mock("@/lib/auth-middleware", () => ({ if (!currentMockUser) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - return handler(request, currentMockUser); + return handler(request, currentMockUser, mockPb); }; }), })); diff --git a/src/app/api/cycle/period/route.ts b/src/app/api/cycle/period/route.ts index 38f8090..3654d59 100644 --- a/src/app/api/cycle/period/route.ts +++ b/src/app/api/cycle/period/route.ts @@ -6,7 +6,6 @@ import { NextResponse } from "next/server"; import { withAuth } from "@/lib/auth-middleware"; import { getCycleDay, getPhase } from "@/lib/cycle"; import { logger } from "@/lib/logger"; -import { createPocketBaseClient } from "@/lib/pocketbase"; interface PeriodLogRequest { startDate?: string; @@ -35,7 +34,7 @@ function isFutureDate(dateStr: string): boolean { return inputDate > today; } -export const POST = withAuth(async (request: NextRequest, user) => { +export const POST = withAuth(async (request: NextRequest, user, pb) => { try { const body = (await request.json()) as PeriodLogRequest; @@ -63,8 +62,6 @@ export const POST = withAuth(async (request: NextRequest, user) => { ); } - const pb = createPocketBaseClient(); - // Calculate predicted date based on previous cycle (if exists) let predictedDateStr: string | null = null; if (user.lastPeriodDate) { diff --git a/src/app/api/garmin/tokens/route.test.ts b/src/app/api/garmin/tokens/route.test.ts index fbf6e74..3dabc7c 100644 --- a/src/app/api/garmin/tokens/route.test.ts +++ b/src/app/api/garmin/tokens/route.test.ts @@ -12,14 +12,12 @@ let currentMockUser: User | null = null; // Track PocketBase update calls const mockPbUpdate = vi.fn().mockResolvedValue({}); -// Mock PocketBase -vi.mock("@/lib/pocketbase", () => ({ - createPocketBaseClient: vi.fn(() => ({ - collection: vi.fn(() => ({ - update: mockPbUpdate, - })), +// Create mock PocketBase client +const mockPb = { + collection: vi.fn(() => ({ + update: mockPbUpdate, })), -})); +}; // Track encryption calls const mockEncrypt = vi.fn((plaintext: string) => `encrypted:${plaintext}`); @@ -36,7 +34,7 @@ vi.mock("@/lib/auth-middleware", () => ({ if (!currentMockUser) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - return handler(request, currentMockUser); + return handler(request, currentMockUser, mockPb); }; }), })); diff --git a/src/app/api/garmin/tokens/route.ts b/src/app/api/garmin/tokens/route.ts index b5fb6dd..a773915 100644 --- a/src/app/api/garmin/tokens/route.ts +++ b/src/app/api/garmin/tokens/route.ts @@ -5,9 +5,8 @@ import { NextResponse } from "next/server"; import { withAuth } from "@/lib/auth-middleware"; import { encrypt } from "@/lib/encryption"; import { daysUntilExpiry } from "@/lib/garmin"; -import { createPocketBaseClient } from "@/lib/pocketbase"; -export const POST = withAuth(async (request, user) => { +export const POST = withAuth(async (request, user, pb) => { const body = await request.json(); const { oauth1, oauth2, expires_at } = body; @@ -57,7 +56,6 @@ export const POST = withAuth(async (request, user) => { const encryptedOauth2 = encrypt(JSON.stringify(oauth2)); // Update user record - const pb = createPocketBaseClient(); await pb.collection("users").update(user.id, { garminOauth1Token: encryptedOauth1, garminOauth2Token: encryptedOauth2, @@ -79,9 +77,7 @@ export const POST = withAuth(async (request, user) => { }); }); -export const DELETE = withAuth(async (_request, user) => { - const pb = createPocketBaseClient(); - +export const DELETE = withAuth(async (_request, user, pb) => { await pb.collection("users").update(user.id, { garminOauth1Token: "", garminOauth2Token: "", diff --git a/src/app/api/history/route.test.ts b/src/app/api/history/route.test.ts index 0016393..eac9b01 100644 --- a/src/app/api/history/route.test.ts +++ b/src/app/api/history/route.test.ts @@ -12,14 +12,12 @@ let currentMockUser: User | null = null; // Track PocketBase collection calls const mockGetList = vi.fn(); -// Mock PocketBase -vi.mock("@/lib/pocketbase", () => ({ - createPocketBaseClient: vi.fn(() => ({ - collection: vi.fn(() => ({ - getList: mockGetList, - })), +// Create mock PocketBase client +const mockPb = { + collection: vi.fn(() => ({ + getList: mockGetList, })), -})); +}; // Mock the auth-middleware module vi.mock("@/lib/auth-middleware", () => ({ @@ -28,7 +26,7 @@ vi.mock("@/lib/auth-middleware", () => ({ if (!currentMockUser) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - return handler(request, currentMockUser); + return handler(request, currentMockUser, mockPb); }; }), })); diff --git a/src/app/api/history/route.ts b/src/app/api/history/route.ts index a7fba8f..88d7c66 100644 --- a/src/app/api/history/route.ts +++ b/src/app/api/history/route.ts @@ -3,7 +3,6 @@ import { NextResponse } from "next/server"; import { withAuth } from "@/lib/auth-middleware"; -import { createPocketBaseClient } from "@/lib/pocketbase"; import type { DailyLog } from "@/types"; // Validation constants @@ -24,7 +23,7 @@ function isValidDateFormat(dateStr: string): boolean { return !Number.isNaN(date.getTime()); } -export const GET = withAuth(async (request, user) => { +export const GET = withAuth(async (request, user, pb) => { const { searchParams } = request.nextUrl; // Parse and validate page parameter @@ -77,7 +76,6 @@ export const GET = withAuth(async (request, user) => { const filter = filters.join(" && "); // Query PocketBase - const pb = createPocketBaseClient(); const result = await pb .collection("dailyLogs") .getList(page, limit, { diff --git a/src/app/api/overrides/route.test.ts b/src/app/api/overrides/route.test.ts index 05c8f8e..3266548 100644 --- a/src/app/api/overrides/route.test.ts +++ b/src/app/api/overrides/route.test.ts @@ -14,6 +14,25 @@ let lastUpdateCall: { data: { activeOverrides: OverrideType[] }; } | null = null; +// Create mock PocketBase client +const mockPb = { + collection: vi.fn(() => ({ + update: vi.fn( + async (id: string, data: { activeOverrides: OverrideType[] }) => { + lastUpdateCall = { id, data }; + // Update the mock user to simulate DB update + if (currentMockUser) { + currentMockUser = { + ...currentMockUser, + activeOverrides: data.activeOverrides, + }; + } + return { ...currentMockUser, ...data }; + }, + ), + })), +}; + // Mock the auth-middleware module vi.mock("@/lib/auth-middleware", () => ({ withAuth: vi.fn((handler) => { @@ -21,32 +40,11 @@ vi.mock("@/lib/auth-middleware", () => ({ if (!currentMockUser) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - return handler(request, currentMockUser); + return handler(request, currentMockUser, mockPb); }; }), })); -// Mock the pocketbase module -vi.mock("@/lib/pocketbase", () => ({ - createPocketBaseClient: vi.fn(() => ({ - collection: vi.fn(() => ({ - update: vi.fn( - async (id: string, data: { activeOverrides: OverrideType[] }) => { - lastUpdateCall = { id, data }; - // Update the mock user to simulate DB update - if (currentMockUser) { - currentMockUser = { - ...currentMockUser, - activeOverrides: data.activeOverrides, - }; - } - return { ...currentMockUser, ...data }; - }, - ), - })), - })), -})); - import { DELETE, POST } from "./route"; describe("POST /api/overrides", () => { diff --git a/src/app/api/overrides/route.ts b/src/app/api/overrides/route.ts index 0af9f49..4aeb421 100644 --- a/src/app/api/overrides/route.ts +++ b/src/app/api/overrides/route.ts @@ -5,7 +5,6 @@ import { NextResponse } from "next/server"; import { withAuth } from "@/lib/auth-middleware"; import { logger } from "@/lib/logger"; -import { createPocketBaseClient } from "@/lib/pocketbase"; import type { OverrideType } from "@/types"; const VALID_OVERRIDE_TYPES: OverrideType[] = [ @@ -27,7 +26,7 @@ function isValidOverrideType(value: unknown): value is OverrideType { * Request body: { override: OverrideType } * Response: { activeOverrides: OverrideType[] } */ -export const POST = withAuth(async (request: NextRequest, user) => { +export const POST = withAuth(async (request: NextRequest, user, pb) => { const body = await request.json(); if (!body.override) { @@ -55,7 +54,6 @@ export const POST = withAuth(async (request: NextRequest, user) => { : [...currentOverrides, overrideToAdd]; // Update the user record in PocketBase - const pb = createPocketBaseClient(); await pb .collection("users") .update(user.id, { activeOverrides: newOverrides }); @@ -74,7 +72,7 @@ export const POST = withAuth(async (request: NextRequest, user) => { * Request body: { override: OverrideType } * Response: { activeOverrides: OverrideType[] } */ -export const DELETE = withAuth(async (request: NextRequest, user) => { +export const DELETE = withAuth(async (request: NextRequest, user, pb) => { const body = await request.json(); if (!body.override) { @@ -100,7 +98,6 @@ export const DELETE = withAuth(async (request: NextRequest, user) => { const newOverrides = currentOverrides.filter((o) => o !== overrideToRemove); // Update the user record in PocketBase - const pb = createPocketBaseClient(); await pb .collection("users") .update(user.id, { activeOverrides: newOverrides }); diff --git a/src/app/api/today/route.test.ts b/src/app/api/today/route.test.ts index 9a21550..52c266f 100644 --- a/src/app/api/today/route.test.ts +++ b/src/app/api/today/route.test.ts @@ -12,24 +12,19 @@ let currentMockUser: User | null = null; // Module-level variable to control mock daily log in tests let currentMockDailyLog: DailyLog | null = null; -// Mock PocketBase client for database operations -vi.mock("@/lib/pocketbase", () => ({ - createPocketBaseClient: vi.fn(() => ({ - collection: vi.fn(() => ({ - getFirstListItem: vi.fn(async () => { - if (!currentMockDailyLog) { - const error = new Error("No DailyLog found"); - (error as { status?: number }).status = 404; - throw error; - } - return currentMockDailyLog; - }), - })), +// Create mock PocketBase client +const mockPb = { + collection: vi.fn(() => ({ + getFirstListItem: vi.fn(async () => { + if (!currentMockDailyLog) { + const error = new Error("No DailyLog found"); + (error as { status?: number }).status = 404; + throw error; + } + return currentMockDailyLog; + }), })), - loadAuthFromCookies: vi.fn(), - isAuthenticated: vi.fn(() => currentMockUser !== null), - getCurrentUser: vi.fn(() => currentMockUser), -})); +}; // Mock the auth-middleware module vi.mock("@/lib/auth-middleware", () => ({ @@ -38,7 +33,7 @@ vi.mock("@/lib/auth-middleware", () => ({ if (!currentMockUser) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - return handler(request, currentMockUser); + return handler(request, currentMockUser, mockPb); }; }), })); diff --git a/src/app/api/today/route.ts b/src/app/api/today/route.ts index 17567f6..8f36416 100644 --- a/src/app/api/today/route.ts +++ b/src/app/api/today/route.ts @@ -12,7 +12,6 @@ import { import { getDecisionWithOverrides } from "@/lib/decision-engine"; import { logger } from "@/lib/logger"; import { getNutritionGuidance } from "@/lib/nutrition"; -import { createPocketBaseClient } from "@/lib/pocketbase"; import type { DailyData, DailyLog, HrvStatus } from "@/types"; // Default biometrics when no Garmin data is available @@ -28,7 +27,7 @@ const DEFAULT_BIOMETRICS: { weekIntensityMinutes: 0, }; -export const GET = withAuth(async (_request, user) => { +export const GET = withAuth(async (_request, user, pb) => { // Validate required user data if (!user.lastPeriodDate) { return NextResponse.json( @@ -70,7 +69,6 @@ export const GET = withAuth(async (_request, user) => { // Try to fetch today's DailyLog for biometrics let biometrics = { ...DEFAULT_BIOMETRICS, phaseLimit }; try { - const pb = createPocketBaseClient(); const today = new Date().toISOString().split("T")[0]; const dailyLog = await pb .collection("dailyLogs") diff --git a/src/app/api/user/route.test.ts b/src/app/api/user/route.test.ts index a672927..60b8d0f 100644 --- a/src/app/api/user/route.test.ts +++ b/src/app/api/user/route.test.ts @@ -12,14 +12,12 @@ let currentMockUser: User | null = null; // Track PocketBase update calls const mockPbUpdate = vi.fn().mockResolvedValue({}); -// Mock PocketBase -vi.mock("@/lib/pocketbase", () => ({ - createPocketBaseClient: vi.fn(() => ({ - collection: vi.fn(() => ({ - update: mockPbUpdate, - })), +// Create mock PocketBase client +const mockPb = { + collection: vi.fn(() => ({ + update: mockPbUpdate, })), -})); +}; // Mock the auth-middleware module vi.mock("@/lib/auth-middleware", () => ({ @@ -28,7 +26,7 @@ vi.mock("@/lib/auth-middleware", () => ({ if (!currentMockUser) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - return handler(request, currentMockUser); + return handler(request, currentMockUser, mockPb); }; }), })); diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index 0e10274..0a56fa7 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -4,7 +4,6 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { withAuth } from "@/lib/auth-middleware"; -import { createPocketBaseClient } from "@/lib/pocketbase"; // Validation constants const CYCLE_LENGTH_MIN = 21; @@ -16,7 +15,7 @@ const TIME_FORMAT_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/; * Returns the authenticated user's profile. * Excludes sensitive fields like encrypted tokens. */ -export const GET = withAuth(async (_request, user) => { +export const GET = withAuth(async (_request, user, _pb) => { // Format date for consistent API response const lastPeriodDate = user.lastPeriodDate ? user.lastPeriodDate.toISOString().split("T")[0] @@ -81,7 +80,7 @@ function validateTimezone(value: unknown): string | null { * Updates the authenticated user's profile. * Allowed fields: cycleLength, notificationTime, timezone */ -export const PATCH = withAuth(async (request: NextRequest, user) => { +export const PATCH = withAuth(async (request: NextRequest, user, pb) => { const body = await request.json(); // Build update object with only valid, updatable fields @@ -132,7 +131,6 @@ export const PATCH = withAuth(async (request: NextRequest, user) => { } // Update the user record in PocketBase - const pb = createPocketBaseClient(); await pb.collection("users").update(user.id, updates); // Build updated user profile for response diff --git a/src/lib/auth-middleware.test.ts b/src/lib/auth-middleware.test.ts index 996d2d0..74ac96c 100644 --- a/src/lib/auth-middleware.test.ts +++ b/src/lib/auth-middleware.test.ts @@ -113,7 +113,12 @@ describe("withAuth", () => { const response = await wrappedHandler(mockRequest); expect(response.status).toBe(200); - expect(handler).toHaveBeenCalledWith(mockRequest, mockUser, undefined); + expect(handler).toHaveBeenCalledWith( + mockRequest, + mockUser, + mockPbClient, + undefined, + ); }); it("loads auth from cookies before checking authentication", async () => { @@ -159,7 +164,7 @@ describe("withAuth", () => { await wrappedHandler(mockRequest, { params: mockParams }); - expect(handler).toHaveBeenCalledWith(mockRequest, mockUser, { + expect(handler).toHaveBeenCalledWith(mockRequest, mockUser, mockPbClient, { params: mockParams, }); }); diff --git a/src/lib/auth-middleware.ts b/src/lib/auth-middleware.ts index 251af17..6f9c305 100644 --- a/src/lib/auth-middleware.ts +++ b/src/lib/auth-middleware.ts @@ -4,6 +4,7 @@ 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"; @@ -16,24 +17,27 @@ import { } from "./pocketbase"; /** - * Route handler function type that receives the authenticated user. + * 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 to the handler. + * 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) => { + * export const GET = withAuth(async (request, user, pb) => { + * const data = await pb.collection("users").getOne(user.id); * return NextResponse.json({ email: user.email }); * }); * ``` @@ -66,8 +70,8 @@ export function withAuth( return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - // Call the original handler with the user context - return await handler(request, user, context); + // 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(