import { ref, computed } from "vue"; // The panel authenticates exactly like any other DriverVault client: it posts to // /api/auth/login and keeps the PocketBase token the API Server relays back. The // panel never talks to PocketBase directly — it doesn't even know its address. const TOKEN_KEY = "dh-panel-token"; function storedToken() { try { return localStorage.getItem(TOKEN_KEY) || ""; } catch { return ""; // private mode } } export const token = ref(storedToken()); export const me = ref(null); export const isSuperadmin = computed(() => me.value?.role === "superadmin"); export const isManager = computed( () => me.value?.role === "admin" || me.value?.role === "superadmin", ); function setToken(value) { token.value = value; try { if (value) localStorage.setItem(TOKEN_KEY, value); else localStorage.removeItem(TOKEN_KEY); } catch { /* private mode — the session just won't survive a reload */ } } /** ApiError carries the HTTP status so callers can branch on 401/403/503. */ export class ApiError extends Error { constructor(message, status, body) { super(message); this.status = status; this.body = body; } } // messageFrom digs a human-readable message out of the several error shapes in // play: this server's {error}, and PocketBase's {message, data:{field:{message}}} // which the user/org endpoints relay verbatim. function messageFrom(body, status) { if (!body || typeof body !== "object") return `HTTP ${status}`; if (body.error) return body.error; const fieldErrors = Object.entries(body.data || {}) .map(([field, e]) => `${field}: ${e?.message || e}`) .filter(Boolean); if (fieldErrors.length) return fieldErrors.join("; "); return body.message || `HTTP ${status}`; } /** * request calls the API Server, attaching the panel's token. A 401 clears the * session so the shell falls back to the login screen. */ export async function request(path, { method = "GET", body, auth = true } = {}) { const headers = {}; if (body !== undefined) headers["Content-Type"] = "application/json"; if (auth && token.value) headers.Authorization = token.value; const resp = await fetch(path, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), }); const text = await resp.text(); let parsed = null; try { parsed = text ? JSON.parse(text) : null; } catch { parsed = null; // non-JSON body (shouldn't happen, but don't explode on it) } if (!resp.ok) { if (resp.status === 401 && auth) logout(); throw new ApiError(messageFrom(parsed, resp.status), resp.status, parsed); } return parsed; } /** login exchanges credentials for a PocketBase token via the API Server. */ export async function login(email, password) { const out = await request("/api/auth/login", { method: "POST", body: { email, password }, auth: false, }); setToken(out.token); await loadMe(); return me.value; } /** loadMe resolves the current identity, including role + organization. */ export async function loadMe() { me.value = await request("/api/identity"); return me.value; } /** logout drops the local session. PocketBase tokens are stateless, so there is * nothing to revoke server-side. */ export function logout() { setToken(""); me.value = null; } /** restore re-validates a token kept from a previous visit. */ export async function restore() { if (!token.value) return null; try { return await loadMe(); } catch { logout(); // expired, revoked, or the server now points at another PocketBase return null; } }