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>
118 lines
5.4 KiB
HTML
118 lines
5.4 KiB
HTML
{% load lucide static %}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>
|
|
{% block title %}RosterChief{% endblock title %}
|
|
</title>
|
|
{% 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);
|
|
})();
|
|
</script>
|
|
<link rel="stylesheet" href="{% static 'css/app.css' %}">
|
|
{% block extra_head %}{% endblock extra_head %}
|
|
</head>
|
|
<body class="min-h-screen bg-base-200">
|
|
<div class="navbar bg-base-100 shadow-sm">
|
|
<div class="flex-1">
|
|
<a class="btn btn-ghost gap-2 text-xl" href="/">
|
|
{% lucide "clipboard-list" size=22 %}
|
|
RosterChief
|
|
</a>
|
|
{% if user.is_authenticated and user.is_staff %}
|
|
<a class="btn btn-ghost btn-sm gap-2" href="{% url 'controlpanel:dashboard' %}">
|
|
{% lucide "sliders-horizontal" size=16 %}
|
|
Control panel
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
<div class="flex-none gap-2">
|
|
{# Both icons are rendered; JS shows the one matching the effective theme. #}
|
|
<button class="btn btn-ghost btn-circle"
|
|
aria-label="Toggle theme"
|
|
data-theme-toggle
|
|
type="button">
|
|
<span data-theme-icon="light">{% lucide "sun" size=20 %}</span>
|
|
<span data-theme-icon="dark" class="hidden">{% lucide "moon" size=20 %}</span>
|
|
</button>
|
|
{% if user.is_authenticated %}
|
|
<div class="dropdown dropdown-end">
|
|
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">{{ user }}</div>
|
|
<ul tabindex="0"
|
|
class="menu dropdown-content z-10 mt-2 w-56 rounded-box bg-base-100 p-2 shadow">
|
|
<li>
|
|
<a href="{% url 'mfa_index' %}">
|
|
{% lucide "shield-check" size=16 %}
|
|
Two-factor authentication
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{% url 'account_change_password' %}">
|
|
{% lucide "key-round" size=16 %}
|
|
Change password
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{% url 'account_logout' %}">
|
|
{% lucide "log-out" size=16 %}
|
|
Sign out
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
{% else %}
|
|
<a class="btn btn-primary btn-sm gap-2" href="{% url 'account_login' %}">
|
|
{% lucide "log-in" size=16 %}
|
|
Sign in
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% if messages %}
|
|
<div class="mx-auto mt-4 w-full max-w-5xl space-y-2 px-4">
|
|
{% for message in messages %}
|
|
<div class="alert {% if message.tags == 'error' %}alert-error{% elif message.tags == 'warning' %}alert-warning{% elif message.tags == 'success' %}alert-success{% else %}alert-info{% endif %}">
|
|
<span>{{ message }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
<main class="mx-auto w-full max-w-5xl p-4">
|
|
{% 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");
|
|
|
|
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();
|
|
});
|
|
});
|
|
|
|
showThemeIcon();
|
|
</script>
|
|
</body>
|
|
</html>
|