v2.2 - redesign admin panel, fix readme.md #58
+70
-25
@@ -102,45 +102,87 @@
|
|||||||
<div class="tab-content pt-3">
|
<div class="tab-content pt-3">
|
||||||
<div class="tab-pane fade show active" id="admin-settings" role="tabpanel" aria-labelledby="admin-settings-tab">
|
<div class="tab-pane fade show active" id="admin-settings" role="tabpanel" aria-labelledby="admin-settings-tab">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-12 col-xl-8">
|
<div class="col-12 col-xxl-8">
|
||||||
<div class="card p-3">
|
<div class="accordion" id="adminSettingsAccordion">
|
||||||
<h4 class="mb-1">Année active</h4>
|
<!-- Active year -->
|
||||||
<p class="text-muted mb-3">
|
<div class="accordion-item">
|
||||||
Cette année pilote tout le site (films affichés, checklist et statistiques).
|
<h2 class="accordion-header" id="adminSettingsHeadingYear">
|
||||||
</p>
|
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#adminSettingsCollapseYear" aria-expanded="true" aria-controls="adminSettingsCollapseYear">
|
||||||
<div class="mb-3">
|
Année active
|
||||||
<label for="active_oscar_year" class="form-label">Année active</label>
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="adminSettingsCollapseYear" class="accordion-collapse collapse show" aria-labelledby="adminSettingsHeadingYear" data-bs-parent="#adminSettingsAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div class="text-muted small mb-2">
|
||||||
|
Pilote tout le site (films, checklist, statistiques).
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap gap-2 align-items-end justify-content-between">
|
||||||
|
<div class="flex-grow-1" style="min-width: 220px;">
|
||||||
|
<label for="active_oscar_year" class="form-label mb-1">Année active</label>
|
||||||
<select class="form-select" id="active_oscar_year"></select>
|
<select class="form-select" id="active_oscar_year"></select>
|
||||||
<div class="form-text">La liste est basée sur les années présentes dans les films.</div>
|
<div class="form-text">Basé sur les années présentes dans les films.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<button type="button" id="save-active-year" class="btn btn-warning">Appliquer l’année</button>
|
<button type="button" id="save-active-year" class="btn btn-warning">Appliquer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-xl-8">
|
<!-- Player -->
|
||||||
<div class="card p-3">
|
<div class="accordion-item">
|
||||||
<h4 class="mb-1">Date des Oscars (par année)</h4>
|
<h2 class="accordion-header" id="adminSettingsHeadingPlayer">
|
||||||
<p class="text-muted mb-3">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#adminSettingsCollapsePlayer" aria-expanded="false" aria-controls="adminSettingsCollapsePlayer">
|
||||||
Configure la date exacte de la cérémonie pour le <strong>compte à rebours</strong> sur Films & Checklist.
|
Lecteur (player)
|
||||||
Si non défini, la date par défaut reste <code>15 mars</code> (comportement actuel).
|
</button>
|
||||||
</p>
|
</h2>
|
||||||
|
<div id="adminSettingsCollapsePlayer" class="accordion-collapse collapse" aria-labelledby="adminSettingsHeadingPlayer" data-bs-parent="#adminSettingsAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div class="d-flex flex-wrap gap-2 align-items-center justify-content-between">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="player_ui_show_status">
|
||||||
|
<label class="form-check-label" for="player_ui_show_status">Afficher “Source” + “Progress” (admins)</label>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" id="player_ui_reload" class="btn btn-outline-secondary btn-sm">Recharger</button>
|
||||||
|
<button type="button" id="player_ui_save" class="btn btn-warning btn-sm">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-text mt-2">
|
||||||
|
Contrôle l’affichage du bloc debug dans <code>player.html</code> (admins uniquement).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<!-- Oscar date -->
|
||||||
<div class="col-md-4">
|
<div class="accordion-item">
|
||||||
<label for="oscar_date_year" class="form-label">Année</label>
|
<h2 class="accordion-header" id="adminSettingsHeadingOscarDate">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#adminSettingsCollapseOscarDate" aria-expanded="false" aria-controls="adminSettingsCollapseOscarDate">
|
||||||
|
Date des Oscars (par année)
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="adminSettingsCollapseOscarDate" class="accordion-collapse collapse" aria-labelledby="adminSettingsHeadingOscarDate" data-bs-parent="#adminSettingsAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div class="text-muted small mb-2">
|
||||||
|
Configure la date de cérémonie pour le compte à rebours (Films & Checklist). Défaut: <code>15 mars</code>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label for="oscar_date_year" class="form-label mb-1">Année</label>
|
||||||
<select class="form-select" id="oscar_date_year"></select>
|
<select class="form-select" id="oscar_date_year"></select>
|
||||||
<div class="form-text">Basé sur les années présentes dans les films.</div>
|
<div class="form-text">Basé sur les années présentes dans les films.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
<label for="oscar_date_value" class="form-label">Date</label>
|
<label for="oscar_date_value" class="form-label mb-1">Date</label>
|
||||||
<input type="date" class="form-control" id="oscar_date_value">
|
<input type="date" class="form-control" id="oscar_date_value">
|
||||||
<div class="form-text">Format: YYYY-MM-DD.</div>
|
<div class="form-text">Format: YYYY-MM-DD.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 d-flex align-items-end justify-content-end gap-2">
|
<div class="col-12 col-md-4 d-flex align-items-end justify-content-end gap-2">
|
||||||
<button type="button" id="clear-oscar-date" class="btn btn-outline-danger">Effacer</button>
|
<button type="button" id="clear-oscar-date" class="btn btn-outline-danger btn-sm">Effacer</button>
|
||||||
<button type="button" id="save-oscar-date" class="btn btn-warning">Enregistrer</button>
|
<button type="button" id="save-oscar-date" class="btn btn-warning btn-sm">Enregistrer</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -152,6 +194,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane fade" id="admin-version" role="tabpanel" aria-labelledby="admin-version-tab">
|
<div class="tab-pane fade" id="admin-version" role="tabpanel" aria-labelledby="admin-version-tab">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ window.onload = async function () {
|
|||||||
const activeYearInput = document.getElementById('active_oscar_year');
|
const activeYearInput = document.getElementById('active_oscar_year');
|
||||||
const saveActiveYearBtn = document.getElementById('save-active-year');
|
const saveActiveYearBtn = document.getElementById('save-active-year');
|
||||||
|
|
||||||
|
// Player UI (settings tab): show/hide admin status blocks in player.html
|
||||||
|
const playerUiShowStatusEl = document.getElementById('player_ui_show_status');
|
||||||
|
const playerUiReloadBtn = document.getElementById('player_ui_reload');
|
||||||
|
const playerUiSaveBtn = document.getElementById('player_ui_save');
|
||||||
|
|
||||||
// Version control tab elements
|
// Version control tab elements
|
||||||
const adminVersionTabBtn = document.getElementById('admin-version-tab');
|
const adminVersionTabBtn = document.getElementById('admin-version-tab');
|
||||||
const appVersionAlertEl = document.getElementById('app_version_alert');
|
const appVersionAlertEl = document.getElementById('app_version_alert');
|
||||||
@@ -132,6 +137,11 @@ window.onload = async function () {
|
|||||||
bodyHtml: '',
|
bodyHtml: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const DEFAULT_PLAYER_UI = Object.freeze({
|
||||||
|
showSource: true,
|
||||||
|
showProgress: true,
|
||||||
|
});
|
||||||
|
|
||||||
function showResponse(kind, message) {
|
function showResponse(kind, message) {
|
||||||
responseEl.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning');
|
responseEl.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning');
|
||||||
responseEl.classList.add(kind === 'success' ? 'alert-success' : kind === 'warning' ? 'alert-warning' : 'alert-danger');
|
responseEl.classList.add(kind === 'success' ? 'alert-success' : kind === 'warning' ? 'alert-warning' : 'alert-danger');
|
||||||
@@ -147,6 +157,67 @@ window.onload = async function () {
|
|||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePlayerUi(value) {
|
||||||
|
const v = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
||||||
|
const showSource = v.showSource === undefined ? DEFAULT_PLAYER_UI.showSource : !!v.showSource;
|
||||||
|
const showProgress = v.showProgress === undefined ? DEFAULT_PLAYER_UI.showProgress : !!v.showProgress;
|
||||||
|
return { showSource, showProgress };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPlayerUi() {
|
||||||
|
const res = await fetch('/api/settings/player-admin-status-ui', { method: 'GET' });
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
const msg = data.message || data.error || `Erreur (${res.status})`;
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
return normalizePlayerUi(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePlayerUi(payload) {
|
||||||
|
const res = await fetch('/api/settings/player-admin-status-ui', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
const msg = data.message || data.error || `Erreur (${res.status})`;
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
return normalizePlayerUi(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPlayerUiForm(value) {
|
||||||
|
if (!playerUiShowStatusEl) return;
|
||||||
|
// Single checkbox controls both flags.
|
||||||
|
playerUiShowStatusEl.checked = !!value.showSource && !!value.showProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlayerUiFormValue() {
|
||||||
|
const enabled = !!playerUiShowStatusEl?.checked;
|
||||||
|
return normalizePlayerUi({
|
||||||
|
showSource: enabled,
|
||||||
|
showProgress: enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPlayerUiIntoUi() {
|
||||||
|
// If the section isn't present (older HTML), skip gracefully.
|
||||||
|
if (!playerUiShowStatusEl) return;
|
||||||
|
try {
|
||||||
|
const value = await fetchPlayerUi();
|
||||||
|
setPlayerUiForm(value);
|
||||||
|
} catch (err) {
|
||||||
|
// Keep defaults so admin can still set something.
|
||||||
|
setPlayerUiForm(DEFAULT_PLAYER_UI);
|
||||||
|
showResponse('warning', err.message || 'Impossible de charger les réglages du lecteur.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function copyToClipboard(text) {
|
async function copyToClipboard(text) {
|
||||||
const t = String(text || '').trim();
|
const t = String(text || '').trim();
|
||||||
if (!t) return false;
|
if (!t) return false;
|
||||||
@@ -2337,6 +2408,41 @@ window.onload = async function () {
|
|||||||
|
|
||||||
hookModal100Inputs();
|
hookModal100Inputs();
|
||||||
await loadCompletionModalIntoUi();
|
await loadCompletionModalIntoUi();
|
||||||
|
await loadPlayerUiIntoUi();
|
||||||
|
|
||||||
|
if (playerUiReloadBtn) {
|
||||||
|
playerUiReloadBtn.addEventListener('click', async () => {
|
||||||
|
playerUiReloadBtn.disabled = true;
|
||||||
|
const old = playerUiReloadBtn.textContent;
|
||||||
|
playerUiReloadBtn.textContent = 'Chargement...';
|
||||||
|
try {
|
||||||
|
await loadPlayerUiIntoUi();
|
||||||
|
showResponse('success', 'Réglages du lecteur rechargés.');
|
||||||
|
} finally {
|
||||||
|
playerUiReloadBtn.disabled = false;
|
||||||
|
playerUiReloadBtn.textContent = old;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerUiSaveBtn) {
|
||||||
|
playerUiSaveBtn.addEventListener('click', async () => {
|
||||||
|
playerUiSaveBtn.disabled = true;
|
||||||
|
const old = playerUiSaveBtn.textContent;
|
||||||
|
playerUiSaveBtn.textContent = 'Enregistrement...';
|
||||||
|
try {
|
||||||
|
const payload = getPlayerUiFormValue();
|
||||||
|
const saved = await savePlayerUi(payload);
|
||||||
|
setPlayerUiForm(saved);
|
||||||
|
showResponse('success', 'Réglages du lecteur enregistrés.');
|
||||||
|
} catch (err) {
|
||||||
|
showResponse('danger', err.message || 'Erreur réseau');
|
||||||
|
} finally {
|
||||||
|
playerUiSaveBtn.disabled = false;
|
||||||
|
playerUiSaveBtn.textContent = old;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (saveActiveYearBtn && activeYearInput) {
|
if (saveActiveYearBtn && activeYearInput) {
|
||||||
saveActiveYearBtn.addEventListener('click', async () => {
|
saveActiveYearBtn.addEventListener('click', async () => {
|
||||||
|
|||||||
+27
-4
@@ -139,6 +139,20 @@ async function fetchActiveOscarYear() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchPlayerAdminStatusUi() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/settings/player-admin-status-ui', { method: 'GET' });
|
||||||
|
if (!res.ok) return { showSource: true, showProgress: true };
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
return {
|
||||||
|
showSource: data?.showSource === undefined ? true : !!data.showSource,
|
||||||
|
showProgress: data?.showProgress === undefined ? true : !!data.showProgress,
|
||||||
|
};
|
||||||
|
} catch (_) {
|
||||||
|
return { showSource: true, showProgress: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchPlaybackProgress(movieId, token) {
|
async function fetchPlaybackProgress(movieId, token) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/movies/${encodeURIComponent(movieId)}/progress`, {
|
const res = await fetch(`/api/movies/${encodeURIComponent(movieId)}/progress`, {
|
||||||
@@ -868,8 +882,9 @@ window.onload = async function () {
|
|||||||
}
|
}
|
||||||
pageLoader.setProgress(14);
|
pageLoader.setProgress(14);
|
||||||
|
|
||||||
// Load the active Oscar year in parallel with the movie fetch.
|
// Load the active Oscar year (and player UI flags) in parallel with the movie fetch.
|
||||||
const activeYearPromise = fetchActiveOscarYear();
|
const activeYearPromise = fetchActiveOscarYear();
|
||||||
|
const playerUiPromise = fetchPlayerAdminStatusUi();
|
||||||
|
|
||||||
let decoded = null;
|
let decoded = null;
|
||||||
try {
|
try {
|
||||||
@@ -889,8 +904,6 @@ window.onload = async function () {
|
|||||||
if (decoded.admin) {
|
if (decoded.admin) {
|
||||||
const adminLink = document.getElementById('admin-control-link');
|
const adminLink = document.getElementById('admin-control-link');
|
||||||
if (adminLink) adminLink.classList.remove('d-none');
|
if (adminLink) adminLink.classList.remove('d-none');
|
||||||
const adminStatus = document.getElementById('player-admin-status');
|
|
||||||
if (adminStatus) adminStatus.classList.remove('d-none');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const logoffBtn = document.getElementById('log-off');
|
const logoffBtn = document.getElementById('log-off');
|
||||||
@@ -927,10 +940,20 @@ window.onload = async function () {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeYear = await activeYearPromise;
|
const [activeYear, playerUi] = await Promise.all([activeYearPromise, playerUiPromise]);
|
||||||
setHeader(movie, { activeYear });
|
setHeader(movie, { activeYear });
|
||||||
pageLoader.setProgress(58);
|
pageLoader.setProgress(58);
|
||||||
|
|
||||||
|
// Admin-only status block (Source + Progress) can be globally disabled via settings.
|
||||||
|
if (decoded?.admin) {
|
||||||
|
const enabled = !!playerUi?.showSource && !!playerUi?.showProgress;
|
||||||
|
const adminStatus = document.getElementById('player-admin-status');
|
||||||
|
if (adminStatus) {
|
||||||
|
if (enabled) adminStatus.classList.remove('d-none');
|
||||||
|
else adminStatus.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resolved = resolveMovieSource(movie);
|
const resolved = resolveMovieSource(movie);
|
||||||
if (!resolved?.src) {
|
if (!resolved?.src) {
|
||||||
setSourceLabel('—');
|
setSourceLabel('—');
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const COMPLETION_MODAL_KEY = "completion_modal_content";
|
|||||||
const WINNERS_BY_YEAR_KEY = "winners_by_year";
|
const WINNERS_BY_YEAR_KEY = "winners_by_year";
|
||||||
const OSCAR_DATES_BY_YEAR_KEY = "oscar_date_by_year";
|
const OSCAR_DATES_BY_YEAR_KEY = "oscar_date_by_year";
|
||||||
const APP_VERSION_KEY = "app_version_control";
|
const APP_VERSION_KEY = "app_version_control";
|
||||||
|
const PLAYER_ADMIN_STATUS_UI_KEY = "player_admin_status_ui";
|
||||||
|
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
|
|
||||||
@@ -23,6 +24,12 @@ const DEFAULT_COMPLETION_MODAL = Object.freeze({
|
|||||||
bodyHtml: "",
|
bodyHtml: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const DEFAULT_PLAYER_ADMIN_STATUS_UI = Object.freeze({
|
||||||
|
// Controls the admin-only debug/status block in player.html
|
||||||
|
showSource: true,
|
||||||
|
showProgress: true,
|
||||||
|
});
|
||||||
|
|
||||||
function parseOscarYear(raw) {
|
function parseOscarYear(raw) {
|
||||||
if (raw === undefined || raw === null || raw === "") return null;
|
if (raw === undefined || raw === null || raw === "") return null;
|
||||||
const n = Number(raw);
|
const n = Number(raw);
|
||||||
@@ -146,6 +153,13 @@ function normalizeCompletionModal(rawValue) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePlayerAdminStatusUi(rawValue) {
|
||||||
|
const v = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue) ? rawValue : {};
|
||||||
|
const showSource = v.showSource === undefined ? DEFAULT_PLAYER_ADMIN_STATUS_UI.showSource : !!v.showSource;
|
||||||
|
const showProgress = v.showProgress === undefined ? DEFAULT_PLAYER_ADMIN_STATUS_UI.showProgress : !!v.showProgress;
|
||||||
|
return { showSource, showProgress };
|
||||||
|
}
|
||||||
|
|
||||||
function createId() {
|
function createId() {
|
||||||
try {
|
try {
|
||||||
// Node 16+ / 18+: randomUUID
|
// Node 16+ / 18+: randomUUID
|
||||||
@@ -431,6 +445,45 @@ router.get("/app-version", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Public: get player admin status UI flags (source/progress visibility)
|
||||||
|
router.get("/player-admin-status-ui", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const existing = await Setting.findOne({ key: PLAYER_ADMIN_STATUS_UI_KEY }).select("value");
|
||||||
|
const normalized = normalizePlayerAdminStatusUi(existing?.value);
|
||||||
|
return res.status(200).json(normalized);
|
||||||
|
} catch (_) {
|
||||||
|
return res.status(200).json(DEFAULT_PLAYER_ADMIN_STATUS_UI);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Admin-only: update player admin status UI flags
|
||||||
|
// Body: { showSource?: boolean, showProgress?: boolean }
|
||||||
|
router.put("/player-admin-status-ui", async (req, res) => {
|
||||||
|
const token = req.headers.authorization?.split(" ")[1];
|
||||||
|
if (!token) return res.status(401).json({ message: "Authentication token is required" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
|
if (!decoded.admin) {
|
||||||
|
return res.status(403).json({ message: "You do not have admin privileges" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = normalizePlayerAdminStatusUi(req.body);
|
||||||
|
const updated = await Setting.findOneAndUpdate(
|
||||||
|
{ key: PLAYER_ADMIN_STATUS_UI_KEY },
|
||||||
|
{ $set: { value: normalized } },
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
).select("value");
|
||||||
|
|
||||||
|
return res.status(200).json(normalizePlayerAdminStatusUi(updated?.value));
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof jwt.JsonWebTokenError) {
|
||||||
|
return res.status(401).json({ message: "Invalid or expired token" });
|
||||||
|
}
|
||||||
|
return res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Admin-only: create a new version entry (optionally activates if none or if activate=true)
|
// Admin-only: create a new version entry (optionally activates if none or if activate=true)
|
||||||
// Body: { version: "1.2.3", message?: "..." }
|
// Body: { version: "1.2.3", message?: "..." }
|
||||||
router.post("/app-version", async (req, res) => {
|
router.post("/app-version", async (req, res) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user