Files
DriverVault/Web App/web/src/auth.js
T
tajniak81andClaude Opus 5 f521d2b220 Web App: two sites, one tab — a server you can switch
Two locations means two full stacks, and until now the app could only ever
see the one that served it. The login screen's "Server settings" could point
it elsewhere, but as a global swap: it replaced the server rather than
adding one, and forgot the session you already had.

There is now a server picker at the bottom of the app rail, after signing
in rather than on the login screen. It names the server you are reading —
which matters most when both sites look identical — and switches in a click.

Each server is its own PocketBase with its own users, so nothing is
federated: you sign into each one once and a token is kept per server. The
home server is the one that served the app, reached same-origin through the
BFF proxy; any other is added by address and called straight from the
browser, which asks nothing new of the network — an API Server the phone app
can reach is internet-facing already.

Two things the switch turned out to need:

Views load on mount, so swapping the session alone left B's user looking at
A's cars. The RouterView is keyed on the active server and a switch returns
to the garage, since record ids belong to the server that issued them.

A request now carries the id of the server it went out to, and a 401 clears
that session rather than whatever is active when the answer lands. A page
fires half a dozen calls at once: the first rejection used to switch away
and the rest then cleared the session of the server it had switched to,
turning one stale remote token into a full sign-out.

A remote session expiring falls back to the home server with the entry left
in place to sign into again. Only Log out clears them all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:55:50 +02:00

116 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}