Add chat nick to admin users
This commit is contained in:
+37
-5
@@ -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);
|
||||
|
||||
@@ -206,6 +206,9 @@
|
||||
<label>Jmeno
|
||||
<input name="displayName" autocomplete="off" placeholder="Petr">
|
||||
</label>
|
||||
<label>Nick v chatu
|
||||
<input name="chatNick" autocomplete="off" placeholder="Petr">
|
||||
</label>
|
||||
<label>Heslo
|
||||
<input name="password" type="password" autocomplete="new-password" placeholder="min. 8 znaku" required>
|
||||
</label>
|
||||
|
||||
+25
-1
@@ -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() {
|
||||
<article class="admin-user-item">
|
||||
<div>
|
||||
<strong>${escapeHtml(user.displayName || user.username)}</strong>
|
||||
<span>${escapeHtml(user.username)} · ${user.isSystem ? "systemovy .env ucet" : "BO ucet"}</span>
|
||||
<span>${escapeHtml(user.username)} · chat: ${escapeHtml(user.chatNick || user.displayName || user.username)} · ${user.isSystem ? "systemovy .env ucet" : "BO ucet"}</span>
|
||||
</div>
|
||||
<div class="admin-user-actions">
|
||||
<span class="password-mask" title="Heslo se nezobrazuje">${user.hasPassword ? "••••••••" : "bez hesla"}</span>
|
||||
<button type="button" data-rename-admin-user="${user.id}" ${user.isSystem ? "disabled" : ""}>Jmeno</button>
|
||||
<button type="button" data-nick-admin-user="${user.id}" ${user.isSystem ? "disabled" : ""}>Nick</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>
|
||||
</div>
|
||||
</article>
|
||||
`).join("") : `<p class="empty">Zatim tu nejsou zadni BO uzivatele.</p>`;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user