Refactor HTTP account system
CI / frontend (pull_request) Successful in 5m13s
CI / reuse (pull_request) Successful in 29s
CI / frontend (push) Successful in 4m15s
CI / reuse (push) Successful in 5s
CI / lint (pull_request) Successful in 2m25s
CI / test (pull_request) Successful in 9m24s
CI / lint (push) Successful in 2m23s
CI / test (push) Successful in 8m19s
CI / frontend (pull_request) Successful in 5m13s
CI / reuse (pull_request) Successful in 29s
CI / frontend (push) Successful in 4m15s
CI / reuse (push) Successful in 5s
CI / lint (pull_request) Successful in 2m25s
CI / test (pull_request) Successful in 9m24s
CI / lint (push) Successful in 2m23s
CI / test (push) Successful in 8m19s
This commit was merged in pull request #61.
This commit is contained in:
@@ -1323,7 +1323,7 @@ function decodeAuthSession(value) {
|
||||
if (typeof session.authenticated !== "boolean") {
|
||||
throw new TypeError("The authentication response has no authenticated flag");
|
||||
}
|
||||
if (session.role !== void 0 && session.role !== "rx" && session.role !== "control") {
|
||||
if (session.role !== void 0 && session.role !== "user" && session.role !== "admin") {
|
||||
throw new TypeError("The authentication response has an invalid role");
|
||||
}
|
||||
if (session.auth_disabled !== void 0 && typeof session.auth_disabled !== "boolean") {
|
||||
@@ -1331,12 +1331,16 @@ function decodeAuthSession(value) {
|
||||
}
|
||||
const decoded = { authenticated: session.authenticated };
|
||||
if (session.role !== void 0) decoded.role = session.role;
|
||||
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,
|
||||
role: "control",
|
||||
role: "admin",
|
||||
auth_disabled: true
|
||||
};
|
||||
async function fetchAuthSession() {
|
||||
@@ -1350,11 +1354,11 @@ async function fetchAuthSession() {
|
||||
return { authenticated: false };
|
||||
}
|
||||
}
|
||||
async function login(passphrase) {
|
||||
async function login(username, password) {
|
||||
const response = await fetch("/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ passphrase })
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
if (response.status === 404) return authDisabledSession;
|
||||
if (!response.ok) {
|
||||
@@ -1363,6 +1367,34 @@ async function login(passphrase) {
|
||||
}
|
||||
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" && (record.role === "user" || record.role === "admin");
|
||||
})) {
|
||||
throw new TypeError("The user list response is malformed");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
async function createUser(username, password, role) {
|
||||
await userRequest("/auth/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password, role }) });
|
||||
}
|
||||
async function updateUser(username, changes) {
|
||||
await userRequest(`/auth/users/${encodeURIComponent(username)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(changes) });
|
||||
}
|
||||
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");
|
||||
@@ -1863,32 +1895,32 @@ function isVchanRdsEntry(value) {
|
||||
}
|
||||
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
||||
var authRole = null;
|
||||
var authUsername = null;
|
||||
var authEnabled = true;
|
||||
async function checkAuthStatus() {
|
||||
return fetchAuthSession();
|
||||
}
|
||||
async function authLogin(passphrase) {
|
||||
return login(passphrase);
|
||||
async function authLogin(username, password) {
|
||||
return login(username, password);
|
||||
}
|
||||
async function authLogout() {
|
||||
try {
|
||||
await logout();
|
||||
authRole = null;
|
||||
authUsername = null;
|
||||
disconnect();
|
||||
setDecodeHistoryOverlayVisible(false);
|
||||
requiredElement("content").style.display = "none";
|
||||
requiredElement("loading").style.display = "none";
|
||||
requiredElement("auth-passphrase").value = "";
|
||||
requiredElement("auth-password").value = "";
|
||||
updateAuthUI();
|
||||
const authStatus = await checkAuthStatus();
|
||||
const allowGuest = authStatus.role === "rx";
|
||||
showAuthGate(allowGuest);
|
||||
showAuthGate();
|
||||
} catch (e) {
|
||||
console.error("Logout failed:", e);
|
||||
showAuthError("Logout failed");
|
||||
}
|
||||
}
|
||||
function showAuthGate(allowGuest = false) {
|
||||
function showAuthGate() {
|
||||
if (!authEnabled) return;
|
||||
setDecodeHistoryOverlayVisible(false);
|
||||
requiredElement("loading").style.display = "none";
|
||||
@@ -1905,10 +1937,6 @@ function showAuthGate(allowGuest = false) {
|
||||
document.querySelectorAll(".tab-panel").forEach((panel) => {
|
||||
panel.style.display = "none";
|
||||
});
|
||||
const guestBtn2 = document.getElementById("auth-guest-btn");
|
||||
if (guestBtn2) {
|
||||
guestBtn2.style.display = allowGuest ? "block" : "none";
|
||||
}
|
||||
document.querySelectorAll(".tab-bar .tab").forEach((btn) => {
|
||||
btn.classList.toggle("active", btn.dataset.tab === "main");
|
||||
});
|
||||
@@ -1952,7 +1980,7 @@ function updateAuthUI() {
|
||||
}
|
||||
if (authRole) {
|
||||
if (badge) badge.style.display = "block";
|
||||
if (badgeRole) badgeRole.textContent = authRole === "control" ? "Control (full access)" : "RX (read-only)";
|
||||
if (badgeRole) badgeRole.textContent = `${authUsername || "local"} — ${authRole === "admin" ? "Admin" : "User (read-only)"}`;
|
||||
if (headerAuthBtn2) {
|
||||
headerAuthBtn2.textContent = "Logout";
|
||||
headerAuthBtn2.style.display = "block";
|
||||
@@ -1968,7 +1996,7 @@ function updateAuthUI() {
|
||||
}
|
||||
function applyAuthRestrictions() {
|
||||
if (!authRole) return;
|
||||
if (authRole === "rx") {
|
||||
if (authRole === "user") {
|
||||
const pttBtn2 = document.getElementById("ptt-btn");
|
||||
const powerBtn2 = document.getElementById("power-btn");
|
||||
const lockBtn2 = document.getElementById("lock-btn");
|
||||
@@ -2271,7 +2299,7 @@ function syncTopBarAccess() {
|
||||
rigSwitch.style.display = loggedOut ? "none" : "";
|
||||
}
|
||||
if (headerRigSwitchSelect) {
|
||||
headerRigSwitchSelect.disabled = loggedOut || authRole === "rx" || lastRigIds.length === 0;
|
||||
headerRigSwitchSelect.disabled = loggedOut || authRole === "user" || lastRigIds.length === 0;
|
||||
}
|
||||
}
|
||||
var overviewDrawPending = false;
|
||||
@@ -2907,7 +2935,7 @@ function applyRigList(activeRigId, rigIds, displayNames) {
|
||||
}
|
||||
const nextKey = lastRigIds.join("\0") + "|" + (lastActiveRigId || "");
|
||||
const rigListChanged = prevKey !== nextKey;
|
||||
const disableSwitch = lastRigIds.length === 0 || !authRole || authRole === "rx";
|
||||
const disableSwitch = lastRigIds.length === 0 || !authRole || authRole === "user";
|
||||
populateRigPicker(headerRigSwitchSelect, lastRigIds, lastActiveRigId, disableSwitch);
|
||||
updateRigSubtitle(lastActiveRigId);
|
||||
window.trxUi?.setActiveRig(lastActiveRigId);
|
||||
@@ -4510,7 +4538,7 @@ function scheduleTuneLinkSync() {
|
||||
async function applyTuneLink(link) {
|
||||
const wanted = link.rig || link.mode || link.freqHz != null || link.bandwidthHz != null;
|
||||
if (!wanted) return;
|
||||
if (authRole === "rx") {
|
||||
if (authRole === "user") {
|
||||
showHint("Read-only session — link not applied", 2500);
|
||||
return;
|
||||
}
|
||||
@@ -5253,8 +5281,8 @@ async function switchRigFromSelect(selectEl) {
|
||||
showHint("No rig selected", 1500);
|
||||
return;
|
||||
}
|
||||
if (authRole === "rx") {
|
||||
showHint("Control role required", 1500);
|
||||
if (authRole === "user") {
|
||||
showHint("Admin role required", 1500);
|
||||
return;
|
||||
}
|
||||
if (!lastRigIds.includes(selectEl.value)) {
|
||||
@@ -5868,7 +5896,7 @@ function navigateToTab(name, options = {}) {
|
||||
const leavingSatellites = _activeTab === "satellites" && name !== "satellites";
|
||||
const { updateHistory = true, replaceHistory = false } = options;
|
||||
if (authEnabled && !authRole && name !== "main") {
|
||||
showAuthGate(false);
|
||||
showAuthGate();
|
||||
return;
|
||||
}
|
||||
const btn = document.querySelector(`.tab-bar .tab[data-tab="${name}"]`);
|
||||
@@ -5981,11 +6009,11 @@ window.addEventListener("resize", () => {
|
||||
scheduleSpectrumLayout();
|
||||
});
|
||||
async function initializeApp() {
|
||||
showAuthGate(false);
|
||||
showAuthGate();
|
||||
const authStatus = await checkAuthStatus();
|
||||
authEnabled = !authStatus.auth_disabled;
|
||||
if (!authEnabled) {
|
||||
authRole = "control";
|
||||
authRole = "admin";
|
||||
hideAuthGate();
|
||||
updateAuthUI();
|
||||
connect();
|
||||
@@ -5997,6 +6025,7 @@ async function initializeApp() {
|
||||
}
|
||||
if (authStatus.authenticated) {
|
||||
authRole = authStatus.role ?? null;
|
||||
authUsername = authStatus.username ?? null;
|
||||
hideAuthGate();
|
||||
updateAuthUI();
|
||||
applyAuthRestrictions();
|
||||
@@ -6006,8 +6035,7 @@ async function initializeApp() {
|
||||
resizeHeaderSignalCanvas();
|
||||
startHeaderSignalSampling();
|
||||
} else {
|
||||
const allowGuest = authStatus.role === "rx";
|
||||
showAuthGate(allowGuest);
|
||||
showAuthGate();
|
||||
}
|
||||
}
|
||||
var settingsUiReady = false;
|
||||
@@ -6019,19 +6047,104 @@ function initSettingsUI() {
|
||||
window.trx.modules.backgroundDecode.initialize(lastActiveRigId, authRole);
|
||||
window.trx.modules.backgroundDecode.wireEvents();
|
||||
}
|
||||
void refreshUserManagement();
|
||||
}
|
||||
async function refreshUserManagement() {
|
||||
const section = document.getElementById("user-management");
|
||||
if (!section) return;
|
||||
section.style.display = authEnabled && authRole === "admin" ? "block" : "none";
|
||||
if (section.style.display === "none") return;
|
||||
const list = requiredElement("user-list");
|
||||
try {
|
||||
const users = await listUsers();
|
||||
list.replaceChildren(...users.map((user) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "sch-row";
|
||||
row.style.cssText = "display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin:.4rem 0";
|
||||
const name = document.createElement("strong");
|
||||
name.textContent = user.username;
|
||||
name.style.minWidth = "10rem";
|
||||
const role = document.createElement("select");
|
||||
role.className = "auth-input";
|
||||
for (const value of ["user", "admin"]) {
|
||||
const option = document.createElement("option");
|
||||
option.value = value;
|
||||
option.textContent = value === "admin" ? "Admin" : "User";
|
||||
option.selected = user.role === value;
|
||||
role.append(option);
|
||||
}
|
||||
const password = document.createElement("input");
|
||||
password.type = "password";
|
||||
password.placeholder = "New password";
|
||||
password.autocomplete = "new-password";
|
||||
password.className = "auth-input";
|
||||
password.minLength = 8;
|
||||
const save = document.createElement("button");
|
||||
save.type = "button";
|
||||
save.textContent = "Save";
|
||||
save.addEventListener("click", async () => {
|
||||
const changes = { role: role.value };
|
||||
if (password.value) changes.password = password.value;
|
||||
await runUserOperation(() => updateUser(user.username, changes));
|
||||
});
|
||||
const remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.textContent = "Remove";
|
||||
remove.className = "danger";
|
||||
remove.disabled = user.username === authUsername;
|
||||
remove.addEventListener("click", async () => {
|
||||
if (await window.trxUi.confirm({ title: "Remove user?", message: `Remove ${user.username} and revoke their sessions?`, confirmLabel: "Remove", danger: true })) {
|
||||
await runUserOperation(() => deleteUser(user.username));
|
||||
}
|
||||
});
|
||||
row.append(name, role, password, save, remove);
|
||||
return row;
|
||||
}));
|
||||
} catch (error) {
|
||||
showUserManagementError(error);
|
||||
}
|
||||
}
|
||||
function showUserManagementError(error) {
|
||||
const element = document.getElementById("user-management-error");
|
||||
if (!element) return;
|
||||
element.textContent = error instanceof Error ? error.message : String(error);
|
||||
element.style.display = "block";
|
||||
}
|
||||
async function runUserOperation(operation) {
|
||||
try {
|
||||
await operation();
|
||||
const error = document.getElementById("user-management-error");
|
||||
if (error) error.style.display = "none";
|
||||
await refreshUserManagement();
|
||||
} catch (reason) {
|
||||
showUserManagementError(reason);
|
||||
}
|
||||
}
|
||||
document.getElementById("user-create-form")?.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
const username = requiredElement("user-create-username");
|
||||
const password = requiredElement("user-create-password");
|
||||
const role = requiredElement("user-create-role");
|
||||
void runUserOperation(async () => {
|
||||
await createUser(username.value, password.value, role.value);
|
||||
username.value = "";
|
||||
password.value = "";
|
||||
role.value = "user";
|
||||
});
|
||||
});
|
||||
requiredElement("auth-form").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const passphraseEl = requiredElement("auth-passphrase");
|
||||
const passphrase = passphraseEl.value;
|
||||
const usernameEl = requiredElement("auth-username");
|
||||
const passwordEl = requiredElement("auth-password");
|
||||
const btn = requiredElement("auth-form").querySelector("button[type=submit]");
|
||||
if (!btn) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = "Logging in...";
|
||||
try {
|
||||
const result = await authLogin(passphrase);
|
||||
const result = await authLogin(usernameEl.value, passwordEl.value);
|
||||
authRole = result.role ?? null;
|
||||
passphraseEl.value = "";
|
||||
authUsername = result.username ?? usernameEl.value;
|
||||
passwordEl.value = "";
|
||||
hideAuthGate();
|
||||
updateAuthUI();
|
||||
applyAuthRestrictions();
|
||||
@@ -6041,28 +6154,13 @@ requiredElement("auth-form").addEventListener("submit", async (e) => {
|
||||
resizeHeaderSignalCanvas();
|
||||
startHeaderSignalSampling();
|
||||
} catch (err) {
|
||||
showAuthError("Invalid passphrase");
|
||||
showAuthError("Invalid username or password");
|
||||
console.error("Login error:", err);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Login";
|
||||
}
|
||||
});
|
||||
var guestBtn = document.getElementById("auth-guest-btn");
|
||||
if (guestBtn) {
|
||||
guestBtn.addEventListener("click", () => {
|
||||
authRole = "rx";
|
||||
requiredElement("auth-passphrase").value = "";
|
||||
hideAuthGate();
|
||||
updateAuthUI();
|
||||
applyAuthRestrictions();
|
||||
connect();
|
||||
connectDecode();
|
||||
initSettingsUI();
|
||||
resizeHeaderSignalCanvas();
|
||||
startHeaderSignalSampling();
|
||||
});
|
||||
}
|
||||
var headerAuthBtn = document.getElementById("header-auth-btn");
|
||||
if (headerAuthBtn) {
|
||||
headerAuthBtn.addEventListener("click", async () => {
|
||||
@@ -6071,7 +6169,7 @@ if (headerAuthBtn) {
|
||||
await authLogout();
|
||||
}
|
||||
} else {
|
||||
showAuthGate(false);
|
||||
showAuthGate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user