Harden app security controls from audit

This commit is contained in:
2026-02-08 18:40:13 +01:00
parent a6364b0802
commit 42e60d2a5a
20 changed files with 689 additions and 109 deletions

View File

@@ -1,7 +1,7 @@
import { t } from "./i18n.js";
import { state } from "./state.js";
import { $ } from "./dom.js";
import { buildLinkOptionLabel, escapeHtml, truncate } from "./ui-utils.js";
import { buildLinkOptionLabel, truncate } from "./ui-utils.js";
function displayPlayerStatus(player) {
if (!player) return "";
@@ -16,14 +16,24 @@ function displayPlayerStatus(player) {
}
function buildStatusSelect(player) {
const statusText = displayPlayerStatus(player);
const canMoveToSuggest = player.phase === "Vote";
return `
<select class="chip admin-status-select" data-set-player-phase="${player.playerId}" aria-label="${t("admin.playerStatus")}">
<option value="" selected>${statusText}</option>
<option value="Suggest" ${canMoveToSuggest ? "" : "disabled"}>${t("admin.statusMoveToSuggest")}</option>
</select>
`;
const select = document.createElement("select");
select.className = "chip admin-status-select";
select.dataset.setPlayerPhase = player.playerId;
select.setAttribute("aria-label", t("admin.playerStatus"));
const current = document.createElement("option");
current.value = "";
current.selected = true;
current.textContent = displayPlayerStatus(player);
const suggest = document.createElement("option");
suggest.value = "Suggest";
suggest.disabled = !canMoveToSuggest;
suggest.textContent = t("admin.statusMoveToSuggest");
select.append(current, suggest);
return select;
}
export function renderAdminVoteStatus() {
@@ -36,17 +46,49 @@ export function renderAdminVoteStatus() {
table.innerHTML = "";
state.adminVoteStatus.voters.forEach((v) => {
const tr = document.createElement("tr");
const gamesTooltip = escapeHtml((v.suggestionTitles || []).join(", "));
const nameText = escapeHtml(truncate(v.name, 28));
const userText = escapeHtml(truncate(v.username, 24));
tr.innerHTML = `
<td title="${escapeHtml(v.name)}">${nameText}</td>
<td class="muted small" title="${escapeHtml(v.username)}">${userText}</td>
<td>${buildStatusSelect(v)}</td>
<td title="${gamesTooltip}">${v.suggestionCount ?? 0}</td>
<td><button class="chip" data-grant-joker="${v.playerId}" type="button">${v.hasJoker ? "🎟" : t("admin.grantJokerChip")}</button></td>
<td><button class="chip danger-chip" data-delete-player="${v.playerId}" data-name="${v.name}" type="button">✕</button></td>
`;
const gamesTooltip = (v.suggestionTitles || []).join(", ");
const nameCell = document.createElement("td");
nameCell.title = v.name ?? "";
nameCell.textContent = truncate(v.name, 28);
const usernameCell = document.createElement("td");
usernameCell.className = "muted small";
usernameCell.title = v.username ?? "";
usernameCell.textContent = truncate(v.username, 24);
const statusCell = document.createElement("td");
statusCell.appendChild(buildStatusSelect(v));
const countCell = document.createElement("td");
countCell.title = gamesTooltip;
countCell.textContent = String(v.suggestionCount ?? 0);
const jokerCell = document.createElement("td");
const jokerButton = document.createElement("button");
jokerButton.className = "chip";
jokerButton.dataset.grantJoker = v.playerId;
jokerButton.type = "button";
jokerButton.textContent = v.hasJoker ? "🎟" : t("admin.grantJokerChip");
jokerCell.appendChild(jokerButton);
const deleteCell = document.createElement("td");
const deleteButton = document.createElement("button");
deleteButton.className = "chip danger-chip";
deleteButton.dataset.deletePlayer = v.playerId;
deleteButton.dataset.name = v.name ?? "";
deleteButton.type = "button";
deleteButton.textContent = "✕";
deleteCell.appendChild(deleteButton);
tr.append(
nameCell,
usernameCell,
statusCell,
countCell,
jokerCell,
deleteCell,
);
table.appendChild(tr);
});

View File

@@ -1,18 +1,28 @@
import { t } from "./i18n.js";
import { toast } from "./dom.js";
import { escapeHtml } from "./ui-utils.js";
export function openLightbox(url, title) {
const overlay = document.createElement("div");
overlay.className = "lightbox";
const safeTitle = escapeHtml(title || "");
overlay.innerHTML = `
<div class="lightbox-content">
<button class="lightbox-close" aria-label="${t("lightbox.close")}">✕</button>
<img src="${url}" alt="${safeTitle}" />
<p>${safeTitle}</p>
</div>
`;
const content = document.createElement("div");
content.className = "lightbox-content";
const closeBtn = document.createElement("button");
closeBtn.className = "lightbox-close";
closeBtn.setAttribute("aria-label", t("lightbox.close"));
closeBtn.type = "button";
closeBtn.textContent = "✕";
const image = document.createElement("img");
image.src = url ?? "";
image.alt = title ?? "";
const caption = document.createElement("p");
caption.textContent = title ?? "";
content.append(closeBtn, image, caption);
overlay.appendChild(content);
overlay.addEventListener("click", (e) => {
if (
e.target.classList.contains("lightbox") ||
@@ -38,15 +48,28 @@ export function openConfirmModal({
overlay.className = "edit-modal";
const panel = document.createElement("div");
panel.className = "edit-panel";
panel.innerHTML = `
<div class="edit-header">
<h3>${title}</h3>
<button class="lightbox-close" aria-label="${t("modal.close")}">x</button>
</div>
<div class="edit-body">
<p>${body}</p>
</div>
`;
const header = document.createElement("div");
header.className = "edit-header";
const heading = document.createElement("h3");
heading.textContent = title ?? "";
const closeBtn = document.createElement("button");
closeBtn.className = "lightbox-close";
closeBtn.setAttribute("aria-label", t("modal.close"));
closeBtn.type = "button";
closeBtn.textContent = "x";
header.append(heading, closeBtn);
const bodyWrap = document.createElement("div");
bodyWrap.className = "edit-body";
const bodyText = document.createElement("p");
bodyText.textContent = body ?? "";
bodyWrap.appendChild(bodyText);
panel.append(header, bodyWrap);
const close = () => overlay.remove();
const actions = document.createElement("div");
actions.className = "stack horizontal confirm-actions";
@@ -63,7 +86,7 @@ export function openConfirmModal({
actions.append(cancelBtn);
cancelBtn.addEventListener("click", close);
}
const bodyContainer = panel.querySelector(".edit-body");
const bodyContainer = bodyWrap;
let passwordInput = null;
if (requirePassword && bodyContainer) {
const field = document.createElement("label");