2026-08-18 22:14:47 +00:00
|
|
|
/**
|
|
|
|
|
* 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";
|
2026-10-07 21:53:47 +00:00
|
|
|
import { billableTimesheetRequest } from "./timesheet-dedup";
|
2026-08-18 22:14:47 +00:00
|
|
|
|
|
|
|
|
export const PB_URL =
|
|
|
|
|
process.env.NEXT_PUBLIC_POCKETBASE_URL ?? "http://127.0.0.1:8090";
|
|
|
|
|
|
2026-08-26 21:05:05 +00:00
|
|
|
/**
|
|
|
|
|
* 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";
|
|
|
|
|
|
2026-08-18 22:14:47 +00:00
|
|
|
const AUTH_STORAGE_KEY = "mega-pb-auth";
|
|
|
|
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
/* Browser client */
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
|
|
|
|
export const pb = new PocketBase(PB_URL);
|
2026-10-07 21:53:47 +00:00
|
|
|
pb.beforeSend = billableTimesheetRequest;
|
2026-08-18 22:14:47 +00:00
|
|
|
|
|
|
|
|
// 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);
|
2026-10-07 21:53:47 +00:00
|
|
|
client.beforeSend = billableTimesheetRequest;
|
2026-08-24 16:39:15 +00:00
|
|
|
// 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;
|
2026-08-18 22:14:47 +00:00
|
|
|
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";
|