Add owner role and admin management controls

This commit is contained in:
2026-02-08 19:01:58 +01:00
parent 97f1b30b75
commit 1c59d68a50
25 changed files with 540 additions and 9 deletions

View File

@@ -72,6 +72,21 @@ export function renderAdminVoteStatus() {
jokerButton.textContent = v.hasJoker ? "🎟" : t("admin.grantJokerChip");
jokerCell.appendChild(jokerButton);
const adminCell = document.createElement("td");
if (v.isOwner) {
const ownerLabel = document.createElement("span");
ownerLabel.className = "muted small";
ownerLabel.textContent = t("admin.owner");
adminCell.appendChild(ownerLabel);
} else {
const adminCheckbox = document.createElement("input");
adminCheckbox.type = "checkbox";
adminCheckbox.dataset.setPlayerAdmin = v.playerId;
adminCheckbox.checked = !!v.isAdmin;
adminCheckbox.setAttribute("aria-label", t("admin.playerAdmin"));
adminCell.appendChild(adminCheckbox);
}
const deleteCell = document.createElement("td");
const deleteButton = document.createElement("button");
deleteButton.className = "chip danger-chip";
@@ -87,6 +102,7 @@ export function renderAdminVoteStatus() {
statusCell,
countCell,
jokerCell,
adminCell,
deleteCell,
);
table.appendChild(tr);

View File

@@ -34,6 +34,7 @@ async function request(path, { method = "GET", body } = {}) {
export const api = {
state: () => request("/api/state"),
me: () => request("/api/me"),
authOptions: () => request("/api/auth/options"),
register: (payload) => request("/api/auth/register", { method: "POST", body: payload }),
login: (payload) => request("/api/auth/login", { method: "POST", body: payload }),
logout: () => request("/api/auth/logout", { method: "POST" }),
@@ -61,6 +62,11 @@ export const adminApi = {
factoryReset: (password) =>
request("/api/admin/factory-reset", { method: "POST", body: { password } }),
grantJoker: (playerId) => request("/api/admin/joker", { method: "POST", body: { playerId } }),
setPlayerAdmin: (playerId, isAdmin) =>
request("/api/admin/player-admin", {
method: "POST",
body: { playerId, isAdmin },
}),
setPlayerPhase: (playerId, phase) =>
request("/api/admin/player-phase", { method: "POST", body: { playerId, phase } }),
deletePlayer: (playerId, password) =>

View File

@@ -127,6 +127,7 @@ function setupPlayerTableActions(runSerializedRefresh) {
const playerTable = $("admin-player-table");
if (!playerTable) return;
const phaseSelectSelector = "[data-set-player-phase]";
const adminCheckboxSelector = "[data-set-player-admin]";
playerTable.addEventListener("focusin", (e) => {
if (e.target.matches?.(phaseSelectSelector)) {
@@ -144,6 +145,25 @@ function setupPlayerTableActions(runSerializedRefresh) {
});
playerTable.addEventListener("change", async (e) => {
const adminCheckbox = e.target.closest(adminCheckboxSelector);
if (adminCheckbox) {
const playerId = adminCheckbox.dataset.setPlayerAdmin;
if (!playerId) return;
const previous = !adminCheckbox.checked;
adminCheckbox.disabled = true;
try {
await adminApi.setPlayerAdmin(playerId, adminCheckbox.checked);
toast(t("admin.roleUpdated"));
await runSerializedRefresh();
} catch (err) {
adminCheckbox.checked = previous;
toast(err.message, true);
} finally {
adminCheckbox.disabled = false;
}
return;
}
const select = e.target.closest(phaseSelectSelector);
if (!select) return;
const playerId = select.dataset.setPlayerPhase;

View File

@@ -46,6 +46,29 @@ function setupAuthModeToggle() {
setAuthMode(state.authMode);
}
function applyRegistrationOptions(ownerExists) {
state.ownerExists = !!ownerExists;
const adminKeyField = $("register-admin-key-field");
const adminKeyInput = $("register-adminkey");
if (!adminKeyField || !adminKeyInput) return;
const hideAdminKeyInput = state.ownerExists;
adminKeyField.classList.toggle("hidden", hideAdminKeyInput);
adminKeyInput.disabled = hideAdminKeyInput;
if (hideAdminKeyInput) {
adminKeyInput.value = "";
}
}
async function refreshRegistrationOptions() {
try {
const options = await api.authOptions();
applyRegistrationOptions(options?.ownerExists);
} catch {
applyRegistrationOptions(false);
}
}
function setupLoginUserEditingHint() {
const loginUser = $("login-username");
if (!loginUser) return;
@@ -121,6 +144,7 @@ function setupRegisterFormHandlers({
return toast(t("auth.cookieRequired"), true);
try {
await api.register({ username, password, displayName, adminKey });
await refreshRegistrationOptions();
setConsent();
toggleConsentRows();
setSavedUsername(username);
@@ -152,6 +176,7 @@ function setupLogoutHandler() {
clearUserState();
state.isAuthenticated = false;
setAuthUI(false);
await refreshRegistrationOptions();
});
}
@@ -178,6 +203,7 @@ function setupSuggestionEntryButtons() {
export function setupAuthHandlers({ runSerializedRefresh }) {
setupAuthModeToggle();
refreshRegistrationOptions();
const consent = setupConsentRows();
setupLoginUserEditingHint();
setupLoginFormHandlers({ ...consent, runSerializedRefresh });

View File

@@ -1,5 +1,6 @@
export const state = {
isAuthenticated: false,
ownerExists: false,
authMode: "login",
me: null,
phase: null,
@@ -19,6 +20,7 @@ export const state = {
};
export function clearUserState() {
state.ownerExists = false;
state.me = null;
state.phase = null;
state.prevPhase = null;