Manage platform admins and feature flags from the control panel

Features tab: create/edit flags, flip global switches, and toggle a flag per
club from the club detail page. Where `everyone` is set the per-club toggle is
replaced by a badge, because a toggle there would have no effect and so would
lie about what is on.

Admins tab: grant, promote, demote and revoke platform access. Gated on
is_superuser, not is_staff -- the panel itself is staff-accessible, so letting
staff grant is_superuser would collapse the two levels into one and stop
is_superuser being a boundary we can later hang anything on.

Two guardrails, enforced in the service so they hold regardless of caller:
you cannot strip your own access (you would lose the panel mid-click), and the
last superuser can never be demoted (the platform would be locked out of
itself). Granted users get an unusable password and must enrol 2FA before they
can sign in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 16:42:23 +02:00
parent fed24bfee3
commit 268cbe1e06
14 changed files with 3913 additions and 29 deletions

View File

@@ -7,14 +7,16 @@
<title>
{% block title %}RosterChief{% endblock title %}
</title>
{# Apply the stored theme before first paint, otherwise the page flashes
the wrong colours. With no stored preference we set nothing, so
daisyUI's `dark --prefersdark` follows the OS. #}
{% comment %}
Apply the stored theme before first paint, otherwise the page flashes the wrong
colours. With no stored preference we set nothing, so daisyUI's `dark --prefersdark`
follows the OS.
{% endcomment %}
<script>
(() => {
const stored = localStorage.getItem("theme");
if (stored) document.documentElement.setAttribute("data-theme", stored);
})();
(() => {
const stored = localStorage.getItem("theme");
if (stored) document.documentElement.setAttribute("data-theme", stored);
})();
</script>
<link rel="stylesheet" href="{% static 'css/app.css' %}">
{% block extra_head %}{% endblock extra_head %}
@@ -88,28 +90,28 @@
{% block main %}{% endblock main %}
</main>
<script>
// No stored preference means "follow the OS", so the effective theme has to
// be read from the OS whenever nothing is set.
const effectiveTheme = () =>
document.documentElement.getAttribute("data-theme") ||
(window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
// No stored preference means "follow the OS", so the effective theme has to
// be read from the OS whenever nothing is set.
const effectiveTheme = () =>
document.documentElement.getAttribute("data-theme") ||
(window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
const showThemeIcon = () => {
const dark = effectiveTheme() === "dark";
document.querySelectorAll('[data-theme-icon="dark"]').forEach((i) => i.classList.toggle("hidden", !dark));
document.querySelectorAll('[data-theme-icon="light"]').forEach((i) => i.classList.toggle("hidden", dark));
};
const showThemeIcon = () => {
const dark = effectiveTheme() === "dark";
document.querySelectorAll('[data-theme-icon="dark"]').forEach((i) => i.classList.toggle("hidden", !dark));
document.querySelectorAll('[data-theme-icon="light"]').forEach((i) => i.classList.toggle("hidden", dark));
};
document.querySelectorAll("[data-theme-toggle]").forEach((button) => {
button.addEventListener("click", () => {
const next = effectiveTheme() === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", next);
localStorage.setItem("theme", next);
showThemeIcon();
});
});
document.querySelectorAll("[data-theme-toggle]").forEach((button) => {
button.addEventListener("click", () => {
const next = effectiveTheme() === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", next);
localStorage.setItem("theme", next);
showThemeIcon();
});
});
showThemeIcon();
showThemeIcon();
</script>
</body>
</html>