diff --git a/public/checklist.html b/public/checklist.html index 3d0a89d..1a93020 100644 --- a/public/checklist.html +++ b/public/checklist.html @@ -112,8 +112,11 @@ diff --git a/routes/settings.js b/routes/settings.js index c7d68b3..6a41d87 100644 --- a/routes/settings.js +++ b/routes/settings.js @@ -7,6 +7,14 @@ const Movie = require("../models/Movie"); const router = express.Router(); const ACTIVE_YEAR_KEY = "active_oscar_year"; +const COMPLETION_MODAL_KEY = "completion_modal_content"; + +const DEFAULT_COMPLETION_MODAL = Object.freeze({ + title: "Félicitations! very nice 🎉🎉🎉", + bodyText: "", + videoSrc: "video/reward.mp4", + bodyHtml: "", +}); function parseOscarYear(raw) { if (raw === undefined || raw === null || raw === "") return null; @@ -16,6 +24,34 @@ function parseOscarYear(raw) { return n; } +function sanitizeHtml(raw) { + // Basic, defensive sanitization (admin-controlled input, rendered for all users). + // We remove script tags and inline event handlers. This is not a complete HTML sanitizer. + const html = String(raw || ""); + const withoutScripts = html.replace(//gi, ""); + const withoutOnAttrs = withoutScripts + .replace(/\son\w+="[^"]*"/gi, "") + .replace(/\son\w+='[^']*'/gi, "") + .replace(/\son\w+=\S+/gi, ""); + return withoutOnAttrs.trim(); +} + +function normalizeCompletionModal(rawValue) { + const value = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue) ? rawValue : {}; + + const title = typeof value.title === "string" ? value.title : DEFAULT_COMPLETION_MODAL.title; + const bodyText = typeof value.bodyText === "string" ? value.bodyText : DEFAULT_COMPLETION_MODAL.bodyText; + const videoSrc = typeof value.videoSrc === "string" ? value.videoSrc : DEFAULT_COMPLETION_MODAL.videoSrc; + const bodyHtml = typeof value.bodyHtml === "string" ? value.bodyHtml : DEFAULT_COMPLETION_MODAL.bodyHtml; + + return { + title: String(title || "").slice(0, 200).trim() || DEFAULT_COMPLETION_MODAL.title, + bodyText: String(bodyText || "").slice(0, 8000), + videoSrc: String(videoSrc || "").slice(0, 2048).trim(), + bodyHtml: sanitizeHtml(String(bodyHtml || "").slice(0, 20000)), + }; +} + async function getOrInitActiveYear() { const existing = await Setting.findOne({ key: ACTIVE_YEAR_KEY }); if (existing && typeof existing.value === "number") return existing.value; @@ -78,5 +114,44 @@ router.put("/year", async (req, res) => { } }); +// Public: get the "100% completion" modal content for checklist. +router.get("/completion-modal", async (req, res) => { + try { + const existing = await Setting.findOne({ key: COMPLETION_MODAL_KEY }).select("value"); + const normalized = normalizeCompletionModal(existing?.value); + return res.status(200).json(normalized); + } catch (err) { + // If DB is down, still return a sane default so the client can render. + return res.status(200).json(DEFAULT_COMPLETION_MODAL); + } +}); + +// Admin-only: set the "100% completion" modal content for checklist. +router.put("/completion-modal", 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 = normalizeCompletionModal(req.body); + const updated = await Setting.findOneAndUpdate( + { key: COMPLETION_MODAL_KEY }, + { $set: { value: normalized } }, + { upsert: true, new: true } + ).select("value"); + + return res.status(200).json(normalizeCompletionModal(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 }); + } +}); + module.exports = router;