diff --git a/src/app/api/cron/garmin-sync/route.test.ts b/src/app/api/cron/garmin-sync/route.test.ts index d92e636..a978f91 100644 --- a/src/app/api/cron/garmin-sync/route.test.ts +++ b/src/app/api/cron/garmin-sync/route.test.ts @@ -23,6 +23,7 @@ vi.mock("@/lib/pocketbase", () => ({ }), create: mockPbCreate, update: mockPbUpdate, + authWithPassword: vi.fn().mockResolvedValue({ token: "admin-token" }), })), })), })); @@ -133,6 +134,8 @@ describe("POST /api/cron/garmin-sync", () => { mockDaysUntilExpiry.mockReturnValue(30); // Default to 30 days remaining mockSendTokenExpirationWarning.mockResolvedValue(undefined); // Reset mock implementation process.env.CRON_SECRET = validSecret; + process.env.POCKETBASE_ADMIN_EMAIL = "admin@test.com"; + process.env.POCKETBASE_ADMIN_PASSWORD = "test-password"; }); describe("Authentication", () => { @@ -159,6 +162,26 @@ describe("POST /api/cron/garmin-sync", () => { expect(response.status).toBe(401); }); + + it("returns 500 when POCKETBASE_ADMIN_EMAIL is not set", async () => { + process.env.POCKETBASE_ADMIN_EMAIL = ""; + + const response = await POST(createMockRequest(`Bearer ${validSecret}`)); + + expect(response.status).toBe(500); + const body = await response.json(); + expect(body.error).toBe("Server misconfiguration"); + }); + + it("returns 500 when POCKETBASE_ADMIN_PASSWORD is not set", async () => { + process.env.POCKETBASE_ADMIN_PASSWORD = ""; + + const response = await POST(createMockRequest(`Bearer ${validSecret}`)); + + expect(response.status).toBe(500); + const body = await response.json(); + expect(body.error).toBe("Server misconfiguration"); + }); }); describe("User fetching", () => { diff --git a/src/app/api/cron/garmin-sync/route.ts b/src/app/api/cron/garmin-sync/route.ts index 930c52d..3410da1 100644 --- a/src/app/api/cron/garmin-sync/route.ts +++ b/src/app/api/cron/garmin-sync/route.ts @@ -58,6 +58,32 @@ export async function POST(request: Request) { const pb = createPocketBaseClient(); + // Authenticate as admin to bypass API rules and list all users + const adminEmail = process.env.POCKETBASE_ADMIN_EMAIL; + const adminPassword = process.env.POCKETBASE_ADMIN_PASSWORD; + if (!adminEmail || !adminPassword) { + logger.error("Missing POCKETBASE_ADMIN_EMAIL or POCKETBASE_ADMIN_PASSWORD"); + return NextResponse.json( + { error: "Server misconfiguration" }, + { status: 500 }, + ); + } + + try { + await pb + .collection("_superusers") + .authWithPassword(adminEmail, adminPassword); + } catch (authError) { + logger.error( + { err: authError }, + "Failed to authenticate as PocketBase admin", + ); + return NextResponse.json( + { error: "Database authentication failed" }, + { status: 500 }, + ); + } + // Fetch all users (we'll filter garminConnected in code to avoid PocketBase query syntax issues) // Also filter out users without required date fields (garminTokenExpiresAt, lastPeriodDate) const allUsers = await pb.collection("users").getFullList();