Files
keywarden/app/templates/base.html
T
2026-04-28 02:08:11 +01:00

566 lines
22 KiB
HTML

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{% block title %}Keywarden{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="{% static 'branding/keywarden-favicon.svg' %}">
<link rel="icon" type="image/png" href="{% static 'ninja/favicon.png' %}">
<meta id="kw-theme-color" name="theme-color" content="#0f172a">
<meta name="csrf-token" content="{{ csrf_token }}">
<meta property="og:title" content="Keywarden">
<meta name="twitter:card" content="summary">
<link rel="stylesheet" href="{% static 'css/fonts-space-grotesk.css' %}">
<link rel="stylesheet" href="{% static 'css/dist/styles.css' %}">
<style>
:root {
color-scheme: light;
--kw-chart-card-border: #d6e0ee;
--kw-chart-card-bg: radial-gradient(120% 140% at 0% 0%, #f8fbff 0%, #eef4ff 50%, #e6eefb 100%);
--kw-chart-text: #0f172a;
--kw-chart-subtext: #475569;
--kw-chart-badge-border: rgba(148, 163, 184, 0.45);
--kw-chart-badge-bg: rgba(255, 255, 255, 0.85);
--kw-chart-badge-text: #1e293b;
--kw-chart-canvas-border: rgba(148, 163, 184, 0.35);
--kw-chart-canvas-bg: rgba(248, 250, 252, 0.75);
--kw-chart-empty-text: #64748b;
}
html[data-theme="dark"] {
color-scheme: dark;
--kw-chart-card-border: #1f2937;
--kw-chart-card-bg: radial-gradient(120% 140% at 0% 0%, #1e293b 0%, #111827 50%, #0b1220 100%);
--kw-chart-text: #f3f4f6;
--kw-chart-subtext: #94a3b8;
--kw-chart-badge-border: rgba(100, 116, 139, 0.45);
--kw-chart-badge-bg: rgba(15, 23, 42, 0.65);
--kw-chart-badge-text: #e2e8f0;
--kw-chart-canvas-border: rgba(71, 85, 105, 0.35);
--kw-chart-canvas-bg: rgba(2, 6, 23, 0.32);
--kw-chart-empty-text: #94a3b8;
}
.kw-shell-width {
max-width: 80rem;
}
.kw-topnav-link {
transition: color 150ms ease, background-color 150ms ease;
}
html[data-display-mode="full"] .kw-shell-width {
max-width: min(1800px, calc(100vw - 1.5rem));
}
@media (min-width: 640px) {
html[data-display-mode="full"] .kw-shell-width {
max-width: min(1880px, calc(100vw - 3rem));
}
}
html[data-theme="dark"] body {
background: linear-gradient(135deg, #020617, #0f172a 45%, #111827);
color: #e2e8f0;
}
html[data-theme="dark"] header,
html[data-theme="dark"] footer {
border-color: #334155 !important;
background-color: rgba(15, 23, 42, 0.92) !important;
}
html[data-theme="dark"] .bg-white {
background-color: #0f172a !important;
}
html[data-theme="dark"] .bg-gray-50 {
background-color: #111827 !important;
}
html[data-theme="dark"] .bg-gray-100 {
background-color: #1f2937 !important;
}
html[data-theme="dark"] .border-gray-100 {
border-color: #1e293b !important;
}
html[data-theme="dark"] .border-gray-200 {
border-color: #334155 !important;
}
html[data-theme="dark"] .border-gray-300 {
border-color: #475569 !important;
}
html[data-theme="dark"] .divide-gray-100 > :not([hidden]) ~ :not([hidden]) {
border-color: #1e293b !important;
}
html[data-theme="dark"] .divide-gray-200 > :not([hidden]) ~ :not([hidden]) {
border-color: #334155 !important;
}
html[data-theme="dark"] .text-gray-900 {
color: #f1f5f9 !important;
}
html[data-theme="dark"] .text-gray-800 {
color: #e2e8f0 !important;
}
html[data-theme="dark"] .text-gray-700 {
color: #e2e8f0 !important;
}
html[data-theme="dark"] .text-gray-600 {
color: #cbd5e1 !important;
}
html[data-theme="dark"] .text-gray-500 {
color: #94a3b8 !important;
}
html[data-theme="dark"] .text-gray-400 {
color: #64748b !important;
}
html[data-theme="dark"] .text-blue-700 {
color: #60a5fa !important;
}
html[data-theme="dark"] .text-emerald-700 {
color: #34d399 !important;
}
html[data-theme="dark"] .text-emerald-800 {
color: #6ee7b7 !important;
}
html[data-theme="dark"] .text-indigo-700 {
color: #818cf8 !important;
}
html[data-theme="dark"] .text-rose-700 {
color: #fb7185 !important;
}
html[data-theme="dark"] .text-red-600 {
color: #f87171 !important;
}
html[data-theme="dark"] .text-red-800 {
color: #fca5a5 !important;
}
html[data-theme="dark"] .text-amber-700 {
color: #fbbf24 !important;
}
html[data-theme="dark"] .text-amber-800 {
color: #fcd34d !important;
}
html[data-theme="dark"] .ring-blue-100 {
--tw-ring-color: rgba(59, 130, 246, 0.35) !important;
}
html[data-theme="dark"] .hover\:bg-gray-50:hover {
background-color: #1f2937 !important;
}
html[data-theme="dark"] .hover\:bg-gray-100:hover {
background-color: #334155 !important;
}
html[data-theme="dark"] .hover\:bg-white:hover {
background-color: #1e293b !important;
}
html[data-theme="dark"] .hover\:text-gray-900:hover {
color: #f1f5f9 !important;
}
html[data-theme="dark"] .bg-blue-50 {
background-color: rgba(37, 99, 235, 0.2) !important;
}
html[data-theme="dark"] .bg-blue-100 {
background-color: rgba(37, 99, 235, 0.3) !important;
}
html[data-theme="dark"] .bg-emerald-50 {
background-color: rgba(5, 150, 105, 0.2) !important;
}
html[data-theme="dark"] .bg-emerald-100 {
background-color: rgba(5, 150, 105, 0.28) !important;
}
html[data-theme="dark"] .bg-indigo-50 {
background-color: rgba(79, 70, 229, 0.22) !important;
}
html[data-theme="dark"] .bg-rose-50 {
background-color: rgba(225, 29, 72, 0.2) !important;
}
html[data-theme="dark"] .bg-rose-100 {
background-color: rgba(225, 29, 72, 0.28) !important;
}
html[data-theme="dark"] .bg-amber-100 {
background-color: rgba(180, 83, 9, 0.35) !important;
}
html[data-theme="dark"] .bg-amber-50 {
background-color: rgba(180, 83, 9, 0.22) !important;
}
html[data-theme="dark"] .bg-red-50 {
background-color: rgba(127, 29, 29, 0.32) !important;
}
html[data-theme="dark"] .bg-gray-200 {
background-color: #334155 !important;
}
html[data-theme="dark"] .hover\:bg-blue-50:hover {
background-color: rgba(37, 99, 235, 0.28) !important;
}
html[data-theme="dark"] .hover\:bg-blue-100:hover {
background-color: rgba(37, 99, 235, 0.35) !important;
}
html[data-theme="dark"] tr.border-t {
border-color: #334155 !important;
}
html[data-theme="dark"] hr {
border-color: #334155 !important;
}
html[data-theme="dark"] input[type="text"],
html[data-theme="dark"] input[type="password"],
html[data-theme="dark"] input[type="email"],
html[data-theme="dark"] input[type="search"],
html[data-theme="dark"] input[type="datetime-local"],
html[data-theme="dark"] input[type="number"],
html[data-theme="dark"] input[type="url"],
html[data-theme="dark"] input[type="tel"],
html[data-theme="dark"] textarea,
html[data-theme="dark"] select {
background-color: #0b1220 !important;
border-color: #475569 !important;
color: #e2e8f0 !important;
}
html[data-theme="dark"] input::placeholder,
html[data-theme="dark"] textarea::placeholder {
color: #94a3b8 !important;
}
html[data-theme="dark"] .kw-topnav-link {
color: #cbd5e1 !important;
}
html[data-theme="dark"] .kw-topnav-link:hover {
background-color: #1f2937 !important;
color: #bfdbfe !important;
}
@media (min-width: 768px) {
html[data-theme="dark"] .kw-topnav-link:hover {
background-color: transparent !important;
color: #93c5fd !important;
}
}
</style>
<script>
(function () {
var key = "keywardenThemeMode";
function normalize(mode) {
return mode === "light" || mode === "dark" || mode === "system" ? mode : "system";
}
function resolve(mode) {
if (mode === "system") {
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
return mode;
}
var mode = "system";
try {
mode = normalize(window.localStorage.getItem(key));
} catch (err) {
mode = "system";
}
var resolved = resolve(mode);
document.documentElement.setAttribute("data-theme-mode", mode);
document.documentElement.setAttribute("data-theme", resolved);
})();
</script>
<script>
(function () {
var key = "keywardenDisplayMode";
function normalize(mode) {
return mode === "full" ? "full" : "compact";
}
var mode = "compact";
try {
mode = normalize(window.localStorage.getItem(key));
} catch (err) {
mode = "compact";
}
document.documentElement.setAttribute("data-display-mode", mode);
})();
</script>
{% block extra_head %}{% endblock %}
</head>
<body class="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 text-slate-900 antialiased font-['Space_Grotesk']">
<div class="min-h-screen flex flex-col">
{% if not is_popout %}
<header class="border-b border-gray-200 bg-white/90 backdrop-blur supports-[backdrop-filter]:bg-white/75">
<nav class="kw-shell-width mx-auto w-full px-4 py-3 lg:px-6">
<div class="flex flex-wrap items-center justify-between">
<a href="/" class="flex items-center gap-3">
<span class="flex h-11 w-11 items-center justify-center rounded-2xl bg-white p-1 shadow-sm ring-1 ring-blue-100">
<img src="{% static 'branding/keywarden-favicon.svg' %}" alt="Keywarden logo" class="h-8 w-8">
</span>
<span class="flex flex-col">
<span class="text-xl font-semibold leading-tight tracking-tight">Keywarden</span>
<span class="text-xs text-gray-500">Access control vault</span>
</span>
</a>
<div class="flex items-center gap-3 md:order-2">
<button
type="button"
data-theme-toggle
class="inline-flex items-center gap-1 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-300"
aria-label="Toggle theme"
>
<span data-theme-icon aria-hidden="true">◐</span>
<span data-theme-label>Theme</span>
</button>
{% if request.user.is_authenticated %}
<a
href="{% url 'accounts:logout' %}"
class="inline-flex items-center rounded-lg bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300"
>
Logout
</a>
{% else %}
<a
href="{% url 'accounts:login' %}"
class="inline-flex items-center rounded-lg bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300"
>
Login
</a>
{% endif %}
<button
data-collapse-toggle="navbar-default"
type="button"
class="inline-flex items-center rounded-lg p-2 text-sm text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 md:hidden"
aria-controls="navbar-default"
aria-expanded="false"
>
<span class="sr-only">Open main menu</span>
<svg class="h-6 w-6" aria-hidden="true" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
<div class="hidden w-full md:order-1 md:block md:w-auto" id="navbar-default">
<ul class="mt-4 flex flex-col rounded-lg border border-gray-100 bg-gray-50 p-4 text-sm font-medium text-gray-700 md:mt-0 md:flex-row md:space-x-6 md:border-0 md:bg-transparent md:p-0">
{% if request.user.is_authenticated %}
<li>
<a
href="{% url 'servers:dashboard' %}"
class="kw-topnav-link block rounded px-3 py-2 hover:bg-gray-100 md:px-0 md:py-0 md:hover:bg-transparent md:hover:text-blue-700"
>
Servers
</a>
</li>
<li>
<a
href="{% url 'accounts:profile' %}"
class="kw-topnav-link block rounded px-3 py-2 hover:bg-gray-100 md:px-0 md:py-0 md:hover:bg-transparent md:hover:text-blue-700"
>
Profile
</a>
</li>
{% if request.user.is_superuser or request.user.is_staff or perms.servers.change_server or perms.access.change_accessrequest %}
<li>
<a
href="{% url 'servers:admin_dashboard' %}"
class="kw-topnav-link block rounded px-3 py-2 hover:bg-gray-100 md:px-0 md:py-0 md:hover:bg-transparent md:hover:text-blue-700"
>
Admin
</a>
</li>
{% endif %}
{% endif %}
</ul>
</div>
</div>
</nav>
</header>
{% endif %}
<main class="kw-shell-width mx-auto w-full flex-1 px-4 py-8 sm:px-6 lg:px-8">
{% block content %}{% endblock %}
</main>
{% if not is_popout %}
<footer class="border-t border-gray-200 bg-white">
<div class="kw-shell-width mx-auto flex w-full flex-col gap-2 px-4 py-6 text-sm text-gray-500 sm:flex-row sm:items-center sm:justify-between sm:px-6 lg:px-8">
<div class="flex items-center gap-2">
<span class="font-medium text-gray-700">Keywarden</span>
<span class="text-gray-300">•</span>
<span>Secure access manager</span>
</div>
<div class="flex items-center gap-3">
<a class="font-medium text-blue-700 hover:underline" href="https://git.ntbx.io/boris/keywarden">Repository</a>
<span class="text-gray-300">•</span>
<a class="font-medium text-blue-700 hover:underline" href="https://ntbx.io">George Wilkinson</a>
</div>
</div>
</footer>
{% endif %}
</div>
<script>
(function () {
window.keywardenGetCsrfToken = function () {
var meta = document.querySelector('meta[name="csrf-token"]');
if (meta && meta.content && meta.content !== "NOTPROVIDED") {
return meta.content;
}
var input = document.querySelector('input[name="csrfmiddlewaretoken"]');
if (input && input.value) {
return input.value;
}
return "";
};
})();
</script>
<script>
(function () {
var key = "keywardenThemeMode";
var root = document.documentElement;
var toggles = document.querySelectorAll("[data-theme-toggle]");
var labels = document.querySelectorAll("[data-theme-label]");
var icons = document.querySelectorAll("[data-theme-icon]");
var themeMeta = document.getElementById("kw-theme-color");
function normalize(mode) {
return mode === "light" || mode === "dark" || mode === "system" ? mode : "system";
}
function resolve(mode) {
if (mode === "system") {
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
return mode;
}
function readMode() {
try {
return normalize(window.localStorage.getItem(key));
} catch (err) {
return "system";
}
}
function writeMode(mode) {
try {
window.localStorage.setItem(key, mode);
} catch (err) {
// Ignore storage failures.
}
}
function apply(mode) {
var resolved = resolve(mode);
root.setAttribute("data-theme-mode", mode);
root.setAttribute("data-theme", resolved);
var labelText = mode === "system" ? "Theme: System" : (mode === "dark" ? "Theme: Dark" : "Theme: Light");
var iconText = resolved === "dark" ? "☾" : "☀";
if (themeMeta) {
themeMeta.setAttribute("content", resolved === "dark" ? "#020617" : "#f8fafc");
}
for (var i = 0; i < labels.length; i += 1) {
labels[i].textContent = labelText;
}
for (var j = 0; j < icons.length; j += 1) {
icons[j].textContent = iconText;
}
if (typeof window.CustomEvent === "function") {
window.dispatchEvent(new CustomEvent("keywarden:theme-mode", { detail: { mode: mode, resolved: resolved } }));
}
return mode;
}
function cycle(mode) {
if (mode === "light") {
return "dark";
}
if (mode === "dark") {
return "system";
}
return "light";
}
var currentMode = apply(readMode());
for (var k = 0; k < toggles.length; k += 1) {
toggles[k].addEventListener("click", function () {
currentMode = apply(cycle(currentMode));
writeMode(currentMode);
});
}
var media = window.matchMedia("(prefers-color-scheme: dark)");
var mediaListener = function () {
if (currentMode === "system") {
apply(currentMode);
}
};
if (typeof media.addEventListener === "function") {
media.addEventListener("change", mediaListener);
} else if (typeof media.addListener === "function") {
media.addListener(mediaListener);
}
window.keywardenGetThemeMode = function () {
return currentMode;
};
window.keywardenSetThemeMode = function (mode) {
currentMode = apply(normalize(mode));
writeMode(currentMode);
return currentMode;
};
})();
</script>
<script>
(function () {
var key = "keywardenDisplayMode";
function normalize(mode) {
return mode === "full" ? "full" : "compact";
}
function getMode() {
try {
return normalize(window.localStorage.getItem(key));
} catch (err) {
return "compact";
}
}
function setMode(mode) {
var normalized = normalize(mode);
document.documentElement.setAttribute("data-display-mode", normalized);
try {
window.localStorage.setItem(key, normalized);
} catch (err) {
// Ignore storage quota or privacy-mode failures.
}
if (typeof window.CustomEvent === "function") {
window.dispatchEvent(new CustomEvent("keywarden:display-mode", { detail: { mode: normalized } }));
}
return normalized;
}
window.keywardenGetDisplayMode = getMode;
window.keywardenSetDisplayMode = setMode;
})();
</script>
<script>
(function () {
var collapseToggles = document.querySelectorAll("[data-collapse-toggle]");
for (var i = 0; i < collapseToggles.length; i += 1) {
collapseToggles[i].addEventListener("click", function (event) {
var button = event.currentTarget;
var targetId = button.getAttribute("data-collapse-toggle");
if (!targetId) {
return;
}
var target = document.getElementById(targetId);
if (!target) {
return;
}
var isHidden = target.classList.contains("hidden");
target.classList.toggle("hidden", !isHidden);
button.setAttribute("aria-expanded", isHidden ? "true" : "false");
});
}
function setTooltipState(tooltip, show) {
if (!tooltip) {
return;
}
tooltip.classList.toggle("invisible", !show);
tooltip.classList.toggle("opacity-0", !show);
tooltip.classList.toggle("opacity-100", show);
}
var tooltipTriggers = document.querySelectorAll("[data-tooltip-target]");
for (var j = 0; j < tooltipTriggers.length; j += 1) {
(function (trigger) {
var tooltipId = trigger.getAttribute("data-tooltip-target");
if (!tooltipId) {
return;
}
var tooltip = document.getElementById(tooltipId);
if (!tooltip) {
return;
}
trigger.addEventListener("mouseenter", function () { setTooltipState(tooltip, true); });
trigger.addEventListener("mouseleave", function () { setTooltipState(tooltip, false); });
trigger.addEventListener("focus", function () { setTooltipState(tooltip, true); });
trigger.addEventListener("blur", function () { setTooltipState(tooltip, false); });
})(tooltipTriggers[j]);
}
})();
</script>
</body>
</html>