Add admin user management

This commit is contained in:
rajchales-monito
2026-07-29 22:27:59 +02:00
parent 913b576efd
commit cac3deb11a
4 changed files with 319 additions and 8 deletions
+123 -1
View File
@@ -19,7 +19,8 @@ const state = {
attentionOn: false,
originalTitle: document.title,
originalFavicon: document.querySelector("link[rel='icon']")?.href || "/favicon.svg",
aiModels: []
aiModels: [],
adminUsers: []
};
const $ = (selector) => document.querySelector(selector);
@@ -87,6 +88,8 @@ $("#openaiApiKey").addEventListener("input", markAiSettingsDirty);
$("#refreshAiModels").addEventListener("click", loadAiModels);
$("#saveAiSettings").addEventListener("click", saveTranslationSettings);
$("#testOpenAiKey").addEventListener("click", testOpenAiKey);
$("#adminUserForm").addEventListener("submit", createAdminUser);
$("#adminUsersList").addEventListener("click", handleAdminUserClick);
document.querySelectorAll("[data-settings-tab]").forEach((button) => {
button.addEventListener("click", () => {
const tab = button.dataset.settingsTab;
@@ -290,6 +293,7 @@ async function showApp() {
$("#appView").classList.remove("hidden");
await loadBootstrap();
await loadAiModels().catch(() => {});
await loadAdminUsers().catch(() => {});
await Promise.all([loadConversations(), loadVisitors()]);
connectEvents();
}
@@ -487,6 +491,124 @@ function fallbackAiModels() {
return ["gpt-5-mini", "gpt-5-nano", "gpt-5.1", "gpt-4.1-mini"];
}
async function loadAdminUsers() {
const data = await api("/api/admin/users");
state.adminUsers = data.users || [];
renderAdminUsers();
}
async function createAdminUser(event) {
event.preventDefault();
const form = event.currentTarget;
const data = new FormData(form);
setAdminUsersStatus("Ukladam uzivatele...");
const result = await api("/api/admin/users", {
method: "POST",
body: {
username: data.get("username"),
displayName: data.get("displayName"),
password: data.get("password")
}
}).catch((error) => error);
if (result?.users) {
state.adminUsers = result.users;
form.reset();
renderAdminUsers();
setAdminUsersStatus("Uzivatel pridan.");
return;
}
setAdminUsersStatus(adminUserError(result));
}
async function handleAdminUserClick(event) {
const resetButton = event.target.closest("[data-reset-admin-user]");
const renameButton = event.target.closest("[data-rename-admin-user]");
const deleteButton = event.target.closest("[data-delete-admin-user]");
if (renameButton) {
const id = renameButton.dataset.renameAdminUser;
const user = state.adminUsers.find((item) => String(item.id) === String(id));
if (!user || user.isSystem) return;
const displayName = window.prompt(`Jmeno operatora pro ${user.username}`, user.displayName || user.username);
if (displayName === null) return;
setAdminUsersStatus("Ukladam jmeno...");
const result = await api(`/api/admin/users/${id}`, {
method: "PATCH",
body: { displayName }
}).catch((error) => error);
if (result?.users) {
state.adminUsers = result.users;
renderAdminUsers();
setAdminUsersStatus("Jmeno ulozeno.");
return;
}
setAdminUsersStatus(adminUserError(result));
}
if (resetButton) {
const id = resetButton.dataset.resetAdminUser;
const user = state.adminUsers.find((item) => String(item.id) === String(id));
if (!user || user.isSystem) return;
const password = window.prompt(`Nove heslo pro ${user.username} (min. 8 znaku)`);
if (!password) return;
setAdminUsersStatus("Menim heslo...");
const result = await api(`/api/admin/users/${id}`, {
method: "PATCH",
body: { password }
}).catch((error) => error);
if (result?.users) {
state.adminUsers = result.users;
renderAdminUsers();
setAdminUsersStatus("Heslo zmeneno.");
return;
}
setAdminUsersStatus(adminUserError(result));
}
if (deleteButton) {
const id = deleteButton.dataset.deleteAdminUser;
const user = state.adminUsers.find((item) => String(item.id) === String(id));
if (!user || user.isSystem) return;
if (!window.confirm(`Smazat BO uzivatele ${user.username}?`)) return;
setAdminUsersStatus("Mazu uzivatele...");
const result = await api(`/api/admin/users/${id}`, { method: "DELETE" }).catch((error) => error);
if (result?.users) {
state.adminUsers = result.users;
renderAdminUsers();
setAdminUsersStatus("Uzivatel smazan.");
return;
}
setAdminUsersStatus(adminUserError(result));
}
}
function renderAdminUsers() {
$("#adminUsersList").innerHTML = state.adminUsers.length ? state.adminUsers.map((user) => `
<article class="admin-user-item">
<div>
<strong>${escapeHtml(user.displayName || user.username)}</strong>
<span>${escapeHtml(user.username)} &middot; ${user.isSystem ? "systemovy .env ucet" : "BO ucet"}</span>
</div>
<span class="password-mask" title="Heslo se nezobrazuje">${user.hasPassword ? "&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;" : "bez hesla"}</span>
<button type="button" data-rename-admin-user="${user.id}" ${user.isSystem ? "disabled" : ""}>Jmeno</button>
<button type="button" data-reset-admin-user="${user.id}" ${user.isSystem ? "disabled" : ""}>Reset hesla</button>
<button type="button" data-delete-admin-user="${user.id}" ${user.isSystem ? "disabled" : ""}>Smazat</button>
</article>
`).join("") : `<p class="empty">Zatim tu nejsou zadni BO uzivatele.</p>`;
}
function adminUserError(error) {
if (error?.error === "bad_username") return "Login musi mit 3-60 znaku: pismena, cisla, tecka, pomlcka, podtrzitko nebo @.";
if (error?.error === "bad_password") return "Heslo musi mit aspon 8 znaku.";
if (error?.error === "username_exists") return "Tento login uz existuje.";
if (error?.error === "system_user_password_env") return "Systemovy .env ucet se meni na serveru v .env.";
if (error?.error === "system_user_profile_env") return "Systemovy .env ucet se prejmenovava na serveru v .env.";
if (error?.error === "system_user_protected") return "Systemovy .env ucet nejde smazat.";
if (error?.error === "cannot_delete_current_user") return "Nemuzes smazat uzivatele, pod kterym jsi prihlaseny.";
return `Akce se nepodarila (${error?.error || "neznama chyba"}).`;
}
function setAdminUsersStatus(message) {
$("#adminUsersStatus").textContent = message;
}
function renderConversations() {
const conversations = filteredConversations();
$("#conversationList").innerHTML = conversations.length ? conversations.map((item) => `