From d602de396ef585a704ba0adb59825edf0bb918ee Mon Sep 17 00:00:00 2001 From: KingKDot Date: Tue, 21 Jul 2026 13:18:30 -0500 Subject: [PATCH] major RBAC imrpovements + fix thumbnails --- Overlord-Server/public/assets/build.js | 14 +- Overlord-Server/public/assets/deploy.js | 18 +- Overlord-Server/public/assets/feature-gate.js | 12 +- Overlord-Server/public/assets/main.css | 4 +- Overlord-Server/public/assets/nav/role-ui.js | 25 +- Overlord-Server/public/assets/render.js | 12 +- Overlord-Server/public/assets/scripts.js | 26 +- Overlord-Server/public/assets/ui.js | 4 +- .../public/assets/user-client-access.js | 3 +- Overlord-Server/public/assets/users.js | 284 ++++++++++-------- Overlord-Server/public/assets/winre.js | 5 +- Overlord-Server/public/users.html | 51 +++- Overlord-Server/src/permission-groups.test.ts | 51 +++- Overlord-Server/src/rbac.test.ts | 4 + Overlord-Server/src/rbac.ts | 98 +++++- .../auth-routes.permissions-array.test.ts | 22 ++ .../src/server/routes/auth-routes.ts | 21 +- .../src/server/routes/build-routes.ts | 6 +- .../src/server/routes/file-share-routes.ts | 12 +- .../src/server/routes/page-routes.ts | 66 ++-- .../src/server/routes/users-routes.ts | 25 +- Overlord-Server/test/e2e/access.e2e.ts | 25 +- Overlord-Server/test/e2e/smoke.e2e.ts | 24 ++ 23 files changed, 599 insertions(+), 213 deletions(-) diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index c617289..1f467b6 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -1702,6 +1702,8 @@ async function init() { } const data = await res.json(); + const permissions = Array.isArray(data.permissions) ? data.permissions : []; + const hasPermission = (permission) => permissions.includes(permission); currentUserRole = data.role; currentUsername = data.username || null; usernameDisplay.textContent = data.username; @@ -1740,21 +1742,25 @@ async function init() { "border-slate-600", ); } - if (data.role === "admin") { + if (hasPermission("users:manage")) { usersLink.classList.remove("hidden"); + } + if (hasPermission("plugins:manage")) { pluginsLink?.classList.remove("hidden"); + } + if (hasPermission("deploys:manage")) { document.getElementById("deploy-link")?.classList.remove("hidden"); } - if (data.role === "admin" || data.role === "operator" || data.canBuild) { + if (hasPermission("clients:build") || data.canBuild) { buildLink?.classList.remove("hidden"); } - if (data.role !== "viewer") { + if (hasPermission("scripts:manage")) { scriptsLink?.classList.remove("hidden"); } - if (data.role !== "admin" && data.role !== "operator" && !data.canBuild) { + if (!hasPermission("clients:build") && !data.canBuild) { buildBtn.disabled = true; buildBtn.innerHTML = ' Build requires permission'; diff --git a/Overlord-Server/public/assets/deploy.js b/Overlord-Server/public/assets/deploy.js index b11d3f9..66bcd62 100644 --- a/Overlord-Server/public/assets/deploy.js +++ b/Overlord-Server/public/assets/deploy.js @@ -63,6 +63,8 @@ async function checkAuth() { } const data = await res.json(); + const permissions = Array.isArray(data.permissions) ? data.permissions : []; + const hasPermission = (permission) => permissions.includes(permission); document.getElementById("username-display").textContent = data.username; const roleBadge = document.getElementById("role-badge"); @@ -84,12 +86,12 @@ async function checkAuth() { "border", "border-purple-800", ); - document.getElementById("metrics-link")?.classList.remove("hidden"); - document.getElementById("scripts-link")?.classList.remove("hidden"); - document.getElementById("build-link")?.classList.remove("hidden"); - document.getElementById("users-link")?.classList.remove("hidden"); - document.getElementById("plugins-link")?.classList.remove("hidden"); - document.getElementById("deploy-link")?.classList.remove("hidden"); + if (hasPermission("audit:view")) document.getElementById("metrics-link")?.classList.remove("hidden"); + if (hasPermission("scripts:manage")) document.getElementById("scripts-link")?.classList.remove("hidden"); + if (hasPermission("clients:build") || data.canBuild) document.getElementById("build-link")?.classList.remove("hidden"); + if (hasPermission("users:manage")) document.getElementById("users-link")?.classList.remove("hidden"); + if (hasPermission("plugins:manage")) document.getElementById("plugins-link")?.classList.remove("hidden"); + if (hasPermission("deploys:manage")) document.getElementById("deploy-link")?.classList.remove("hidden"); } else if (data.role === "operator") { roleBadge.classList.add( "bg-blue-900/50", @@ -106,8 +108,8 @@ async function checkAuth() { ); } - if (data.role !== "admin") { - alert("Access denied. Admin role required."); + if (!hasPermission("deploys:manage")) { + alert("Access denied. Missing deploys:manage permission."); window.location.href = "/"; } } catch (err) { diff --git a/Overlord-Server/public/assets/feature-gate.js b/Overlord-Server/public/assets/feature-gate.js index 3c8b09b..42ba52f 100644 --- a/Overlord-Server/public/assets/feature-gate.js +++ b/Overlord-Server/public/assets/feature-gate.js @@ -23,7 +23,9 @@ export async function checkFeatureAccess(feature, clientId) { const data = await res.json(); if (data.allowed) return true; - const reasons = Array.isArray(data.denied) ? data.denied : []; + const reasons = Array.isArray(data.denialDetails) && data.denialDetails.length + ? data.denialDetails + : (Array.isArray(data.denied) ? data.denied : []); showAccessDenied(reasons); return false; } catch { @@ -32,7 +34,13 @@ export async function checkFeatureAccess(feature, clientId) { } function showAccessDenied(reasons) { - const messages = reasons.map((r) => DENY_LABELS[r] || "Access denied.").join(" "); + const messages = reasons + .map((reason) => { + if (typeof reason === "string") return DENY_LABELS[reason] || "Access denied."; + if (reason && typeof reason.message === "string") return reason.message; + return "Access denied."; + }) + .join(" "); document.body.style.visibility = "hidden"; diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index 3ea6769..8cd658e 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -3183,8 +3183,8 @@ tbody tr:hover { background: #0d1018; border: 1px solid var(--cv-border); } -.cv-thumb-img { object-fit: cover; cursor: pointer; } -.cv-thumb { position: relative; } +.cv-thumb-img { object-fit: cover; } +.cv-thumb { position: relative; cursor: pointer; } .cv-thumb-host { position: relative; isolation: isolate; } .cv-thumb-overlay { position: absolute; diff --git a/Overlord-Server/public/assets/nav/role-ui.js b/Overlord-Server/public/assets/nav/role-ui.js index 16d648d..2b55b46 100644 --- a/Overlord-Server/public/assets/nav/role-ui.js +++ b/Overlord-Server/public/assets/nav/role-ui.js @@ -53,25 +53,28 @@ export function applyUserRoleUI(user, refs) { ); } + const permissions = new Set(Array.isArray(user.permissions) ? user.permissions : []); + const can = (permission) => permissions.has(permission); + const canAny = (...items) => items.some((permission) => can(permission)); + // Helper: unhide an element by ID in both topbar and sidebar const unhide = (id) => { const el = document.getElementById(id); if (el) el.classList.remove("hidden"); }; - if (user.role === "admin") { - unhide("users-link"); - unhide("plugins-link"); - unhide("logs-link"); - unhide("sol-publish-link"); - } - if (user.role === "admin" || user.role === "operator") { - unhide("build-link"); + if (can("users:manage")) unhide("users-link"); + if (can("plugins:manage")) unhide("plugins-link"); + if (can("audit:view")) unhide("logs-link"); + if (can("system:configure")) unhide("sol-publish-link"); + if (can("clients:build") || user.canBuild) unhide("build-link"); + if (can("clients:enroll")) unhide("enrollment-link"); + if (can("scripts:manage")) unhide("scripts-link"); + if (can("deploys:manage")) unhide("deploy-link"); + if (canAny("system:notifications", "clients:control", "clients:enroll")) { unhide("notifications-link"); - unhide("enrollment-link"); } - if (user.role !== "viewer") { - unhide("scripts-link"); + if (user.role !== "viewer" || user.canUploadFiles) { unhide("file-share-link"); } } diff --git a/Overlord-Server/public/assets/render.js b/Overlord-Server/public/assets/render.js index 93350d5..00d03f3 100644 --- a/Overlord-Server/public/assets/render.js +++ b/Overlord-Server/public/assets/render.js @@ -516,13 +516,17 @@ export function createRenderer({ return; } - const thumbImg = e.target.closest(".thumb-img"); - if (thumbImg) { + const thumbHost = e.target.closest("[data-thumb-host]"); + if (thumbHost) { + e.preventDefault(); + e.stopPropagation(); if (card.dataset.online === "true") { if (pingClient) pingClient(clientId); requestThumbnail(clientId); } - if (thumbImg.src) openModal(thumbImg.src); + const thumbImg = thumbHost.querySelector("img[data-thumb-img]"); + const src = thumbImg?.currentSrc || thumbImg?.src || ""; + if (src) openModal(src); return; } @@ -1190,6 +1194,8 @@ export function createRenderer({ `; return article; } + setupGridDelegation(); + return { renderMerge, setLayout }; } diff --git a/Overlord-Server/public/assets/scripts.js b/Overlord-Server/public/assets/scripts.js index 349b3a7..0507b77 100644 --- a/Overlord-Server/public/assets/scripts.js +++ b/Overlord-Server/public/assets/scripts.js @@ -68,6 +68,8 @@ async function checkAuth() { } const data = await res.json(); + const permissions = Array.isArray(data.permissions) ? data.permissions : []; + const hasPermission = (permission) => permissions.includes(permission); document.getElementById("username-display").textContent = data.username; const roleBadge = document.getElementById("role-badge"); @@ -105,25 +107,27 @@ async function checkAuth() { ); } - if (data.role === "admin") { + if (hasPermission("audit:view")) { document.getElementById("metrics-link")?.classList.remove("hidden"); + } + if (hasPermission("scripts:manage")) { document.getElementById("scripts-link")?.classList.remove("hidden"); + } + if (hasPermission("clients:build") || data.canBuild) { document.getElementById("build-link")?.classList.remove("hidden"); + } + if (hasPermission("deploys:manage")) { document.getElementById("deploy-link")?.classList.remove("hidden"); + } + if (hasPermission("users:manage")) { document.getElementById("users-link")?.classList.remove("hidden"); + } + if (hasPermission("plugins:manage")) { document.getElementById("plugins-link")?.classList.remove("hidden"); - } else if (data.role === "operator") { - document.getElementById("metrics-link")?.classList.remove("hidden"); - document.getElementById("scripts-link")?.classList.remove("hidden"); - document.getElementById("build-link")?.classList.remove("hidden"); } - if (data.canBuild) { - document.getElementById("build-link")?.classList.remove("hidden"); - } - - if (data.role === "viewer") { - alert("Access denied. Operator or Admin role required."); + if (!hasPermission("scripts:manage")) { + alert("Access denied. Missing scripts:manage permission."); window.location.href = "/"; } } catch (err) { diff --git a/Overlord-Server/public/assets/ui.js b/Overlord-Server/public/assets/ui.js index cba2a31..866a212 100644 --- a/Overlord-Server/public/assets/ui.js +++ b/Overlord-Server/public/assets/ui.js @@ -469,14 +469,14 @@ export function openModal(src) { setTimeout(() => { modalImg.src = src; - modal.classList.remove("Virtual"); + modal.classList.remove("hidden"); modal.classList.add("flex"); }, 10); } export function closeModal() { modal.classList.remove("flex"); - modal.classList.add("Virtual"); + modal.classList.add("hidden"); } export function wireModalClose() { diff --git a/Overlord-Server/public/assets/user-client-access.js b/Overlord-Server/public/assets/user-client-access.js index cb4f736..48f84bd 100644 --- a/Overlord-Server/public/assets/user-client-access.js +++ b/Overlord-Server/public/assets/user-client-access.js @@ -150,7 +150,8 @@ async function loadCurrentUser() { return; } state.me = await res.json(); - if (state.me.role !== "admin") { + const permissions = Array.isArray(state.me.permissions) ? state.me.permissions : []; + if (!permissions.includes("users:manage")) { window.location.href = "/"; } } diff --git a/Overlord-Server/public/assets/users.js b/Overlord-Server/public/assets/users.js index f559084..cf0ea16 100644 --- a/Overlord-Server/public/assets/users.js +++ b/Overlord-Server/public/assets/users.js @@ -14,10 +14,16 @@ const closeModal = document.getElementById("close-modal"); const cancelBtn = document.getElementById("cancel-btn"); const errorMessage = document.getElementById("error-message"); const errorText = document.getElementById("error-text"); +const generatePasswordBtn = document.getElementById("generate-password-btn"); +const generatedPasswordHint = document.getElementById("generated-password-hint"); const logoutBtn = document.getElementById("logout-btn"); const currentUserEl = document.getElementById("username-display"); const currentRoleEl = document.getElementById("role-badge"); +function hasPermission(permission) { + return Array.isArray(currentUser?.permissions) && currentUser.permissions.includes(permission); +} + async function getCurrentUser() { try { const res = await fetch("/api/auth/me"); @@ -59,25 +65,27 @@ async function getCurrentUser() { ); } - if (currentUser.role === "admin") { + if (hasPermission("audit:view")) { document.getElementById("metrics-link")?.classList.remove("hidden"); + } + if (hasPermission("scripts:manage")) { document.getElementById("scripts-link")?.classList.remove("hidden"); + } + if (hasPermission("clients:build") || currentUser.canBuild) { document.getElementById("build-link")?.classList.remove("hidden"); + } + if (hasPermission("users:manage")) { document.getElementById("users-link")?.classList.remove("hidden"); + } + if (hasPermission("plugins:manage")) { document.getElementById("plugins-link")?.classList.remove("hidden"); + } + if (hasPermission("deploys:manage")) { document.getElementById("deploy-link")?.classList.remove("hidden"); - } else if (currentUser.role === "operator") { - document.getElementById("metrics-link")?.classList.remove("hidden"); - document.getElementById("scripts-link")?.classList.remove("hidden"); - document.getElementById("build-link")?.classList.remove("hidden"); } - if (currentUser.canBuild) { - document.getElementById("build-link")?.classList.remove("hidden"); - } - - if (currentUser.role !== "admin") { - alert("Access denied. Admin role required."); + if (!hasPermission("users:manage")) { + alert("Access denied. Missing users:manage permission."); window.location.href = "/"; } @@ -220,17 +228,6 @@ function renderUsers() { > - ${user.role === "operator" ? ` - - ` : ''} ${user.role !== "admin" ? ` @@ -351,9 +349,6 @@ function attachActionListeners() { case "client-access": configureClientAccess(userId, username, role); break; - case "feature-permissions": - configureFeaturePermissions(userId, username); - break; case "plugin-access": configurePluginAccess(userId, username); break; @@ -380,6 +375,9 @@ function showModal(title) { function hideModal() { userModal.classList.add("hidden"); userForm.reset(); + generatedPasswordHint?.classList.add("hidden"); + const passwordInput = document.getElementById("password"); + if (passwordInput) passwordInput.type = "password"; } function showError(message) { @@ -387,10 +385,41 @@ function showError(message) { errorMessage.classList.remove("hidden"); } +function generatePassword(length = 20) { + const lower = "abcdefghjkmnpqrstuvwxyz"; + const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ"; + const digits = "23456789"; + const symbols = "!@#$%^&*()-_=+[]{}"; + const required = [lower, upper, digits, symbols]; + const alphabet = required.join(""); + const bytes = new Uint8Array(length); + crypto.getRandomValues(bytes); + const chars = required.map((set, index) => set[bytes[index] % set.length]); + for (let i = required.length; i < length; i++) { + chars.push(alphabet[bytes[i] % alphabet.length]); + } + for (let i = chars.length - 1; i > 0; i--) { + const j = bytes[i] % (i + 1); + [chars[i], chars[j]] = [chars[j], chars[i]]; + } + return chars.join(""); +} + +generatePasswordBtn?.addEventListener("click", () => { + const passwordInput = document.getElementById("password"); + if (!passwordInput) return; + passwordInput.value = generatePassword(); + passwordInput.type = "text"; + generatedPasswordHint?.classList.remove("hidden"); + passwordInput.focus(); + passwordInput.select(); +}); + addUserBtn.addEventListener("click", () => { showModal("Add User"); document.getElementById("password-field").classList.remove("hidden"); document.getElementById("password").required = true; + generatedPasswordHint?.classList.add("hidden"); }); closeModal.addEventListener("click", hideModal); @@ -550,96 +579,6 @@ const FEATURE_LABELS = { client_metadata: { label: "Edit Client Metadata", icon: "fa-pen-to-square" }, }; -window.configureFeaturePermissions = async function (userId, username) { - try { - const res = await fetch(`/api/users/${userId}/feature-permissions`); - if (!res.ok) throw new Error("Failed to load feature permissions"); - const data = await res.json(); - const perms = data.permissions; - const features = data.features; - - let modal = document.getElementById("feature-perms-modal"); - if (modal) modal.remove(); - - modal = document.createElement("div"); - modal.id = "feature-perms-modal"; - modal.className = "fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"; - modal.innerHTML = ` -
-
-

Feature Permissions

- -
-

- Manage feature access for ${escapeHtml(username)}. - Disabled features will return 403 when accessed. -

-
- ${features.map(f => { - const meta = FEATURE_LABELS[f] || { label: f, icon: "fa-puzzle-piece" }; - const checked = perms[f] !== false; - return ` - `; - }).join("")} -
-
- - -
-
`; - document.body.appendChild(modal); - - document.getElementById("close-feature-modal").addEventListener("click", () => modal.remove()); - modal.addEventListener("click", (e) => { if (e.target === modal) modal.remove(); }); - - document.getElementById("save-feature-perms").addEventListener("click", async () => { - const toggles = modal.querySelectorAll("[data-feature]"); - const permissions = {}; - toggles.forEach(t => { permissions[t.dataset.feature] = t.checked; }); - - try { - const saveRes = await fetch(`/api/users/${userId}/feature-permissions`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ permissions }), - }); - if (!saveRes.ok) throw new Error("Failed to save"); - modal.remove(); - if (window.showToast) window.showToast("Feature permissions updated", "success"); - } catch (err) { - alert("Failed to save feature permissions"); - } - }); - - document.getElementById("reset-feature-perms").addEventListener("click", async () => { - if (!confirm("Reset all feature permissions to defaults (all enabled)?")) return; - try { - const delRes = await fetch(`/api/users/${userId}/feature-permissions`, { method: "DELETE" }); - if (!delRes.ok) throw new Error("Failed to reset"); - modal.remove(); - if (window.showToast) window.showToast("Feature permissions reset", "success"); - } catch (err) { - alert("Failed to reset feature permissions"); - } - }); - } catch (err) { - console.error("Feature permissions error:", err); - alert("Failed to load feature permissions"); - } -}; window.configurePluginAccess = async function (userId, username) { try { @@ -1099,18 +1038,74 @@ async function deleteGroup(groupId, groupName) { } } +function renderEffectivePermissions(container, effective) { + const permissions = Array.isArray(effective?.permissions) ? effective.permissions : []; + const granted = permissions.filter((p) => p.allowed); + const denied = permissions.filter((p) => !p.allowed); + const ordered = [...granted, ...denied]; + const clientScope = effective?.clientAccess?.scope || "unknown"; + const pluginScope = effective?.pluginAccess?.scope || "unknown"; + const features = effective?.featurePermissions || {}; + const disabledFeatures = Object.entries(features) + .filter(([, allowed]) => allowed === false) + .map(([feature]) => feature); + + container.innerHTML = ` +
+
+ Client scope + ${escapeHtml(clientScope)} +
+
+ Plugin scope + ${escapeHtml(pluginScope)} +
+
+ Disabled features + ${disabledFeatures.length ? disabledFeatures.map(escapeHtml).join(", ") : "none"} +
+
+ ${ordered + .map((permission) => { + const sources = Array.isArray(permission.sources) ? permission.sources : []; + const sourceHtml = sources.length + ? sources + .map((source) => `${escapeHtml(source.label || source.type || "source")}`) + .join(" ") + : 'No grant source'; + return ` +
+
+
+
${escapeHtml(permission.id)}
+
${escapeHtml(permission.description || "")}
+
+ ${permission.allowed ? "granted" : "denied"} +
+
${sourceHtml}
+
`; + }) + .join("")} + `; +} + + async function openUserPermissionsModal(userId, username) { editingUserId = userId; const modal = document.getElementById("user-perms-modal"); const title = document.getElementById("user-perms-title"); const groupsBox = document.getElementById("user-groups-list"); + const effectiveBox = document.getElementById("user-effective-perms-list"); const extrasBox = document.getElementById("user-extras-list"); + const featuresBox = document.getElementById("user-features-list"); const errorBox = document.getElementById("user-perms-error"); title.textContent = `Permissions: ${username}`; errorBox.classList.add("hidden"); groupsBox.innerHTML = `

Loading...

`; extrasBox.innerHTML = `

Loading...

`; + featuresBox.innerHTML = `

Loading...

`; + effectiveBox.innerHTML = `

Loading...

`; modal.classList.remove("hidden"); if (allPermissions.length === 0 || allGroups.length === 0) { @@ -1118,12 +1113,18 @@ async function openUserPermissionsModal(userId, username) { } try { - const [groupsRes, extrasRes] = await Promise.all([ + const [groupsRes, extrasRes, effectiveRes, featureRes] = await Promise.all([ fetch(`/api/users/${userId}/permission-groups`), fetch(`/api/users/${userId}/extra-permissions`), + fetch(`/api/users/${userId}/effective-permissions`), + fetch(`/api/users/${userId}/feature-permissions`), ]); const groupsData = await groupsRes.json().catch(() => ({})); const extrasData = await extrasRes.json().catch(() => ({})); + const effectiveData = await effectiveRes.json().catch(() => ({})); + const featureData = await featureRes.json().catch(() => ({})); + const targetUser = users.find((candidate) => candidate.id === userId); + featuresBox.dataset.defaultAllowed = targetUser?.role === "viewer" ? "0" : "1"; const assignedGroups = new Set(groupsData.groupIds || []); const assignedExtras = new Set(extrasData.permissions || []); @@ -1154,6 +1155,32 @@ async function openUserPermissionsModal(userId, username) { `, ) .join(""); + + const featurePermissions = featureData.permissions || {}; + const features = Array.isArray(featureData.features) + ? featureData.features + : Object.keys(featurePermissions); + featuresBox.innerHTML = features.length === 0 + ? `

No feature gates available.

` + : features + .map((feature) => { + const meta = FEATURE_LABELS[feature] || { label: feature, icon: "fa-puzzle-piece" }; + const checked = featurePermissions[feature] !== false; + return ` + `; + }) + .join(""); + + renderEffectivePermissions(effectiveBox, effectiveData); } catch (err) { console.error(err); errorBox.textContent = "Failed to load current assignments"; @@ -1167,6 +1194,13 @@ function closeUserPermsModal() { } document.getElementById("close-user-perms-modal")?.addEventListener("click", closeUserPermsModal); document.getElementById("user-perms-cancel-btn")?.addEventListener("click", closeUserPermsModal); +document.getElementById("reset-user-features-btn")?.addEventListener("click", () => { + const featuresBox = document.getElementById("user-features-list"); + const defaultAllowed = featuresBox?.dataset.defaultAllowed !== "0"; + document.querySelectorAll("#user-features-list input[data-feature]").forEach((el) => { + el.checked = defaultAllowed; + }); +}); document.getElementById("user-perms-save-btn")?.addEventListener("click", async () => { if (!editingUserId) return; @@ -1176,11 +1210,15 @@ document.getElementById("user-perms-save-btn")?.addEventListener("click", async const permissions = Array.from( document.querySelectorAll("#user-extras-list input[type=checkbox]:checked"), ).map((el) => el.dataset.perm); + const featurePermissions = {}; + document.querySelectorAll("#user-features-list input[data-feature]").forEach((el) => { + featurePermissions[el.dataset.feature] = el.checked; + }); const errorBox = document.getElementById("user-perms-error"); errorBox.classList.add("hidden"); try { - const [gRes, eRes] = await Promise.all([ + const [gRes, eRes, fRes] = await Promise.all([ fetch(`/api/users/${editingUserId}/permission-groups`, { method: "PUT", headers: { "Content-Type": "application/json" }, @@ -1191,9 +1229,15 @@ document.getElementById("user-perms-save-btn")?.addEventListener("click", async headers: { "Content-Type": "application/json" }, body: JSON.stringify({ permissions }), }), + fetch(`/api/users/${editingUserId}/feature-permissions`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ permissions: featurePermissions }), + }), ]); - if (!gRes.ok || !eRes.ok) { - const data = await (gRes.ok ? eRes : gRes).json().catch(() => ({})); + if (!gRes.ok || !eRes.ok || !fRes.ok) { + const failed = !gRes.ok ? gRes : (!eRes.ok ? eRes : fRes); + const data = await failed.json().catch(() => ({})); errorBox.textContent = data.error || "Failed to save"; errorBox.classList.remove("hidden"); return; diff --git a/Overlord-Server/public/assets/winre.js b/Overlord-Server/public/assets/winre.js index 96a2cdd..d9e6b9b 100644 --- a/Overlord-Server/public/assets/winre.js +++ b/Overlord-Server/public/assets/winre.js @@ -56,6 +56,7 @@ async function checkAuth() { const data = await res.json(); document.getElementById("username-display").textContent = data.username; + const permissions = Array.isArray(data.permissions) ? data.permissions : []; const roleBadge = document.getElementById("role-badge"); const roleBadges = { @@ -77,8 +78,8 @@ async function checkAuth() { roleBadge.classList.add("bg-slate-700", "text-slate-300", "border", "border-slate-600"); } - if (data.role !== "admin") { - alert("Access denied. Admin role required."); + if (!permissions.includes("clients:winre")) { + alert("Access denied. Missing clients:winre permission."); window.location.href = "/"; } } catch (err) { diff --git a/Overlord-Server/public/users.html b/Overlord-Server/public/users.html index 709bb5b..cfbbac1 100644 --- a/Overlord-Server/public/users.html +++ b/Overlord-Server/public/users.html @@ -166,16 +166,26 @@ class="block text-sm font-medium text-slate-300 mb-2" >Password - +
+ + +
+

Minimum 6 characters

@@ -256,8 +266,8 @@ -