From 7de3c8ca62d02e8ad77c566f16db5f707aa55fe3 Mon Sep 17 00:00:00 2001 From: JCEEE <0xjceee@proton.me> Date: Wed, 30 Sep 2026 08:12:30 +0100 Subject: [PATCH] propose new chore update fix --- frontend/src/lib/server/pocketbase.ts | 47 +++++++++++++++++++-------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/frontend/src/lib/server/pocketbase.ts b/frontend/src/lib/server/pocketbase.ts index 14e3eb3..5a5af5a 100644 --- a/frontend/src/lib/server/pocketbase.ts +++ b/frontend/src/lib/server/pocketbase.ts @@ -30,9 +30,16 @@ export function pbUser(event: RequestEvent) { // Superuser PB client (memoized). Reserved for server-only privileged // operations that must bypass collection rules: creating child users, minting // OTP-login tokens, and verifying OTPs against the superuser-only otp. +// +// NOTE: the auth token expires (prod PB showed ~36h). A stale memoized client +// goes out UNAUTHENTICATED, and PocketBase answers unauthenticated getOne() +// calls with 404 "resource wasn't found" — which the toggle path misreads as +// CHORE_GONE for every chore. So always re-auth when the stored token is no +// longer valid instead of reusing a dead client. let superClient: PocketBase | null = null; export async function createSuperClient() { - if (superClient) return superClient; + if (superClient?.authStore.isValid) return superClient; + superClient = null; const pb = new PocketBase(PB_ENDPOINT); pb.autoCancellation(false); await pb.collection('_superusers').authWithPassword(String(PB_EMAIL), String(PB_PASSWORD)); @@ -40,29 +47,43 @@ export async function createSuperClient() { return pb; } +// Run a superuser op, retrying once with a freshly-authenticated client when +// the first attempt hits an auth-shaped failure. A locally-valid token can +// still be dead server-side (restart/revoked secret rotation), and PB +// surfaces that as 401/403 — or as 404 when a viewRule then denies access. +async function withSuperRetry(op: (pb: PocketBase) => Promise): Promise { + try { + return await op(await createSuperClient()); + } catch (e: any) { + const status = e?.status; + if (status === 401 || status === 403 || status === 404) { + superClient = null; + return await op(await createSuperClient()); + } + throw e; + } +} + // Superuser CRUD facade, built on the memoized SDK superuser client. All // server PB access (authenticated user + superuser) lives in this one module. export const pbAdmin = { async getList(collection: string, filter = '') { - const pb = await createSuperClient(); - const options: { filter?: string } = {}; - if (filter) options.filter = filter; - return pb.collection(collection).getFullList(options); + return withSuperRetry((pb) => { + const options: { filter?: string } = {}; + if (filter) options.filter = filter; + return pb.collection(collection).getFullList(options); + }); }, async getOne(collection: string, id: string) { - const pb = await createSuperClient(); - return pb.collection(collection).getOne(id); + return withSuperRetry((pb) => pb.collection(collection).getOne(id)); }, async create(collection: string, data: Record) { - const pb = await createSuperClient(); - return pb.collection(collection).create(data); + return withSuperRetry((pb) => pb.collection(collection).create(data)); }, async update(collection: string, id: string, data: Record) { - const pb = await createSuperClient(); - return pb.collection(collection).update(id, data); + return withSuperRetry((pb) => pb.collection(collection).update(id, data)); }, async remove(collection: string, id: string) { - const pb = await createSuperClient(); - return pb.collection(collection).delete(id); + return withSuperRetry((pb) => pb.collection(collection).delete(id)); } };