The left rail is back to the three places you actually go — Garage, Charging, Settings — and user management moves inside Settings as an admin-only tab, next to a new Organization tab that used to be a card buried in the personal settings. Tab order is Personal settings, Users, Organization, Integrations. /admin redirects to /settings?tab=users so old links keep working, and ?tab= picks the starting tab in general. AdminUsers moves from views/ to components/ since it is a panel now, not a route, and its page header becomes a section header like its neighbours. The personal panel was split in two around the integrations markup, which left no gap between the Profile and Privacy cards; it is one block again. Creating a user gets an organization picker for superadmins, defaulting to "no organization" so an org-less account stays a deliberate choice. Admins see no picker: the server pins their members to their own org regardless, which users_test.go now covers along with both superadmin paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
60 lines
2.4 KiB
JavaScript
60 lines
2.4 KiB
JavaScript
// Reactive auth state shared across the app. Token + user are persisted to
|
||
// localStorage so a refresh keeps the session. The actual network calls live in
|
||
// api.js (which reads the token from localStorage on each request).
|
||
import { reactive, computed } from "vue";
|
||
import { api, TOKEN_KEY, USER_KEY } from "./api";
|
||
import { applyProfilePrefs } from "./prefs";
|
||
|
||
export const state = reactive({
|
||
token: localStorage.getItem(TOKEN_KEY) || "",
|
||
user: JSON.parse(localStorage.getItem(USER_KEY) || "null"),
|
||
// Full settings-panel profile (bio, theme, locale, ...), fetched separately
|
||
// from /api/me since the login response only carries id/email/name.
|
||
profile: null,
|
||
});
|
||
|
||
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.
|
||
export const isAdmin = computed(
|
||
() => MANAGER_ROLES.includes(state.profile?.role) || MANAGER_ROLES.includes(state.user?.role)
|
||
);
|
||
|
||
export async function login(email, password) {
|
||
// 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.login(email, password);
|
||
state.token = res.token;
|
||
state.user = res.record;
|
||
localStorage.setItem(TOKEN_KEY, res.token);
|
||
localStorage.setItem(USER_KEY, JSON.stringify(res.record));
|
||
await refreshProfile();
|
||
return res;
|
||
}
|
||
|
||
export function logout() {
|
||
state.token = "";
|
||
state.user = null;
|
||
state.profile = null;
|
||
localStorage.removeItem(TOKEN_KEY);
|
||
localStorage.removeItem(USER_KEY);
|
||
}
|
||
|
||
// 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;
|
||
}
|