MEGAINFRA-WEBAPP/src/lib/pb.ts
DEls cb463c0faa feat(workforce): add timesheet exports and duplicate protection
Add per-site CSV and PDF matrix reports, editable sick-note date ranges, and weekend calendar styling. Preserve verified duplicate uploads for audit while excluding their entries from hours and costs, and prevent repeat files and overlapping daily imports.
2026-10-07 23:53:47 +02:00

120 lines
4.7 KiB
TypeScript

/**
* 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/<collection>/<record>/) 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<PocketBase> {
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";