Revert "Implement admin back-pass flow and guarded admin actions"
This reverts commit 5595bfd3b1.
This commit is contained in:
@@ -15,27 +15,6 @@ function displayPlayerStatus(player) {
|
||||
return phase;
|
||||
}
|
||||
|
||||
function renderStatusSelect(player) {
|
||||
const statusText = displayPlayerStatus(player);
|
||||
const safeStatusText = escapeHtml(statusText);
|
||||
const playerId = escapeHtml(player.playerId);
|
||||
|
||||
if (player.phase === "Vote") {
|
||||
return `
|
||||
<select class="admin-status-select" data-player-phase="${playerId}" data-current-phase="Vote">
|
||||
<option value="Vote" selected>${safeStatusText}</option>
|
||||
<option value="Suggest">${escapeHtml(t("admin.statusSuggesting"))}</option>
|
||||
</select>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<select class="admin-status-select" disabled data-player-phase="${playerId}" data-current-phase="${escapeHtml(player.phase)}">
|
||||
<option value="${escapeHtml(player.phase)}" selected>${safeStatusText}</option>
|
||||
</select>
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderAdminVoteStatus() {
|
||||
if (!state.me?.isAdmin) return;
|
||||
const statusBadge = $("admin-ready-status");
|
||||
@@ -45,13 +24,14 @@ export function renderAdminVoteStatus() {
|
||||
table.innerHTML = "";
|
||||
state.adminVoteStatus.voters.forEach((v) => {
|
||||
const tr = document.createElement("tr");
|
||||
const statusText = displayPlayerStatus(v);
|
||||
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>${renderStatusSelect(v)}</td>
|
||||
<td>${statusText}</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>
|
||||
|
||||
@@ -56,11 +56,10 @@ export const api = {
|
||||
export const adminApi = {
|
||||
setResultsOpen: (resultsOpen) => request("/api/admin/results", { method: "POST", body: { resultsOpen } }),
|
||||
voteStatus: () => request("/api/admin/vote-status"),
|
||||
reset: (adminPassword) => request("/api/admin/reset", { method: "POST", body: { adminPassword } }),
|
||||
factoryReset: (adminPassword) => request("/api/admin/factory-reset", { method: "POST", body: { adminPassword } }),
|
||||
reset: () => request("/api/admin/reset", { method: "POST" }),
|
||||
factoryReset: () => request("/api/admin/factory-reset", { method: "POST" }),
|
||||
grantJoker: (playerId) => request("/api/admin/joker", { method: "POST", body: { playerId } }),
|
||||
deletePlayer: (playerId, adminPassword) => request(`/api/admin/players/${playerId}`, { method: "DELETE", body: { adminPassword } }),
|
||||
setPlayerPhase: (playerId, phase) => request(`/api/admin/players/${playerId}/phase`, { method: "POST", body: { phase } }),
|
||||
deletePlayer: (playerId) => request(`/api/admin/players/${playerId}`, { method: "DELETE" }),
|
||||
linkSuggestions: (sourceSuggestionId, targetSuggestionId) =>
|
||||
request("/api/admin/link-suggestions", { method: "POST", body: { sourceSuggestionId, targetSuggestionId } }),
|
||||
unlinkSuggestions: (suggestionId) =>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { t } from "./i18n.js";
|
||||
import { state } from "./state.js";
|
||||
import { $, toast } from "./dom.js";
|
||||
import {
|
||||
openPasswordConfirmModal,
|
||||
openConfirmModal,
|
||||
openResultsRelockModal,
|
||||
renderPhasePill,
|
||||
} from "./ui.js";
|
||||
@@ -13,10 +13,8 @@ async function adminAction(fn, successMessage, runSerializedRefresh) {
|
||||
await fn();
|
||||
toast(successMessage);
|
||||
await runSerializedRefresh();
|
||||
return true;
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,56 +32,24 @@ function setupAdminPanelToggle() {
|
||||
}
|
||||
|
||||
function setupResetButtons(runSerializedRefresh) {
|
||||
const askPasswordThenRun = ({
|
||||
title,
|
||||
body,
|
||||
confirmLabel,
|
||||
action,
|
||||
done,
|
||||
}) => {
|
||||
openPasswordConfirmModal({
|
||||
title,
|
||||
body,
|
||||
confirmLabel,
|
||||
onConfirm: async (password, close) => {
|
||||
const success = await adminAction(
|
||||
() => action(password),
|
||||
done,
|
||||
runSerializedRefresh,
|
||||
);
|
||||
if (success) close();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
$("reset").addEventListener("click", () =>
|
||||
askPasswordThenRun({
|
||||
title: t("admin.reset"),
|
||||
body: t("admin.resetConfirmBody"),
|
||||
confirmLabel: t("admin.reset"),
|
||||
action: (password) => adminApi.reset(password),
|
||||
done: t("admin.resetDone"),
|
||||
}),
|
||||
adminAction(adminApi.reset, t("admin.resetDone"), runSerializedRefresh),
|
||||
);
|
||||
|
||||
$("factory-reset").addEventListener("click", () =>
|
||||
askPasswordThenRun({
|
||||
title: t("admin.factoryReset"),
|
||||
body: t("admin.factoryResetConfirmBody"),
|
||||
confirmLabel: t("admin.factoryReset"),
|
||||
action: (password) => adminApi.factoryReset(password),
|
||||
done: t("admin.factoryResetDone"),
|
||||
}),
|
||||
adminAction(
|
||||
adminApi.factoryReset,
|
||||
t("admin.factoryResetDone"),
|
||||
runSerializedRefresh,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function setupResultsToggle(runSerializedRefresh) {
|
||||
const resultsToggle = $("results-open-toggle");
|
||||
const resultsToggle = $("results-open");
|
||||
if (!resultsToggle) return;
|
||||
|
||||
resultsToggle.addEventListener("click", async () => {
|
||||
const desired = !state.resultsOpen;
|
||||
resultsToggle.disabled = true;
|
||||
resultsToggle.addEventListener("change", async (e) => {
|
||||
const desired = !!e.target.checked;
|
||||
try {
|
||||
const resp = await adminApi.setResultsOpen(desired);
|
||||
const wasResultsOpen = state.resultsOpen;
|
||||
@@ -96,9 +62,8 @@ function setupResultsToggle(runSerializedRefresh) {
|
||||
toast(t("admin.resultsUpdated"));
|
||||
await runSerializedRefresh();
|
||||
} catch (err) {
|
||||
e.target.checked = !desired;
|
||||
toast(err.message, true);
|
||||
} finally {
|
||||
resultsToggle.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -127,45 +92,6 @@ function setupPlayerTableActions(runSerializedRefresh) {
|
||||
const playerTable = $("admin-player-table");
|
||||
if (!playerTable) return;
|
||||
|
||||
const syncSelectFocusState = () => {
|
||||
state.adminStatusMenuOpen = !!playerTable.querySelector(
|
||||
".admin-status-select:focus",
|
||||
);
|
||||
};
|
||||
|
||||
playerTable.addEventListener("focusin", (e) => {
|
||||
if (e.target.closest(".admin-status-select")) {
|
||||
state.adminStatusMenuOpen = true;
|
||||
}
|
||||
});
|
||||
|
||||
playerTable.addEventListener("focusout", () => {
|
||||
window.setTimeout(syncSelectFocusState, 0);
|
||||
});
|
||||
|
||||
playerTable.addEventListener("change", async (e) => {
|
||||
const statusSelect = e.target.closest(".admin-status-select");
|
||||
if (!statusSelect || statusSelect.disabled) return;
|
||||
|
||||
const playerId = statusSelect.dataset.playerPhase;
|
||||
const currentPhase = statusSelect.dataset.currentPhase;
|
||||
const desiredPhase = statusSelect.value;
|
||||
if (!playerId || !desiredPhase || desiredPhase === currentPhase) return;
|
||||
|
||||
statusSelect.disabled = true;
|
||||
try {
|
||||
await adminApi.setPlayerPhase(playerId, desiredPhase);
|
||||
toast(t("admin.statusUpdated"));
|
||||
await runSerializedRefresh();
|
||||
} catch (err) {
|
||||
statusSelect.value = currentPhase ?? statusSelect.value;
|
||||
toast(err.message, true);
|
||||
} finally {
|
||||
statusSelect.disabled = false;
|
||||
state.adminStatusMenuOpen = false;
|
||||
}
|
||||
});
|
||||
|
||||
playerTable.addEventListener("click", async (e) => {
|
||||
const grantBtn = e.target.closest("[data-grant-joker]");
|
||||
const deleteBtn = e.target.closest("[data-delete-player]");
|
||||
@@ -181,13 +107,13 @@ function setupPlayerTableActions(runSerializedRefresh) {
|
||||
} else if (deleteBtn) {
|
||||
const playerId = deleteBtn.dataset.deletePlayer;
|
||||
const name = deleteBtn.dataset.name || "";
|
||||
openPasswordConfirmModal({
|
||||
openConfirmModal({
|
||||
title: t("admin.deleteTitle"),
|
||||
body: t("admin.deleteBody", { name }),
|
||||
confirmLabel: t("admin.deleteConfirm"),
|
||||
onConfirm: async (password, close) => {
|
||||
onConfirm: async (close) => {
|
||||
try {
|
||||
await adminApi.deletePlayer(playerId, password);
|
||||
await adminApi.deletePlayer(playerId);
|
||||
toast(t("admin.deleteDone"));
|
||||
close();
|
||||
await runSerializedRefresh();
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { api } from "./api.js";
|
||||
import { t } from "./i18n.js";
|
||||
import {
|
||||
state,
|
||||
clearUserState,
|
||||
clearSavedUsername,
|
||||
setSavedUsername,
|
||||
} from "./state.js";
|
||||
import { state, clearUserState, setSavedUsername } from "./state.js";
|
||||
import { $, toast } from "./dom.js";
|
||||
import {
|
||||
handleAuthError,
|
||||
@@ -144,38 +139,24 @@ function setupLogoutHandler() {
|
||||
const logoutBtn = $("logout");
|
||||
if (!logoutBtn) return;
|
||||
|
||||
const clearAuthFormFields = () => {
|
||||
[
|
||||
"login-username",
|
||||
"login-password",
|
||||
"register-username",
|
||||
"register-password",
|
||||
"register-displayName",
|
||||
"register-adminkey",
|
||||
].forEach((id) => {
|
||||
const input = $(id);
|
||||
if (input) input.value = "";
|
||||
});
|
||||
|
||||
["login-consent", "register-consent"].forEach((id) => {
|
||||
const box = $(id);
|
||||
if (box) box.checked = false;
|
||||
});
|
||||
};
|
||||
|
||||
logoutBtn.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
const lastUser = state.me?.username;
|
||||
try {
|
||||
await api.logout();
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
}
|
||||
clearUserState();
|
||||
clearSavedUsername();
|
||||
state.isAuthenticated = false;
|
||||
setAuthMode("login");
|
||||
setAuthUI(false);
|
||||
clearAuthFormFields();
|
||||
if (lastUser) {
|
||||
setSavedUsername(lastUser);
|
||||
const loginUser = $("login-username");
|
||||
if (loginUser) loginUser.value = lastUser;
|
||||
const loginPass = $("login-password");
|
||||
if (loginPass) loginPass.value = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -34,15 +34,7 @@ export async function loadSuggestionsData() {
|
||||
const latest = await api.allSuggestions();
|
||||
const latestSig = signatureSuggestions(latest);
|
||||
const changed = latestSig !== state.allSuggestionsSig;
|
||||
const canCompareWithDisplayedVoteList =
|
||||
state.phase === "Vote" &&
|
||||
state.votesRendered &&
|
||||
!!state.displayedVoteSuggestionsSig;
|
||||
if (
|
||||
changed &&
|
||||
canCompareWithDisplayedVoteList &&
|
||||
latestSig !== state.displayedVoteSuggestionsSig
|
||||
) {
|
||||
if (changed && state.phase === "Vote" && state.allSuggestionsSig) {
|
||||
const added = latest
|
||||
.filter((s) => !prevById[s.id])
|
||||
.map((s) => s.name);
|
||||
|
||||
@@ -80,84 +80,6 @@ export function openConfirmModal({
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
export function openPasswordConfirmModal({
|
||||
title,
|
||||
body,
|
||||
confirmLabel,
|
||||
cancelLabel = t("modal.cancel"),
|
||||
onConfirm,
|
||||
}) {
|
||||
const overlay = document.createElement("div");
|
||||
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 close = () => overlay.remove();
|
||||
const bodyWrap = panel.querySelector(".edit-body");
|
||||
const fieldWrap = document.createElement("label");
|
||||
fieldWrap.className = "stack";
|
||||
fieldWrap.innerHTML = `
|
||||
<span class="label">${t("admin.passwordLabel")}</span>
|
||||
<input type="password" autocomplete="current-password" />
|
||||
`;
|
||||
bodyWrap?.appendChild(fieldWrap);
|
||||
const passwordInput = fieldWrap.querySelector("input");
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "stack horizontal";
|
||||
const confirmBtn = document.createElement("button");
|
||||
confirmBtn.className = "danger";
|
||||
confirmBtn.textContent = confirmLabel ?? t("modal.confirm");
|
||||
actions.append(confirmBtn);
|
||||
|
||||
if (cancelLabel !== null && cancelLabel !== undefined) {
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.className = "ghost";
|
||||
cancelBtn.type = "button";
|
||||
cancelBtn.textContent = cancelLabel;
|
||||
actions.append(cancelBtn);
|
||||
cancelBtn.addEventListener("click", close);
|
||||
}
|
||||
bodyWrap?.appendChild(actions);
|
||||
|
||||
overlay.addEventListener("click", (e) => {
|
||||
if (
|
||||
e.target.classList.contains("edit-modal") ||
|
||||
e.target.classList.contains("lightbox-close")
|
||||
) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
confirmBtn.addEventListener("click", async () => {
|
||||
const password = passwordInput?.value ?? "";
|
||||
if (!password.trim()) {
|
||||
toast(t("admin.passwordRequired"), true);
|
||||
passwordInput?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await onConfirm?.(password, close);
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
}
|
||||
});
|
||||
|
||||
overlay.appendChild(panel);
|
||||
document.body.appendChild(overlay);
|
||||
passwordInput?.focus();
|
||||
}
|
||||
|
||||
export function openResultsRelockModal() {
|
||||
openConfirmModal({
|
||||
title: t("results.relockedTitle"),
|
||||
|
||||
@@ -11,12 +11,10 @@ export const state = {
|
||||
mySuggestions: [],
|
||||
allSuggestions: [],
|
||||
allSuggestionsSig: null,
|
||||
displayedVoteSuggestionsSig: null,
|
||||
myVotes: [],
|
||||
results: [],
|
||||
votesRendered: false,
|
||||
adminVoteStatus: null,
|
||||
adminStatusMenuOpen: false,
|
||||
};
|
||||
|
||||
export function clearUserState() {
|
||||
@@ -29,13 +27,9 @@ export function clearUserState() {
|
||||
state.counts = null;
|
||||
state.mySuggestions = [];
|
||||
state.allSuggestions = [];
|
||||
state.allSuggestionsSig = null;
|
||||
state.displayedVoteSuggestionsSig = null;
|
||||
state.myVotes = [];
|
||||
state.results = [];
|
||||
state.votesRendered = false;
|
||||
state.adminVoteStatus = null;
|
||||
state.adminStatusMenuOpen = false;
|
||||
const adminCard = document.getElementById("admin-card");
|
||||
if (adminCard) adminCard.classList.add("hidden");
|
||||
}
|
||||
@@ -44,5 +38,3 @@ export const getSavedUsername = () =>
|
||||
localStorage.getItem("last_username") || "";
|
||||
export const setSavedUsername = (name) =>
|
||||
localStorage.setItem("last_username", name);
|
||||
export const clearSavedUsername = () =>
|
||||
localStorage.removeItem("last_username");
|
||||
|
||||
@@ -27,7 +27,6 @@ import { renderResults } from "./results-ui.js";
|
||||
import {
|
||||
openConfirmModal,
|
||||
openLightbox,
|
||||
openPasswordConfirmModal,
|
||||
openResultsRelockModal,
|
||||
openSuggestionsChangedModal,
|
||||
} from "./modals-ui.js";
|
||||
@@ -65,7 +64,6 @@ export {
|
||||
openConfirmModal,
|
||||
openLightbox,
|
||||
openNewSuggestionModal,
|
||||
openPasswordConfirmModal,
|
||||
openResultsRelockModal,
|
||||
openSuggestionsChangedModal,
|
||||
renderAllSuggestions,
|
||||
|
||||
@@ -42,7 +42,6 @@ export function renderVotes() {
|
||||
li.querySelector(".card-body").appendChild(footer);
|
||||
list.appendChild(li);
|
||||
});
|
||||
state.displayedVoteSuggestionsSig = state.allSuggestionsSig;
|
||||
updatePhaseNav();
|
||||
updateMissingBadgeFromDom();
|
||||
list.scrollTop = prevScroll;
|
||||
@@ -203,11 +202,9 @@ export function updatePhaseNav() {
|
||||
|
||||
showNav("nav-suggest", phase === "Suggest");
|
||||
showNav("nav-vote", phase === "Vote");
|
||||
const playerCanMoveBackToSuggest =
|
||||
!isAdmin && phase === "Vote" && state.hasJoker;
|
||||
const jokerBtn = $("open-joker-modal");
|
||||
if (jokerBtn) {
|
||||
const showJoker = false;
|
||||
const showJoker = phase === "Vote" && state.hasJoker;
|
||||
jokerBtn.classList.toggle("hidden", !showJoker);
|
||||
jokerBtn.disabled = !showJoker;
|
||||
}
|
||||
@@ -245,14 +242,11 @@ export function updatePhaseNav() {
|
||||
renderAdminLinker();
|
||||
updateMissingBadgeFromDom();
|
||||
|
||||
const votePrev = $("nav-vote-prev");
|
||||
if (votePrev) {
|
||||
const canUseBack = isAdmin || playerCanMoveBackToSuggest;
|
||||
votePrev.classList.toggle("hidden", !canUseBack);
|
||||
votePrev.textContent = playerCanMoveBackToSuggest
|
||||
? t("nav.backToSuggestOnce")
|
||||
: t("nav.prev");
|
||||
}
|
||||
const backButtons = ["nav-vote-prev"];
|
||||
backButtons.forEach((id) => {
|
||||
const btn = $(id);
|
||||
if (btn) btn.classList.toggle("hidden", !isAdmin);
|
||||
});
|
||||
|
||||
const suggestNext = $("nav-suggest-next");
|
||||
if (suggestNext) {
|
||||
@@ -274,10 +268,8 @@ export function updatePhaseNav() {
|
||||
: t("nav.next");
|
||||
}
|
||||
|
||||
const adminResultsToggle = $("results-open-toggle");
|
||||
const adminResultsToggle = $("results-open");
|
||||
if (adminResultsToggle) {
|
||||
adminResultsToggle.textContent = state.resultsOpen
|
||||
? t("admin.resultsOpenButtonDisable")
|
||||
: t("admin.resultsOpenButtonEnable");
|
||||
adminResultsToggle.checked = !!state.resultsOpen;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user