// Reactive auth state for whichever server is active. Tokens are held per // server by servers.js (each one is a separate PocketBase, so a session cannot // be shared); this module mirrors the active one into `state` and keeps the // richer profile in step, so every view can go on reading `state.user` without // knowing that more than one server exists. import { reactive, computed, watch } from "vue"; import { api } from "./api"; import { servers, serverById, sessionFor, setSession, clearSession, clearAllSessions, setActive, baseFor, HOME_ID, } from "./servers"; import { applyProfilePrefs } from "./prefs"; import { t } from "./i18n"; export const state = reactive({ token: "", user: null, // Full settings-panel profile (bio, theme, locale, ...), fetched separately // from /api/me since the login response only carries id/email/name. profile: null, }); function syncFromActive() { const session = sessionFor(servers.activeId); state.token = session?.token || ""; state.user = session?.user || null; } syncFromActive(); // Switching servers swaps the whole session: a different PocketBase, a // different user record, a different set of preferences. Watching the active id // *and* its token also covers a session being cleared underneath us — which is // what api.js does when a server answers 401. watch( () => [servers.activeId, sessionFor(servers.activeId)?.token], ([id], [prevId]) => { syncFromActive(); if (id !== prevId) state.profile = null; if (state.token && !state.profile) refreshProfile().catch(() => {}); } ); export const isAuthenticated = computed(() => !!state.token); // Roles that may manage users. A superadmin is an admin that also spans every // organization; the API Server enforces the difference, the UI just needs to // know whether to offer the Settings › Users tab at all. const MANAGER_ROLES = ["admin", "superadmin"]; // Admin gate for the UI. Driven by the full profile (fetched from /api/me), // which always reflects the current role from the DB — so a promotion/demotion // takes effect on the next profile refresh without needing a re-login. It is // per-server: being an admin on one server says nothing about the other. export const isAdmin = computed( () => MANAGER_ROLES.includes(state.profile?.role) || MANAGER_ROLES.includes(state.user?.role) ); // Logs into one server and makes it active. The login screen calls it for // whichever server is active; the switcher's connect dialog calls it for one // that isn't yet, which is why the credentials are checked against that // server's own base before anything switches. export async function connect(serverId, email, password) { const server = serverById(serverId); if (!server) throw new Error(t("servers.unknown")); // The API Server proxies login to PocketBase and relays its response // verbatim, so the user is under `record` (PocketBase's name) and the token // is PocketBase's own — this app no longer holds a server-minted JWT. const res = await api.loginAt(baseFor(server), email, password); setSession(serverId, res.token, res.record); state.profile = null; if (servers.activeId === serverId) syncFromActive(); else setActive(serverId); await refreshProfile(); return res; } export function login(email, password) { return connect(servers.activeId, email, password); } // Signs out of one server without leaving the app — the switcher's per-server // action. Dropping the active one falls back to home, the same way an expired // token does. export function disconnect(serverId) { clearSession(serverId); if (servers.activeId === serverId && serverId !== HOME_ID) setActive(HOME_ID); } // Signs out everywhere. Leaving the app means leaving every server you reached // from it — leaving a live token behind on a shared browser would be worse than // the inconvenience of logging back in. export function logout() { clearAllSessions(); setActive(HOME_ID); state.token = ""; state.user = null; state.profile = null; } // Fetches the full profile (used for Settings + to apply appearance prefs). // Safe to call on app boot when a token already exists from a previous visit. export async function refreshProfile() { if (!state.token) return null; const profile = await api.getMe(); state.profile = profile; applyProfilePrefs(profile); return profile; }