CI / frontend (pull_request) Successful in 5m21s
CI / lint (pull_request) Successful in 2m24s
CI / test (pull_request) Successful in 9m11s
CI / test (push) Successful in 8m16s
CI / frontend (push) Successful in 4m24s
CI / reuse (push) Successful in 5s
CI / reuse (pull_request) Successful in 5s
CI / lint (push) Successful in 2m25s
Assisted-By: Codex (GPT-5) Signed-off-by: Stan Grams <sjg@haxx.space>
137 lines
5.0 KiB
JavaScript
137 lines
5.0 KiB
JavaScript
// src/api/auth.ts
|
|
var AUTH_ROLES = ["guest", "read", "control", "transmit", "write", "administrator"];
|
|
var AUTH_ADMIN_ROLES = AUTH_ROLES.filter((role) => role !== "guest");
|
|
var AUTH_ROLE_LABELS = {
|
|
guest: "Guest",
|
|
read: "Read",
|
|
control: "Control",
|
|
transmit: "Transmit",
|
|
write: "Write",
|
|
administrator: "Administrator"
|
|
};
|
|
function isAuthRole(value) {
|
|
return typeof value === "string" && AUTH_ROLES.includes(value);
|
|
}
|
|
function normalizeAuthRoles(roles) {
|
|
return AUTH_ROLES.filter((role) => roles.includes(role));
|
|
}
|
|
function hasAuthRole(roles, required) {
|
|
return roles.includes("administrator") || roles.includes(required) || required === "read" && roles.includes("guest") || required === "read" && (roles.includes("control") || roles.includes("transmit"));
|
|
}
|
|
function hasAccountControls(roles) {
|
|
return roles.length > 0 && !roles.includes("guest");
|
|
}
|
|
function decodeRoles(value, context) {
|
|
if (!Array.isArray(value) || !value.every(isAuthRole)) {
|
|
throw new TypeError(`${context} has invalid roles`);
|
|
}
|
|
return normalizeAuthRoles(value);
|
|
}
|
|
function decodeAuthSession(value) {
|
|
if (typeof value !== "object" || value === null) {
|
|
throw new TypeError("The authentication response is malformed");
|
|
}
|
|
const session = value;
|
|
if (typeof session.authenticated !== "boolean") {
|
|
throw new TypeError("The authentication response has no authenticated flag");
|
|
}
|
|
if (session.auth_disabled !== void 0 && typeof session.auth_disabled !== "boolean") {
|
|
throw new TypeError("The authentication response has an invalid auth_disabled flag");
|
|
}
|
|
const decoded = {
|
|
authenticated: session.authenticated,
|
|
roles: decodeRoles(session.roles, "The authentication response")
|
|
};
|
|
if (session.username !== void 0) {
|
|
if (typeof session.username !== "string") throw new TypeError("The authentication response has an invalid username");
|
|
decoded.username = session.username;
|
|
}
|
|
if (session.auth_disabled !== void 0) decoded.auth_disabled = session.auth_disabled;
|
|
return decoded;
|
|
}
|
|
var authDisabledSession = {
|
|
authenticated: true,
|
|
roles: [...AUTH_ADMIN_ROLES],
|
|
auth_disabled: true
|
|
};
|
|
async function fetchAuthSession() {
|
|
try {
|
|
const response = await fetch("/auth/session");
|
|
if (response.status === 404) return authDisabledSession;
|
|
if (!response.ok) return { authenticated: false, roles: [] };
|
|
return decodeAuthSession(await response.json());
|
|
} catch (error) {
|
|
console.error("Auth check failed:", error);
|
|
return { authenticated: false, roles: [] };
|
|
}
|
|
}
|
|
async function login(username, password) {
|
|
const response = await fetch("/auth/login", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ username, password })
|
|
});
|
|
if (response.status === 404) return authDisabledSession;
|
|
if (!response.ok) {
|
|
const message = await response.text();
|
|
throw new Error(message || "Login failed");
|
|
}
|
|
return decodeAuthSession(await response.json());
|
|
}
|
|
async function userRequest(path, init) {
|
|
const response = await fetch(path, init);
|
|
if (!response.ok) {
|
|
const payload = await response.json().catch(() => ({}));
|
|
throw new Error(payload.error || `User operation failed (${response.status})`);
|
|
}
|
|
return response;
|
|
}
|
|
async function listUsers() {
|
|
const value = await userRequest("/auth/users").then((response) => response.json());
|
|
if (!Array.isArray(value) || !value.every((user) => {
|
|
if (typeof user !== "object" || user === null) return false;
|
|
const record = user;
|
|
return typeof record.username === "string" && typeof record.enabled === "boolean" && Array.isArray(record.roles) && record.roles.every(isAuthRole);
|
|
})) {
|
|
throw new TypeError("The user list response is malformed");
|
|
}
|
|
return value.map((user) => ({ ...user, roles: normalizeAuthRoles(user.roles) }));
|
|
}
|
|
async function createUser(username, password, roles, enabled = true) {
|
|
await userRequest("/auth/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password, roles, enabled }) });
|
|
}
|
|
async function updateUser(username, changes) {
|
|
await userRequest(`/auth/users/${encodeURIComponent(username)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(changes) });
|
|
}
|
|
async function changeOwnPassword(currentPassword, newPassword) {
|
|
await userRequest("/auth/account/password", {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ current_password: currentPassword, new_password: newPassword })
|
|
});
|
|
}
|
|
async function deleteUser(username) {
|
|
await userRequest(`/auth/users/${encodeURIComponent(username)}`, { method: "DELETE" });
|
|
}
|
|
async function logout() {
|
|
const response = await fetch("/auth/logout", { method: "POST" });
|
|
if (response.status !== 404 && !response.ok) throw new Error("Logout failed");
|
|
}
|
|
|
|
export {
|
|
AUTH_ROLES,
|
|
AUTH_ADMIN_ROLES,
|
|
AUTH_ROLE_LABELS,
|
|
normalizeAuthRoles,
|
|
hasAuthRole,
|
|
hasAccountControls,
|
|
fetchAuthSession,
|
|
login,
|
|
listUsers,
|
|
createUser,
|
|
updateUser,
|
|
changeOwnPassword,
|
|
deleteUser,
|
|
logout
|
|
};
|