/** * PocketBase client for MEGA INFRASTRUCTURE. * * Two clients are exported: * - `pb` — browser/client-safe client (uses NEXT_PUBLIC_POCKETBASE_URL). * Reads its auth token from localStorage so the user * stays signed in across reloads. * - `pbAdmin` — server-only client authenticated as the superuser. * Uses POCKETBASE_ADMIN_EMAIL + POCKETBASE_ADMIN_PASSWORD * from the server environment and caches the token in * module scope. * * Always import from this file. Do not call `new Pocketbase(...)` elsewhere. */ import PocketBase from "pocketbase"; import { billableTimesheetRequest } from "./timesheet-dedup"; export const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL ?? "http://127.0.0.1:8090"; /** * Browser-reachable PocketBase base URL for fetching uploaded files. * PB file URLs (/api/files///) are served by the * same host the browser uses to talk to PB, so we derive this from * NEXT_PUBLIC_POCKETBASE_URL rather than hardcoding the prod hostname. * Falls back to the same localhost default as PB_URL so dev "just works". */ export const PB_BASE_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL ?? "http://127.0.0.1:8090"; const AUTH_STORAGE_KEY = "mega-pb-auth"; /* ------------------------------------------------------------------ */ /* Browser client */ /* ------------------------------------------------------------------ */ export const pb = new PocketBase(PB_URL); pb.beforeSend = billableTimesheetRequest; // Restore persisted auth on first access (browser only). if (typeof window !== "undefined") { try { const raw = window.localStorage.getItem(AUTH_STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw) as { token?: string; model?: unknown }; if (parsed.token) { pb.authStore.save(parsed.token, parsed.model as never); } } } catch { /* localStorage may be blocked — ignore */ } pb.authStore.onChange(() => { try { if (pb.authStore.isValid) { window.localStorage.setItem( AUTH_STORAGE_KEY, JSON.stringify({ token: pb.authStore.token, model: pb.authStore.model }) ); } else { window.localStorage.removeItem(AUTH_STORAGE_KEY); } } catch { /* ignore */ } }, true); } /* ------------------------------------------------------------------ */ /* Admin client (server-only) */ /* ------------------------------------------------------------------ */ let _adminClient: PocketBase | null = null; let _adminToken: string | null = null; export async function pbAdmin(): Promise { if (_adminClient && _adminToken) return _adminClient; const email = process.env.POCKETBASE_ADMIN_EMAIL; const password = process.env.POCKETBASE_ADMIN_PASSWORD; const presetToken = process.env.POCKETBASE_ADMIN_TOKEN; if (!email || !password) { throw new Error( "pbAdmin(): POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD are not set in the server environment." ); } const client = new PocketBase(PB_URL); client.beforeSend = billableTimesheetRequest; // Disable SDK auto-cancellation on the admin client. Auto-cancel is a // browser feature (cancels in-flight requests when the user navigates // away) keyed off (method + URL-path). On the server, the module-level // client cache means every concurrent route handler shares ONE client // instance, so the second concurrent request to the same endpoint // aborts the first one and we get cascading AbortError 500s - visible // as soon as anything fires parallel requests (e.g. multi-file upload, // or a Promise.all over PB collections). Server-side requests have // nothing to navigate away from, so disabling is safe. // // The field is `private` in the public .d.ts even though the runtime // supports it. The double-cast (object -> never -> our shape) is the // only way to write to a `private` field on a class outside that class // without `any`. (client as unknown as { enableAutoCancellation: boolean }).enableAutoCancellation = false; if (presetToken) { client.authStore.save(presetToken, null); } else { await client.collection("_superusers").authWithPassword(email, password); } _adminClient = client; _adminToken = client.authStore.token; return client; } /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ export type { RecordModel } from "pocketbase";