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.
120 lines
4.7 KiB
TypeScript
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";
|