This commit is contained in:
KingKDot
2026-03-12 21:26:53 -05:00
parent 5da706bc88
commit 409ed35f5b
30 changed files with 1097 additions and 36 deletions
+6 -5
View File
@@ -141,7 +141,7 @@ async function init() {
document.getElementById("deploy-link")?.classList.remove("hidden");
}
if (data.role === "admin" || data.role === "operator") {
if (data.role === "admin" || data.role === "operator" || data.canBuild) {
buildLink?.classList.remove("hidden");
}
@@ -149,10 +149,10 @@ async function init() {
scriptsLink?.classList.remove("hidden");
}
if (data.role !== "admin" && data.role !== "operator") {
if (data.role !== "admin" && data.role !== "operator" && !data.canBuild) {
buildBtn.disabled = true;
buildBtn.innerHTML =
'<i class="fa-solid fa-lock"></i> <span>Build requires admin/operator role</span>';
'<i class="fa-solid fa-lock"></i> <span>Build requires permission</span>';
}
await loadServerVersion();
@@ -539,13 +539,14 @@ async function deleteBuild(buildId) {
}
try {
const res = await fetch(`/api/build/${buildId}/delete`, {
const res = await fetch(`/api/build/${encodeURIComponent(buildId)}/delete`, {
method: "DELETE",
credentials: "include",
});
if (!res.ok) {
throw new Error("Failed to delete build");
const data = await res.json().catch(() => ({}));
throw new Error(data.error || "Failed to delete build");
}
const buildElement = document.getElementById(`build-${buildId}`);
+1 -1
View File
@@ -186,7 +186,7 @@ async function loadCurrentUser() {
usersLink?.classList.remove("hidden");
}
if (currentUser.role === "admin" || currentUser.role === "operator") {
if (currentUser.role === "admin" || currentUser.role === "operator" || currentUser.canBuild) {
buildLink?.classList.remove("hidden");
}
+1 -1
View File
@@ -550,7 +550,7 @@ async function checkAuth() {
document.getElementById("build-link")?.classList.remove("hidden");
document.getElementById("plugins-link")?.classList.remove("hidden");
document.getElementById("deploy-link")?.classList.remove("hidden");
} else if (data.role === "operator") {
} else if (data.role === "operator" || data.canBuild) {
document.getElementById("build-link")?.classList.remove("hidden");
}
@@ -7,6 +7,7 @@ export function applyUserRoleUI(user, refs) {
pluginsLink,
scriptsLink,
logsLink,
notificationsLink,
} = refs;
if (!user || !usernameDisplay || !roleBadge) return;
@@ -68,6 +69,7 @@ export function applyUserRoleUI(user, refs) {
}
if (user.role === "admin" || user.role === "operator") {
buildLink?.classList.remove("hidden");
notificationsLink?.classList.remove("hidden");
}
if (user.role !== "viewer") {
scriptsLink?.classList.remove("hidden");
@@ -62,7 +62,7 @@ export function mountNav(host) {
<a
href="/notifications"
id="notifications-link"
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 hover:bg-slate-800 text-slate-300 transition-colors"
class="hidden inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 hover:bg-slate-800 text-slate-300 transition-colors"
><i class="fa-solid fa-bell text-yellow-400"></i> Notifications</a
>
<a
@@ -128,5 +128,6 @@ export function mountNav(host) {
pluginsLink: document.getElementById("plugins-link"),
scriptsLink: document.getElementById("scripts-link"),
logsLink: document.getElementById("logs-link"),
notificationsLink: document.getElementById("notifications-link"),
};
}
+1 -1
View File
@@ -60,7 +60,7 @@ async function checkAuth() {
document.getElementById("users-link")?.classList.remove("hidden");
document.getElementById("plugins-link")?.classList.remove("hidden");
document.getElementById("deploy-link")?.classList.remove("hidden");
} else if (data.role === "operator") {
} else if (data.role === "operator" || data.canBuild) {
document.getElementById("build-link")?.classList.remove("hidden");
}
+4
View File
@@ -112,6 +112,10 @@ async function checkAuth() {
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.");
window.location.href = "/";
+45
View File
@@ -19,6 +19,9 @@ const prefsForm = document.getElementById("prefs-form");
const prefNotificationsInput = document.getElementById("pref-notifications");
const prefRefreshSecondsInput = document.getElementById("pref-refresh-seconds");
const myTelegramChatIdInput = document.getElementById("my-telegram-chat-id");
const saveMyTelegramBtn = document.getElementById("save-my-telegram");
const bansTableBody = document.getElementById("bans-table-body");
const bansPermissionNote = document.getElementById("bans-permission-note");
const refreshBansBtn = document.getElementById("refresh-bans-btn");
@@ -447,6 +450,46 @@ async function runCertbotAutoSetup() {
}
}
async function loadMyTelegram() {
if (!myTelegramChatIdInput) return;
if (currentUser?.telegramChatId) {
myTelegramChatIdInput.value = currentUser.telegramChatId;
} else {
try {
const res = await fetch("/api/settings/telegram", { credentials: "include" });
if (res.ok) {
const data = await res.json();
myTelegramChatIdInput.value = data.telegramChatId || "";
}
} catch {}
}
}
async function saveMyTelegram() {
if (!myTelegramChatIdInput) return;
const chatId = myTelegramChatIdInput.value.trim();
try {
const res = await fetch("/api/settings/telegram", {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ telegramChatId: chatId }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
showMessage(data.error || "Failed to save Telegram settings.", "error");
return;
}
myTelegramChatIdInput.value = data.telegramChatId || "";
showMessage(chatId ? "Telegram chat ID saved." : "Telegram notifications disabled.");
} catch {
showMessage("Failed to save Telegram settings.", "error");
}
}
async function loadBannedIps() {
if (!currentUser) return;
@@ -537,12 +580,14 @@ async function init() {
try {
await loadCurrentUser();
loadPrefs();
await loadMyTelegram();
await loadSecurityPolicy();
await loadTlsSettings();
await loadBannedIps();
passwordForm.addEventListener("submit", updatePassword);
prefsForm.addEventListener("submit", savePrefs);
if (saveMyTelegramBtn) saveMyTelegramBtn.addEventListener("click", saveMyTelegram);
securityForm.addEventListener("submit", saveSecurityPolicy);
tlsForm.addEventListener("submit", saveTlsSettings);
tlsCertbotAutoBtn.addEventListener("click", runCertbotAutoSetup);
+44
View File
@@ -68,6 +68,10 @@ async function getCurrentUser() {
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.");
window.location.href = "/";
@@ -149,6 +153,13 @@ function renderUsers() {
</td>
<td class="px-6 py-4">
${getRoleBadge(user.role)}
${user.role !== "admin" ? `
<span class="ml-1 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium cursor-pointer transition-colors ${user.can_build ? 'bg-green-900/30 text-green-300 border border-green-800 hover:bg-green-900/50' : 'bg-slate-700/30 text-slate-500 border border-slate-700 hover:bg-slate-700/50'}"
data-action="toggle-build" data-user-id="${user.id}" data-username="${escapeHtml(user.username)}" data-can-build="${user.can_build ? 1 : 0}"
title="${user.can_build ? 'Can build (click to revoke)' : 'Cannot build (click to grant)'}">
<i class="fa-solid fa-hammer mr-1"></i>${user.can_build ? 'Build' : 'No Build'}
</span>
` : ''}
</td>
<td class="px-6 py-4 text-sm text-slate-400">
${formatDate(user.created_at)}
@@ -234,6 +245,15 @@ function attachActionListeners() {
}
const listener = (e) => {
const toggleBuild = e.target.closest("[data-action='toggle-build']");
if (toggleBuild) {
const userId = parseInt(toggleBuild.dataset.userId);
const username = toggleBuild.dataset.username;
const canBuild = toggleBuild.dataset.canBuild === "1";
toggleBuildPermission(userId, username, canBuild);
return;
}
const btn = e.target.closest(".user-action-btn");
if (!btn) return;
@@ -429,5 +449,29 @@ window.configureClientAccess = async function (userId, username, role) {
window.location.href = `/user-client-access?${params.toString()}`;
};
window.toggleBuildPermission = async function (userId, username, currentCanBuild) {
const newVal = !currentCanBuild;
if (!confirm(`${newVal ? 'Grant' : 'Revoke'} build permission for ${username}?`)) return;
try {
const res = await fetch(`/api/users/${userId}/can-build`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ canBuild: newVal }),
});
const data = await res.json();
if (res.ok) {
await loadUsers();
} else {
alert(data.error || "Failed to update build permission");
}
} catch (err) {
console.error("Toggle build permission error:", err);
alert("Network error. Please try again.");
}
};
getCurrentUser();
loadUsers();
+30
View File
@@ -104,6 +104,36 @@
</button>
</form>
<div id="telegram-settings" class="space-y-3 pt-1 border-t border-slate-800 pt-4">
<div>
<h3 class="font-medium">Notification Settings</h3>
<p class="text-xs text-slate-400">
Receive Telegram notifications for clients you have access to.
</p>
</div>
<label class="block">
<span class="text-sm text-slate-300">Your Telegram Chat ID</span>
<input
id="my-telegram-chat-id"
type="text"
placeholder="e.g. 123456789 (leave empty to disable)"
class="mt-1 w-full px-3 py-2 rounded-lg bg-slate-900 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-sky-500"
/>
</label>
<p class="text-xs text-slate-400">
Send <code>/start</code> to the Telegram bot, then use <code>/id</code> or
<a href="https://t.me/userinfobot" target="_blank" rel="noopener" class="text-sky-400 hover:underline">@userinfobot</a>
to find your chat ID.
</p>
<button
type="button"
id="save-my-telegram"
class="px-4 py-2 rounded-lg bg-cyan-700 hover:bg-cyan-600 transition-colors text-white font-medium"
>
<i class="fa-brands fa-telegram mr-2"></i>Save Telegram Settings
</button>
</div>
<form id="security-form" class="space-y-3 pt-1 border-t border-slate-800 pt-4">
<div>
<h3 class="font-medium">Security Policy</h3>