import { t } from "@lingui/core/macro" import PocketBase from "pocketbase" import { basePath } from "@/components/router" import { toast } from "@/components/ui/use-toast" import { dynamicActivate, getLocale } from "@/lib/i18n" import type { ChartTimes, UserSettings } from "@/types" import { $alerts, $allSystemsById, $allSystemsByName, $userSettings } from "./stores" import { chartTimeData, debounce } from "./utils" /** PocketBase JS Client */ export const pb = new PocketBase(basePath) export const isAdmin = () => pb.authStore.record?.role === "admin" export const isReadOnlyUser = () => pb.authStore.record?.role === "readonly" const verifyAuth = () => { pb.collection("users") .authRefresh() .catch(() => { logOut() toast({ title: t`Failed to authenticate`, description: t`Please log in again`, variant: "destructive", }) }) } const verifyAuthDebounced = debounce(verifyAuth, 100) // verify the session whenever any API request returns a 4xx response (e.g. an // expired JWT). The auth-refresh endpoint is excluded to avoid a loop, since // it returns 401 itself when the token is no longer valid. pb.afterSend = (response, data) => { if ( (response.status === 401 || response.status === 403) && pb.authStore.token && !response.url.includes("auth-refresh") ) { verifyAuthDebounced() } return data } /** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */ export function logOut() { $allSystemsByName.set({}) $allSystemsById.set({}) $alerts.set({}) $userSettings.set({} as UserSettings) sessionStorage.setItem("lo", "t") // prevent auto login on logout pb.authStore.clear() pb.realtime.unsubscribe() } /** Save a partial update to user settings in database immediately */ export async function saveUserSettings(newSettings: Partial) { // get fresh copy of settings so concurrent changes aren't overwritten const req = await pb.collection("user_settings").getFirstListItem("", { fields: "id,settings" }) const updatedSettings = await pb.collection("user_settings").update(req.id, { settings: { ...req.settings, ...newSettings, }, }) $userSettings.set(updatedSettings.settings) } // keys queued by queueUserSettings, flushed together in a single request so that // two debounced saves for different keys can't race each other's read-modify-write // and silently drop one of the changes let queuedSettings: Partial = {} const flushQueuedSettings = debounce(() => { const toSave = queuedSettings queuedSettings = {} if (Object.keys(toSave).length === 0) { return } saveUserSettings(toSave).catch(console.error) }, 1000) /** Queue a partial user settings update, merging with any other pending keys and saving them together after a debounce window */ export function queueUserSettings(newSettings: Partial) { queuedSettings = { ...queuedSettings, ...newSettings } flushQueuedSettings() } /** Fetch or create user settings in database */ export async function updateUserSettings() { try { const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" }) $userSettings.set(req.settings) dynamicActivate(req.settings.lang || getLocale()) return } catch (e) { console.error("get settings", e) } // create user settings if error fetching existing try { const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record?.id }) $userSettings.set(createdSettings.settings) dynamicActivate(createdSettings.settings.lang || getLocale()) } catch (e) { console.error("create settings", e) } } export function getPbTimestamp(timeString: ChartTimes, d?: Date, createdIsNumber?: boolean) { d ||= chartTimeData[timeString].getOffset(new Date()) if (createdIsNumber) { return d.getTime() } const year = d.getUTCFullYear() const month = String(d.getUTCMonth() + 1).padStart(2, "0") const day = String(d.getUTCDate()).padStart(2, "0") const hours = String(d.getUTCHours()).padStart(2, "0") const minutes = String(d.getUTCMinutes()).padStart(2, "0") const seconds = String(d.getUTCSeconds()).padStart(2, "0") return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` }