From 16e44c5f793c4dd9b389fc9337fced46d45ca0cc Mon Sep 17 00:00:00 2001 From: rajchales-monito Date: Wed, 29 Jul 2026 22:33:22 +0200 Subject: [PATCH] Add chat nick to admin users --- public/admin.css | 42 +++++++++++++++++++++++++++++++++++++----- public/admin.html | 3 +++ public/admin.js | 34 +++++++++++++++++++++++++++++----- server.js | 39 +++++++++++++++++++++++++-------------- 4 files changed, 94 insertions(+), 24 deletions(-) diff --git a/public/admin.css b/public/admin.css index 847a862..f41d09f 100644 --- a/public/admin.css +++ b/public/admin.css @@ -280,13 +280,23 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } .admin-user-form { grid-column: 1 / -1; display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; + padding: 10px; + border: 1px solid var(--line); + border-radius: 8px; + background: #fbfdfb; } -.admin-user-form label:nth-child(3), +.admin-user-form label { + min-width: 0; +} +.admin-user-form label:nth-child(4), .admin-user-form button { grid-column: 1 / -1; } +.admin-user-form button { + padding: 10px 12px; +} .admin-users-list { grid-column: 1 / -1; display: grid; @@ -294,10 +304,10 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } } .admin-user-item { display: grid; - grid-template-columns: minmax(0, 1fr) auto auto auto auto; + grid-template-columns: minmax(0, 1fr); align-items: center; gap: 8px; - padding: 9px 10px; + padding: 10px; border: 1px solid var(--line); border-radius: 8px; background: #fff; @@ -311,7 +321,19 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } color: var(--muted); font-size: 12px; } +.admin-user-item .admin-user-actions { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.admin-user-item button { + padding: 7px 9px; + font-size: 12px; +} .password-mask { + display: inline-flex; + align-items: center; + min-height: 30px; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; letter-spacing: 1px; } @@ -322,6 +344,16 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } #adminUsersStatus { grid-column: 1 / -1; } +@media (max-width: 1100px) { + .admin-user-form { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +@media (max-width: 720px) { + .admin-user-form { + grid-template-columns: 1fr; + } +} .list-tools { padding: 10px 14px; border-bottom: 1px solid var(--line); diff --git a/public/admin.html b/public/admin.html index 1e7daa9..e9db091 100644 --- a/public/admin.html +++ b/public/admin.html @@ -206,6 +206,9 @@ + diff --git a/public/admin.js b/public/admin.js index 0cc5be1..7c8c09c 100644 --- a/public/admin.js +++ b/public/admin.js @@ -507,6 +507,7 @@ async function createAdminUser(event) { body: { username: data.get("username"), displayName: data.get("displayName"), + chatNick: data.get("chatNick"), password: data.get("password") } }).catch((error) => error); @@ -523,6 +524,7 @@ async function createAdminUser(event) { async function handleAdminUserClick(event) { const resetButton = event.target.closest("[data-reset-admin-user]"); const renameButton = event.target.closest("[data-rename-admin-user]"); + const nickButton = event.target.closest("[data-nick-admin-user]"); const deleteButton = event.target.closest("[data-delete-admin-user]"); if (renameButton) { const id = renameButton.dataset.renameAdminUser; @@ -543,6 +545,25 @@ async function handleAdminUserClick(event) { } setAdminUsersStatus(adminUserError(result)); } + if (nickButton) { + const id = nickButton.dataset.nickAdminUser; + const user = state.adminUsers.find((item) => String(item.id) === String(id)); + if (!user || user.isSystem) return; + const chatNick = window.prompt(`Nick v chatu pro ${user.username}`, user.chatNick || user.displayName || user.username); + if (chatNick === null) return; + setAdminUsersStatus("Ukladam nick..."); + const result = await api(`/api/admin/users/${id}`, { + method: "PATCH", + body: { chatNick } + }).catch((error) => error); + if (result?.users) { + state.adminUsers = result.users; + renderAdminUsers(); + setAdminUsersStatus("Nick ulozen."); + return; + } + setAdminUsersStatus(adminUserError(result)); + } if (resetButton) { const id = resetButton.dataset.resetAdminUser; const user = state.adminUsers.find((item) => String(item.id) === String(id)); @@ -584,12 +605,15 @@ function renderAdminUsers() {
${escapeHtml(user.displayName || user.username)} - ${escapeHtml(user.username)} · ${user.isSystem ? "systemovy .env ucet" : "BO ucet"} + ${escapeHtml(user.username)} · chat: ${escapeHtml(user.chatNick || user.displayName || user.username)} · ${user.isSystem ? "systemovy .env ucet" : "BO ucet"} +
+ - ${user.hasPassword ? "••••••••" : "bez hesla"} - - -
`).join("") : `

Zatim tu nejsou zadni BO uzivatele.

`; } diff --git a/server.js b/server.js index 6406afd..6ea5977 100644 --- a/server.js +++ b/server.js @@ -128,6 +128,7 @@ function initDb() { company_id INTEGER NOT NULL REFERENCES companies(id), username TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, + chat_nick TEXT, password_hash TEXT, is_system INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -237,6 +238,7 @@ function initDb() { ensureColumn("messages", "translation_status", "TEXT"); ensureColumn("messages", "translation_error", "TEXT"); ensureColumn("admin_users", "password_hash", "TEXT"); + ensureColumn("admin_users", "chat_nick", "TEXT"); ensureColumn("admin_users", "is_system", "INTEGER NOT NULL DEFAULT 0"); ensureColumn("admin_users", "updated_at", "TEXT"); db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_client_id ON messages(conversation_id, client_message_id) WHERE client_message_id IS NOT NULL"); @@ -272,13 +274,14 @@ function seedDefaults() { for (const user of configuredUsers()) { db.prepare(` - INSERT INTO admin_users (company_id, username, display_name, is_system, updated_at) - VALUES (?, ?, ?, 1, CURRENT_TIMESTAMP) + INSERT INTO admin_users (company_id, username, display_name, chat_nick, is_system, updated_at) + VALUES (?, ?, ?, ?, 1, CURRENT_TIMESTAMP) ON CONFLICT(username) DO UPDATE SET display_name = excluded.display_name, + chat_nick = excluded.chat_nick, is_system = 1, updated_at = CURRENT_TIMESTAMP - `).run(companyId, user.username, user.displayName || user.username); + `).run(companyId, user.username, user.displayName || user.username, user.chatNick || user.displayName || user.username); } } @@ -326,17 +329,18 @@ function configuredUsers() { const username = process.env[`USER_${i}`]; const password = process.env[`PASS_${i}`]; const displayName = process.env[`NAME_${i}`] || username; + const chatNick = process.env[`NICK_${i}`] || displayName; if (!username && !password) break; - if (username && password) users.push({ username, password, displayName }); + if (username && password) users.push({ username, password, displayName, chatNick }); } return users; } function adminProfile(username) { const configured = configuredUsers().find((user) => user.username === username); - if (configured) return { username: configured.username, displayName: configured.displayName }; - const row = db.prepare("SELECT username, display_name FROM admin_users WHERE username = ?").get(username); - return row ? { username: row.username, displayName: row.display_name } : { username, displayName: username }; + if (configured) return { username: configured.username, displayName: configured.displayName, chatNick: configured.chatNick }; + const row = db.prepare("SELECT username, display_name, chat_nick FROM admin_users WHERE username = ?").get(username); + return row ? { username: row.username, displayName: row.display_name, chatNick: row.chat_nick || row.display_name } : { username, displayName: username, chatNick: username }; } async function adminLogin(req, res) { @@ -346,12 +350,12 @@ async function adminLogin(req, res) { const configured = configuredUsers().find((u) => u.username === username && u.password === password); const dbUser = configured ? null : db.prepare("SELECT * FROM admin_users WHERE username = ? AND password_hash IS NOT NULL").get(username); const match = configured || (dbUser && verifyPassword(password, dbUser.password_hash) - ? { username: dbUser.username, displayName: dbUser.display_name } + ? { username: dbUser.username, displayName: dbUser.display_name, chatNick: dbUser.chat_nick || dbUser.display_name } : null); if (!match) return json(res, 401, { error: "bad_credentials" }); const token = signSession(match.username); res.setHeader("Set-Cookie", `${COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${60 * 60 * 24 * 14}`); - return json(res, 200, { ok: true, user: { username: match.username, displayName: match.displayName } }); + return json(res, 200, { ok: true, user: { username: match.username, displayName: match.displayName, chatNick: match.chatNick } }); } function adminLogout(res) { @@ -485,14 +489,15 @@ async function createAdminUser(req, res) { const companyId = ensureCompany(); const username = normalizeAdminUsername(body.username); const displayName = normalizeDisplayName(body.displayName) || username; + const chatNick = normalizeDisplayName(body.chatNick) || displayName; const password = String(body.password || ""); if (!username) return json(res, 400, { error: "bad_username" }); if (!validAdminPassword(password)) return json(res, 400, { error: "bad_password" }); try { db.prepare(` - INSERT INTO admin_users (company_id, username, display_name, password_hash, is_system, updated_at) - VALUES (?, ?, ?, ?, 0, CURRENT_TIMESTAMP) - `).run(companyId, username, displayName, hashPassword(password)); + INSERT INTO admin_users (company_id, username, display_name, chat_nick, password_hash, is_system, updated_at) + VALUES (?, ?, ?, ?, ?, 0, CURRENT_TIMESTAMP) + `).run(companyId, username, displayName, chatNick, hashPassword(password)); return json(res, 201, { users: listAdminUsers() }); } catch (error) { if (String(error.message || "").includes("UNIQUE")) return json(res, 409, { error: "username_exists" }); @@ -507,11 +512,16 @@ async function updateAdminUser(req, res, url) { if (!user) return json(res, 404, { error: "user_not_found" }); const displayName = normalizeDisplayName(body.displayName); + const chatNick = normalizeDisplayName(body.chatNick); const password = String(body.password || ""); if (displayName) { if (user.is_system) return json(res, 400, { error: "system_user_profile_env" }); db.prepare("UPDATE admin_users SET display_name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(displayName, id); } + if (chatNick) { + if (user.is_system) return json(res, 400, { error: "system_user_profile_env" }); + db.prepare("UPDATE admin_users SET chat_nick = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(chatNick, id); + } if (password) { if (user.is_system) return json(res, 400, { error: "system_user_password_env" }); if (!validAdminPassword(password)) return json(res, 400, { error: "bad_password" }); @@ -532,13 +542,14 @@ function deleteAdminUser(req, res, url) { function listAdminUsers() { return db.prepare(` - SELECT id, username, display_name, password_hash IS NOT NULL AS has_password, is_system, created_at, updated_at + SELECT id, username, display_name, chat_nick, password_hash IS NOT NULL AS has_password, is_system, created_at, updated_at FROM admin_users ORDER BY username COLLATE NOCASE ASC `).all().map((user) => ({ id: user.id, username: user.username, displayName: user.display_name, + chatNick: user.chat_nick || user.display_name, hasPassword: Boolean(user.has_password || user.is_system), isSystem: Boolean(user.is_system), createdAt: user.created_at, @@ -774,7 +785,7 @@ async function createAdminMessage(req, res, url) { if (!attachmentsAreAllowed(body.attachments || [])) return json(res, 400, { error: "only_images_allowed" }); const conversation = findConversation(publicIdFrom(url)); if (!conversation) return json(res, 404, { error: "conversation_not_found" }); - const messageId = insertMessage(conversation.id, "admin", req.adminUser.displayName || req.adminUser.username, String(body.message || "").trim()); + const messageId = insertMessage(conversation.id, "admin", req.adminUser.chatNick || req.adminUser.displayName || req.adminUser.username, String(body.message || "").trim()); saveAttachments(conversation.id, messageId, body.attachments || []); await translateStoredMessage(conversation.id, messageId, "admin", body); bumpConversation(conversation.id, conversation.status === "new" ? "open" : conversation.status);