Show operator name groups in widget
This commit is contained in:
@@ -11,6 +11,7 @@ AUTH_SECRET=change-me-generate-long-random-secret
|
|||||||
# Admin users, extend with USER_2/PASS_2, USER_3/PASS_3, ...
|
# Admin users, extend with USER_2/PASS_2, USER_3/PASS_3, ...
|
||||||
USER_1=admin
|
USER_1=admin
|
||||||
PASS_1=change-me
|
PASS_1=change-me
|
||||||
|
NAME_1=Alex
|
||||||
|
|
||||||
# Seed first company/site
|
# Seed first company/site
|
||||||
DEFAULT_COMPANY_NAME=MAAL
|
DEFAULT_COMPANY_NAME=MAAL
|
||||||
|
|||||||
+12
-2
@@ -252,13 +252,22 @@
|
|||||||
|
|
||||||
function renderConversation(messages, data) {
|
function renderConversation(messages, data) {
|
||||||
messages.innerHTML = "";
|
messages.innerHTML = "";
|
||||||
|
let lastAdminName = null;
|
||||||
for (const message of data.messages) {
|
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;
|
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");
|
const item = document.createElement("article");
|
||||||
item.className = `mf-message ${sender}`;
|
item.className = `mf-message ${sender}`;
|
||||||
item.innerHTML = `${body ? `<p>${escapeHtml(body)}</p>` : ""}${attachments.map((a) => `<a class="mf-photo" href="${apiBase}${a.url}" target="_blank" rel="noreferrer"><img src="${apiBase}${a.url}" alt="${escapeHtml(a.name)}"></a>`).join("")}`;
|
item.innerHTML = `${body ? `<p>${escapeHtml(body)}</p>` : ""}${attachments.map((a) => `<a class="mf-photo" href="${apiBase}${a.url}" target="_blank" rel="noreferrer"><img src="${apiBase}${a.url}" alt="${escapeHtml(a.name)}"></a>`).join("")}`;
|
||||||
@@ -367,6 +376,7 @@
|
|||||||
.mf-message { max-width: 86%; padding: 9px 11px; border-radius: 8px; background: #f1f5f2; color: ${c.text}; }
|
.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.admin { align-self: flex-start; background: #eef7f4; }
|
||||||
.mf-message.visitor { align-self: flex-end; background: ${c.brand}; color: ${c.brandText}; }
|
.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 p { margin: 0; white-space: pre-wrap; }
|
||||||
.mf-message a { color: inherit; display: inline-block; margin-top: 6px; }
|
.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; }
|
.mf-photo img { display: block; width: min(180px, 100%); max-height: 130px; object-fit: cover; border-radius: 6px; }
|
||||||
|
|||||||
@@ -229,19 +229,24 @@ function configuredUsers() {
|
|||||||
for (let i = 1; i < 50; i += 1) {
|
for (let i = 1; i < 50; i += 1) {
|
||||||
const username = process.env[`USER_${i}`];
|
const username = process.env[`USER_${i}`];
|
||||||
const password = process.env[`PASS_${i}`];
|
const password = process.env[`PASS_${i}`];
|
||||||
|
const displayName = process.env[`NAME_${i}`] || username;
|
||||||
if (!username && !password) break;
|
if (!username && !password) break;
|
||||||
if (username && password) users.push({ username, password });
|
if (username && password) users.push({ username, password, displayName });
|
||||||
}
|
}
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function adminProfile(username) {
|
||||||
|
return configuredUsers().find((user) => user.username === username) || { username, displayName: username };
|
||||||
|
}
|
||||||
|
|
||||||
async function adminLogin(req, res) {
|
async function adminLogin(req, res) {
|
||||||
const body = await readJson(req);
|
const body = await readJson(req);
|
||||||
const match = configuredUsers().find((u) => u.username === body.username && u.password === body.password);
|
const match = configuredUsers().find((u) => u.username === body.username && u.password === body.password);
|
||||||
if (!match) return json(res, 401, { error: "bad_credentials" });
|
if (!match) return json(res, 401, { error: "bad_credentials" });
|
||||||
const token = signSession(match.username);
|
const token = signSession(match.username);
|
||||||
res.setHeader("Set-Cookie", `${COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${60 * 60 * 24 * 14}`);
|
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) {
|
function adminLogout(res) {
|
||||||
@@ -253,7 +258,7 @@ function requireAdmin(req, res, next) {
|
|||||||
const token = parseCookies(req.headers.cookie || "")[COOKIE_NAME];
|
const token = parseCookies(req.headers.cookie || "")[COOKIE_NAME];
|
||||||
const username = token && verifySession(token);
|
const username = token && verifySession(token);
|
||||||
if (!username) return json(res, 401, { error: "unauthorized" });
|
if (!username) return json(res, 401, { error: "unauthorized" });
|
||||||
req.adminUser = { username };
|
req.adminUser = adminProfile(username);
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,7 +428,7 @@ async function createAdminMessage(req, res, url) {
|
|||||||
if (!attachmentsAreAllowed(body.attachments || [])) return json(res, 400, { error: "only_images_allowed" });
|
if (!attachmentsAreAllowed(body.attachments || [])) return json(res, 400, { error: "only_images_allowed" });
|
||||||
const conversation = findConversation(publicIdFrom(url));
|
const conversation = findConversation(publicIdFrom(url));
|
||||||
if (!conversation) return json(res, 404, { error: "conversation_not_found" });
|
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 || []);
|
saveAttachments(conversation.id, messageId, body.attachments || []);
|
||||||
bumpConversation(conversation.id, conversation.status === "new" ? "open" : conversation.status);
|
bumpConversation(conversation.id, conversation.status === "new" ? "open" : conversation.status);
|
||||||
markConversationSeen(conversation.id);
|
markConversationSeen(conversation.id);
|
||||||
|
|||||||
Reference in New Issue
Block a user