feat: multi-user UI login with admin and monitor roles

Add sac_users table, JWT roles, settings/users API guarded for admin, Users page in UI, and sac_manage_user.py CLI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-01 09:27:14 +10:00
parent bc77f82307
commit 56c469ddbd
20 changed files with 859 additions and 85 deletions
+33
View File
@@ -1,15 +1,34 @@
const TOKEN_KEY = "sac_token";
const ROLE_KEY = "sac_role";
export function getToken(): string | null {
return localStorage.getItem(TOKEN_KEY);
}
export function getRole(): string | null {
return localStorage.getItem(ROLE_KEY);
}
export function isAdmin(): boolean {
return getRole() === "admin";
}
export function setToken(token: string): void {
localStorage.setItem(TOKEN_KEY, token);
}
export function setRole(role: string): void {
localStorage.setItem(ROLE_KEY, role);
}
export function setAuth(token: string, role: string): void {
setToken(token);
setRole(role);
}
export function clearToken(): void {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(ROLE_KEY);
}
export async function apiFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
@@ -27,6 +46,9 @@ export async function apiFetch<T>(path: string, init: RequestInit = {}): Promise
window.location.href = "/login";
throw new Error("Unauthorized");
}
if (res.status === 403) {
throw new Error("Forbidden");
}
if (!res.ok) {
const text = await res.text();
throw new Error(text || res.statusText);
@@ -37,6 +59,17 @@ export async function apiFetch<T>(path: string, init: RequestInit = {}): Promise
export interface TokenResponse {
access_token: string;
token_type: string;
username: string;
role: string;
}
export interface MeResponse {
username: string;
role: string;
}
export function fetchMe(): Promise<MeResponse> {
return apiFetch<MeResponse>("/api/v1/auth/me");
}
export interface EventSummary {