diff --git a/.env.example b/.env.example index eb97c72..50a2855 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,7 @@ AUTH_SECRET=change-me-generate-long-random-secret # Admin users, extend with USER_2/PASS_2, USER_3/PASS_3, ... USER_1=admin PASS_1=change-me +NAME_1=Alex # Seed first company/site DEFAULT_COMPANY_NAME=MAAL diff --git a/public/widget.js b/public/widget.js index 8b92273..ffd812c 100644 --- a/public/widget.js +++ b/public/widget.js @@ -252,13 +252,22 @@ function renderConversation(messages, data) { messages.innerHTML = ""; + let lastAdminName = null; for (const message of data.messages) { - addMessage(messages, message.senderType, message.body, message.attachments || []); + addMessage(messages, message.senderType, message.senderName, message.body, message.attachments || [], lastAdminName); + if (message.senderType === "admin") lastAdminName = message.senderName || "Operator"; + else lastAdminName = null; } messages.scrollTop = messages.scrollHeight; } - function addMessage(messages, sender, body, attachments) { + function addMessage(messages, sender, senderName, body, attachments, lastAdminName) { + if (sender === "admin" && (senderName || "Operator") !== lastAdminName) { + const label = document.createElement("div"); + label.className = "mf-operator-label"; + label.textContent = `${senderName || "Operator"} odpovida`; + messages.append(label); + } const item = document.createElement("article"); item.className = `mf-message ${sender}`; item.innerHTML = `${body ? `

${escapeHtml(body)}

` : ""}${attachments.map((a) => `${escapeHtml(a.name)}`).join("")}`; @@ -367,6 +376,7 @@ .mf-message { max-width: 86%; padding: 9px 11px; border-radius: 8px; background: #f1f5f2; color: ${c.text}; } .mf-message.admin { align-self: flex-start; background: #eef7f4; } .mf-message.visitor { align-self: flex-end; background: ${c.brand}; color: ${c.brandText}; } + .mf-operator-label { align-self: flex-start; margin: 4px 0 -4px; color: ${c.muted}; font-size: 12px; font-weight: 700; } .mf-message p { margin: 0; white-space: pre-wrap; } .mf-message a { color: inherit; display: inline-block; margin-top: 6px; } .mf-photo img { display: block; width: min(180px, 100%); max-height: 130px; object-fit: cover; border-radius: 6px; } diff --git a/server.js b/server.js index c4c0e41..1a16c44 100644 --- a/server.js +++ b/server.js @@ -229,19 +229,24 @@ function configuredUsers() { for (let i = 1; i < 50; i += 1) { const username = process.env[`USER_${i}`]; const password = process.env[`PASS_${i}`]; + const displayName = process.env[`NAME_${i}`] || username; if (!username && !password) break; - if (username && password) users.push({ username, password }); + if (username && password) users.push({ username, password, displayName }); } return users; } +function adminProfile(username) { + return configuredUsers().find((user) => user.username === username) || { username, displayName: username }; +} + async function adminLogin(req, res) { const body = await readJson(req); const match = configuredUsers().find((u) => u.username === body.username && u.password === body.password); 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 } }); + return json(res, 200, { ok: true, user: { username: match.username, displayName: match.displayName } }); } function adminLogout(res) { @@ -253,7 +258,7 @@ function requireAdmin(req, res, next) { const token = parseCookies(req.headers.cookie || "")[COOKIE_NAME]; const username = token && verifySession(token); if (!username) return json(res, 401, { error: "unauthorized" }); - req.adminUser = { username }; + req.adminUser = adminProfile(username); return next(); } @@ -423,7 +428,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.username, String(body.message || "").trim()); + const messageId = insertMessage(conversation.id, "admin", req.adminUser.displayName || req.adminUser.username, String(body.message || "").trim()); saveAttachments(conversation.id, messageId, body.attachments || []); bumpConversation(conversation.id, conversation.status === "new" ? "open" : conversation.status); markConversationSeen(conversation.id);