Fix 404 error when saving user preferences
Routes using withAuth were creating new unauthenticated PocketBase clients, causing 404 errors when trying to update records. Modified withAuth to pass the authenticated pb client to handlers so they can use it for database operations. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<NextResponse> {
|
||||
export async function POST(_request: Request): Promise<NextResponse> {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
|
||||
@@ -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(() => ({
|
||||
// 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);
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -13,17 +13,13 @@ let currentMockUser: User | null = null;
|
||||
const mockPbUpdate = vi.fn();
|
||||
const mockPbCreate = vi.fn();
|
||||
|
||||
vi.mock("@/lib/pocketbase", () => ({
|
||||
createPocketBaseClient: vi.fn(() => ({
|
||||
// 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);
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(() => ({
|
||||
// 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);
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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(() => ({
|
||||
// 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);
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -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<DailyLog>(page, limit, {
|
||||
|
||||
@@ -14,21 +14,8 @@ let lastUpdateCall: {
|
||||
data: { activeOverrides: OverrideType[] };
|
||||
} | null = null;
|
||||
|
||||
// Mock the auth-middleware module
|
||||
vi.mock("@/lib/auth-middleware", () => ({
|
||||
withAuth: vi.fn((handler) => {
|
||||
return async (request: NextRequest) => {
|
||||
if (!currentMockUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
return handler(request, currentMockUser);
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the pocketbase module
|
||||
vi.mock("@/lib/pocketbase", () => ({
|
||||
createPocketBaseClient: vi.fn(() => ({
|
||||
// Create mock PocketBase client
|
||||
const mockPb = {
|
||||
collection: vi.fn(() => ({
|
||||
update: vi.fn(
|
||||
async (id: string, data: { activeOverrides: OverrideType[] }) => {
|
||||
@@ -44,7 +31,18 @@ vi.mock("@/lib/pocketbase", () => ({
|
||||
},
|
||||
),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
// Mock the auth-middleware module
|
||||
vi.mock("@/lib/auth-middleware", () => ({
|
||||
withAuth: vi.fn((handler) => {
|
||||
return async (request: NextRequest) => {
|
||||
if (!currentMockUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
return handler(request, currentMockUser, mockPb);
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
import { DELETE, POST } from "./route";
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -12,9 +12,8 @@ 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(() => ({
|
||||
// Create mock PocketBase client
|
||||
const mockPb = {
|
||||
collection: vi.fn(() => ({
|
||||
getFirstListItem: vi.fn(async () => {
|
||||
if (!currentMockDailyLog) {
|
||||
@@ -25,11 +24,7 @@ vi.mock("@/lib/pocketbase", () => ({
|
||||
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);
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(() => ({
|
||||
// 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);
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<T = unknown> = (
|
||||
request: NextRequest,
|
||||
user: User,
|
||||
pb: PocketBase,
|
||||
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.
|
||||
* 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<T = unknown>(
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user