Release: v0.1.0-alpha
Release Docker Image / Build & Push Docker Image (release) Failing after 1m30s
Release Docker Image / Build & Push Docker Image (release) Failing after 1m30s
This commit is contained in:
@@ -0,0 +1,669 @@
|
||||
{{define "base"}}
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>{{.Title}} - {{appName}}</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<!-- Preload icon font to prevent re-decode lag on tab restore -->
|
||||
<link rel="preload" href="/static/css/fonts/tabler-icons.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<!-- Resolve theme BEFORE loading CSS to prevent FOUC (flash of unstyled content) -->
|
||||
<script>
|
||||
(function() {
|
||||
var theme = '{{with .User}}{{.Theme}}{{end}}' || 'auto';
|
||||
var resolved = theme;
|
||||
if (theme === 'auto') {
|
||||
resolved = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
||||
}
|
||||
document.documentElement.setAttribute('data-bs-theme', resolved);
|
||||
document.documentElement.style.colorScheme = resolved;
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
/* Critical inline styles: prevent white flash between page navigations */
|
||||
html[data-bs-theme="dark"],
|
||||
html[data-bs-theme="dark"] body { background-color: #0F1829; color-scheme: dark; }
|
||||
html[data-bs-theme="light"],
|
||||
html[data-bs-theme="light"] body { background-color: #f1f5f9; color-scheme: light; }
|
||||
.navbar-brand-image { height: 2rem; }
|
||||
.keywarden-brand { font-weight: 700; font-size: 1.25rem; color: #206bc4; }
|
||||
[data-bs-theme="dark"] .keywarden-brand { color: #4da3ff; }
|
||||
/* Text selection colors */
|
||||
[data-bs-theme="dark"] ::selection { background: #3d6098; color: #f0f4f8; }
|
||||
[data-bs-theme="dark"] ::-moz-selection { background: #3d6098; color: #f0f4f8; }
|
||||
[data-bs-theme="light"] ::selection { background: #b3d4fc; color: #1a1a1a; }
|
||||
[data-bs-theme="light"] ::-moz-selection { background: #b3d4fc; color: #1a1a1a; }
|
||||
|
||||
/* Consistent spacing between Tabler icons and adjacent text */
|
||||
i.ti { margin-right: 0.25em; }
|
||||
.btn-icon > i.ti, .input-icon-addon > i.ti, .nav-link-icon > i.ti { margin-right: 0; }
|
||||
|
||||
/* ══ PAGE LAYOUT: full-width header on top, sidebar + content below ══ */
|
||||
.page {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.page-content-row {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Full-width top header ── */
|
||||
header.navbar.keywarden-top-header {
|
||||
background: #1D2B38 !important;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||
flex-shrink: 0;
|
||||
z-index: 1030;
|
||||
}
|
||||
[data-bs-theme="light"] header.navbar.keywarden-top-header {
|
||||
background: #1D2B38 !important;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.08);
|
||||
}
|
||||
header.navbar.keywarden-top-header .nav-link { color: #c8d6e5 !important; }
|
||||
header.navbar.keywarden-top-header .nav-link .text-secondary { color: #8fa8c8 !important; }
|
||||
header.navbar.keywarden-top-header .fw-bold { color: #e8eef5; }
|
||||
|
||||
/* ── Header brand area (left side, aligned with sidebar) ── */
|
||||
.keywarden-header-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.keywarden-header-brand .keywarden-brand { color: #c8d6e5; font-size: 1.15rem; }
|
||||
.keywarden-header-brand .keywarden-brand:hover { color: #fff; text-decoration: none; }
|
||||
.keywarden-header-brand .keywarden-brand i.ti { color: #4da3ff; font-size: 1.3rem; }
|
||||
|
||||
/* ── Angular header buttons ── */
|
||||
header.navbar.keywarden-top-header .btn-header-modern {
|
||||
color: #c8d6e5;
|
||||
background: rgba(255,255,255,0.06);
|
||||
border: 2px solid rgba(255,255,255,0.18);
|
||||
border-radius: 4px;
|
||||
padding: 0.35rem 1rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
header.navbar.keywarden-top-header .btn-header-modern:hover {
|
||||
color: #fff;
|
||||
background: rgba(255,255,255,0.14);
|
||||
border-color: rgba(255,255,255,0.32);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
header.navbar.keywarden-top-header .btn-header-modern.btn-icon {
|
||||
width: 2.16875rem;
|
||||
height: 2.16875rem;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ── Sidebar (vertical, below header) ── */
|
||||
.navbar-vertical {
|
||||
background: #1D2B38 !important;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.navbar-vertical > .container-fluid {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.navbar-vertical .navbar-collapse {
|
||||
flex: 1;
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.navbar-vertical .navbar-nav {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
[data-bs-theme="light"] .navbar-vertical { background: #1D2B38 !important; }
|
||||
|
||||
/* ── Page content area ── */
|
||||
.page-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
[data-bs-theme="dark"] .page-wrapper { background: #0F1829; }
|
||||
[data-bs-theme="dark"] .page-body { background: #0F1829; }
|
||||
html[data-bs-theme="dark"],
|
||||
html[data-bs-theme="dark"] body { background-color: #0F1829 !important; }
|
||||
.page-body { content-visibility: auto; contain-intrinsic-size: auto 500px; }
|
||||
|
||||
/* ── Narrower dashboard stat cards ── */
|
||||
.stat-card-narrow { max-width: 220px; }
|
||||
|
||||
/* ── Desktop: sidebar always visible ── */
|
||||
@media (min-width: 992px) {
|
||||
.navbar-vertical {
|
||||
width: 256px !important;
|
||||
min-width: 256px !important;
|
||||
position: relative !important;
|
||||
top: auto !important;
|
||||
bottom: auto !important;
|
||||
left: auto !important;
|
||||
}
|
||||
.navbar-vertical .container-fluid {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
.navbar-vertical .nav-link-title {
|
||||
opacity: 1;
|
||||
white-space: nowrap;
|
||||
pointer-events: auto;
|
||||
width: auto;
|
||||
}
|
||||
.navbar-vertical .nav-link {
|
||||
justify-content: start;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
.navbar-vertical .nav-link-icon {
|
||||
margin-right: 0.5rem;
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
.navbar-vertical .nav-category {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
padding: 0.6rem 0.75rem 0.25rem;
|
||||
}
|
||||
.navbar-vertical .nav-item + .nav-category,
|
||||
.navbar-vertical .nav-category + .nav-category {
|
||||
margin-top: 0.4rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding-top: 0.65rem;
|
||||
}
|
||||
.navbar-vertical .nav-category:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
/* Hide mobile burger button on desktop */
|
||||
.mobile-menu-toggle {
|
||||
display: none !important;
|
||||
}
|
||||
.page-wrapper {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Mobile: off-canvas sidebar ── */
|
||||
@media (max-width: 991.98px) {
|
||||
aside.navbar-vertical {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 280px !important;
|
||||
max-width: 85vw;
|
||||
z-index: 1050;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
overflow-y: auto;
|
||||
padding-top: 3.5rem;
|
||||
}
|
||||
aside.navbar-vertical.mobile-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
aside.navbar-vertical .navbar-collapse {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
aside.navbar-vertical .container-fluid {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
aside.navbar-vertical .nav-link-title {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
width: auto;
|
||||
}
|
||||
aside.navbar-vertical .nav-link {
|
||||
justify-content: start;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
aside.navbar-vertical .nav-link-icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
aside.navbar-vertical .nav-category {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
padding: 0.6rem 0.75rem 0.25rem;
|
||||
}
|
||||
/* Backdrop overlay */
|
||||
.mobile-sidebar-backdrop {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 1040;
|
||||
}
|
||||
.mobile-sidebar-backdrop.show {
|
||||
display: block;
|
||||
}
|
||||
/* Mobile burger button */
|
||||
.mobile-menu-toggle {
|
||||
background: rgba(255,255,255,0.06);
|
||||
border: 1px solid rgba(255,255,255,0.10);
|
||||
color: #8fa8c8;
|
||||
border-radius: 6px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
padding: 0;
|
||||
}
|
||||
.mobile-menu-toggle:hover {
|
||||
background: rgba(255,255,255,0.16);
|
||||
color: #fff;
|
||||
}
|
||||
.mobile-menu-toggle .ti {
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Sidebar category headers ── */
|
||||
.nav-category {
|
||||
display: block;
|
||||
padding: 0.6rem 0.75rem 0.25rem;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: rgba(138, 166, 200, 0.55);
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
.nav-category:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
/* Show a subtle divider line above categories (except the first) */
|
||||
.nav-category + .nav-category,
|
||||
.nav-item + .nav-category {
|
||||
margin-top: 0.4rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding-top: 0.65rem;
|
||||
}
|
||||
|
||||
/* ── Avatar in dropdown ── */
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
<!-- Tabler CSS (self-hosted to prevent FOUC) -->
|
||||
<link rel="stylesheet" href="/static/css/tabler.min.css">
|
||||
<link rel="stylesheet" href="/static/css/tabler-icons.min.css">
|
||||
</head>
|
||||
<body class="layout-fluid">
|
||||
<div class="page">
|
||||
<!-- ═══ FULL-WIDTH TOP HEADER ═══ -->
|
||||
<header class="navbar d-print-none keywarden-top-header" data-bs-theme="dark">
|
||||
<div class="container-fluid px-3">
|
||||
<div class="d-flex align-items-center w-100">
|
||||
<!-- Logo + Brand (left side, aligned with sidebar) -->
|
||||
<div class="keywarden-header-brand">
|
||||
<button class="mobile-menu-toggle d-lg-none" onclick="toggleMobileMenu()" title="Menü" id="mobile-menu-btn">
|
||||
<i class="ti ti-menu-2" id="mobile-menu-icon"></i>
|
||||
</button>
|
||||
<a href="/dashboard" class="keywarden-brand text-decoration-none d-flex align-items-center">
|
||||
<i class="ti ti-key"></i> {{appName}}
|
||||
</a>
|
||||
</div>
|
||||
<!-- Spacer -->
|
||||
<div class="flex-grow-1"></div>
|
||||
<!-- Repository Link -->
|
||||
<div class="nav-item d-none d-md-flex me-2">
|
||||
<a href="https://git.techniverse.net/scriptos/keywarden" class="btn btn-header-modern btn-sm" target="_blank" rel="noopener noreferrer" title="Source code on Gitea">
|
||||
<i class="ti ti-brand-git"></i> Repository
|
||||
</a>
|
||||
</div>
|
||||
<!-- Documentation -->
|
||||
<div class="nav-item d-none d-md-flex me-2">
|
||||
<a href="https://git.techniverse.net/scriptos/keywarden/src/branch/master/docs" class="btn btn-header-modern btn-sm" target="_blank" rel="noopener noreferrer" title="Documentation">
|
||||
<i class="ti ti-book"></i> Docs
|
||||
</a>
|
||||
</div>
|
||||
<!-- Theme Toggle -->
|
||||
<div class="nav-item d-flex me-2">
|
||||
<button id="theme-toggle" class="btn btn-header-modern btn-sm btn-icon" title="Toggle theme" onclick="toggleTheme()">
|
||||
<i class="ti ti-sun" id="theme-icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- User Badge -->
|
||||
{{with .User}}
|
||||
<div class="nav-item dropdown">
|
||||
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
|
||||
<span class="avatar avatar-sm rounded-circle bg-primary-lt">
|
||||
{{if .AvatarBase64}}
|
||||
<img src="/avatar/{{.ID}}" class="avatar-img" alt="Avatar">
|
||||
{{else}}
|
||||
<i class="ti ti-user" style="font-size: 1.1rem;"></i>
|
||||
{{end}}
|
||||
</span>
|
||||
<div class="d-none d-xl-block ps-2">
|
||||
<div class="fw-bold">{{.Username}}</div>
|
||||
<div class="mt-1 small text-secondary">{{if eq .Role "owner"}}Owner{{else if eq .Role "admin"}}Administrator{{else}}User{{end}}</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
||||
<a class="dropdown-item text-danger" href="/logout">
|
||||
<i class="ti ti-logout"></i> Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- ═══ CONTENT ROW: sidebar + main ═══ -->
|
||||
<div class="page-content-row">
|
||||
<!-- Mobile sidebar backdrop -->
|
||||
<div class="mobile-sidebar-backdrop" id="mobile-sidebar-backdrop" onclick="closeMobileMenu()"></div>
|
||||
<!-- Sidebar -->
|
||||
<aside class="navbar navbar-vertical navbar-expand-lg" data-bs-theme="dark" id="keywarden-sidebar">
|
||||
<div class="container-fluid">
|
||||
<div class="collapse navbar-collapse" id="sidebar-menu">
|
||||
<ul class="navbar-nav pt-lg-3">
|
||||
<!-- ── Overview ── -->
|
||||
<li class="nav-category">Overview</li>
|
||||
<li class="nav-item{{if eq .Active "dashboard"}} active{{end}}">
|
||||
<a class="nav-link" href="/dashboard">
|
||||
<span class="nav-link-icon"><i class="ti ti-dashboard"></i></span>
|
||||
<span class="nav-link-title">Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- ── Infrastructure (Admin/Owner only) ── -->
|
||||
{{with .User}}
|
||||
{{if or (eq .Role "admin") (eq .Role "owner")}}
|
||||
<li class="nav-category">Infrastructure</li>
|
||||
<li class="nav-item{{if eq $.Active "servers"}} active{{end}}">
|
||||
<a class="nav-link" href="/servers">
|
||||
<span class="nav-link-icon"><i class="ti ti-server"></i></span>
|
||||
<span class="nav-link-title">Hosts</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item{{if eq $.Active "groups"}} active{{end}}">
|
||||
<a class="nav-link" href="/groups">
|
||||
<span class="nav-link-icon"><i class="ti ti-folders"></i></span>
|
||||
<span class="nav-link-title">Groups</span>
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<!-- ── Key Management ── -->
|
||||
<li class="nav-category">Key Management</li>
|
||||
<li class="nav-item{{if eq .Active "keys"}} active{{end}}">
|
||||
<a class="nav-link" href="/keys">
|
||||
<span class="nav-link-icon"><i class="ti ti-key"></i></span>
|
||||
<span class="nav-link-title">SSH Keys</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- My Access (visible to all users) -->
|
||||
<li class="nav-item{{if eq .Active "my_access"}} active{{end}}">
|
||||
<a class="nav-link" href="/my/access">
|
||||
<span class="nav-link-icon"><i class="ti ti-shield-check"></i></span>
|
||||
<span class="nav-link-title">My Access</span>
|
||||
</a>
|
||||
</li>
|
||||
{{with .User}}
|
||||
{{if or (eq .Role "admin") (eq .Role "owner")}}
|
||||
<li class="nav-item{{if eq $.Active "deploy"}} active{{end}}">
|
||||
<a class="nav-link" href="/deploy">
|
||||
<span class="nav-link-icon"><i class="ti ti-send"></i></span>
|
||||
<span class="nav-link-title">Deploy Keys</span>
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<!-- ── Operations (Admin/Owner only) ── -->
|
||||
{{with .User}}
|
||||
{{if or (eq .Role "admin") (eq .Role "owner")}}
|
||||
<li class="nav-category">Operations</li>
|
||||
<li class="nav-item{{if eq $.Active "cron"}} active{{end}}">
|
||||
<a class="nav-link" href="/cron">
|
||||
<span class="nav-link-icon"><i class="ti ti-clock"></i></span>
|
||||
<span class="nav-link-title">Temporary Access</span>
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<li class="nav-item{{if eq .Active "audit"}} active{{end}}">
|
||||
<a class="nav-link" href="/audit">
|
||||
<span class="nav-link-icon"><i class="ti ti-list-details"></i></span>
|
||||
<span class="nav-link-title">Audit Log</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- ── Administration ── -->
|
||||
<li class="nav-category">Administration</li>
|
||||
{{with .User}}
|
||||
{{if or (eq .Role "admin") (eq .Role "owner")}}
|
||||
<li class="nav-item{{if eq $.Active "users"}} active{{end}}">
|
||||
<a class="nav-link" href="/users">
|
||||
<span class="nav-link-icon"><i class="ti ti-users"></i></span>
|
||||
<span class="nav-link-title">Users</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item{{if eq $.Active "assignments"}} active{{end}}">
|
||||
<a class="nav-link" href="/assignments">
|
||||
<span class="nav-link-icon"><i class="ti ti-shield-lock"></i></span>
|
||||
<span class="nav-link-title">Access Assignments</span>
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<li class="nav-item{{if eq .Active "settings"}} active{{end}}">
|
||||
<a class="nav-link" href="/settings">
|
||||
<span class="nav-link-icon"><i class="ti ti-settings"></i></span>
|
||||
<span class="nav-link-title">Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
{{with .User}}
|
||||
{{if eq .Role "owner"}}
|
||||
<li class="nav-item{{if eq $.Active "admin_settings"}} active{{end}}">
|
||||
<a class="nav-link" href="/admin/settings">
|
||||
<span class="nav-link-icon"><i class="ti ti-shield-cog"></i></span>
|
||||
<span class="nav-link-title">Admin Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<!-- ── System (Admin/Owner only) ── -->
|
||||
{{with .User}}
|
||||
{{if or (eq .Role "admin") (eq .Role "owner")}}
|
||||
<li class="nav-category">System</li>
|
||||
<li class="nav-item{{if eq $.Active "system_info"}} active{{end}}">
|
||||
<a class="nav-link" href="/system">
|
||||
<span class="nav-link-icon"><i class="ti ti-info-circle"></i></span>
|
||||
<span class="nav-link-title">System Information</span>
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main content -->
|
||||
<div class="page-wrapper">
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="page-pretitle">Keywarden – Centralized SSH Key Management and Deployment</div>
|
||||
<h2 class="page-title">{{.Title}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
{{if .Flash}}
|
||||
<div class="alert alert-{{.Flash.Type}} alert-dismissible" role="alert">
|
||||
<div class="d-flex">
|
||||
<div>
|
||||
{{if eq .Flash.Type "success"}}<i class="ti ti-check icon alert-icon"></i>{{end}}
|
||||
{{if eq .Flash.Type "danger"}}<i class="ti ti-alert-circle icon alert-icon"></i>{{end}}
|
||||
{{if eq .Flash.Type "warning"}}<i class="ti ti-alert-triangle icon alert-icon"></i>{{end}}
|
||||
</div>
|
||||
<div>{{.Flash.Message}}</div>
|
||||
</div>
|
||||
<a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{template "content" .}}
|
||||
</div>
|
||||
</div>
|
||||
<footer class="footer footer-transparent d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="row text-center align-items-center">
|
||||
<div class="col-12">
|
||||
<span class="text-secondary">© 2026 Keywarden – Centralized SSH Key Management and Deployment | AGPLv3</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div><!-- /page-content-row -->
|
||||
</div><!-- /page -->
|
||||
<!-- Tabler JS (self-hosted) -->
|
||||
<script src="/static/js/tabler.min.js"></script>
|
||||
<script>
|
||||
// --- Theme Toggle ---
|
||||
function getResolvedTheme() {
|
||||
var stored = document.documentElement.getAttribute('data-bs-theme');
|
||||
return stored || 'light';
|
||||
}
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.setAttribute('data-bs-theme', theme);
|
||||
document.documentElement.style.colorScheme = theme;
|
||||
updateThemeIcon(theme);
|
||||
}
|
||||
function updateThemeIcon(theme) {
|
||||
var icon = document.getElementById('theme-icon');
|
||||
if (!icon) return;
|
||||
icon.className = theme === 'dark' ? 'ti ti-moon' : 'ti ti-sun';
|
||||
}
|
||||
function toggleTheme() {
|
||||
var current = getResolvedTheme();
|
||||
var next = current === 'dark' ? 'light' : 'dark';
|
||||
applyTheme(next);
|
||||
// Persist choice via API (fire-and-forget)
|
||||
var csrf = (document.cookie.match(/(?:^|;\s*)_csrf=([^;]*)/) || [])[1] || '';
|
||||
fetch('/settings/theme', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: 'theme=' + encodeURIComponent(next) + '&_csrf=' + encodeURIComponent(csrf)
|
||||
});
|
||||
}
|
||||
// Set initial icon on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateThemeIcon(getResolvedTheme());
|
||||
});
|
||||
|
||||
// --- Mobile Sidebar Toggle ---
|
||||
function toggleMobileMenu() {
|
||||
var sidebar = document.getElementById('keywarden-sidebar');
|
||||
var backdrop = document.getElementById('mobile-sidebar-backdrop');
|
||||
var icon = document.getElementById('mobile-menu-icon');
|
||||
if (!sidebar) return;
|
||||
var isOpen = sidebar.classList.toggle('mobile-open');
|
||||
if (backdrop) backdrop.classList.toggle('show', isOpen);
|
||||
if (icon) icon.className = isOpen ? 'ti ti-x' : 'ti ti-menu-2';
|
||||
}
|
||||
function closeMobileMenu() {
|
||||
var sidebar = document.getElementById('keywarden-sidebar');
|
||||
var backdrop = document.getElementById('mobile-sidebar-backdrop');
|
||||
var icon = document.getElementById('mobile-menu-icon');
|
||||
if (sidebar) sidebar.classList.remove('mobile-open');
|
||||
if (backdrop) backdrop.classList.remove('show');
|
||||
if (icon) icon.className = 'ti ti-menu-2';
|
||||
}
|
||||
|
||||
function copyToClipboard(elementId, btn) {
|
||||
var el = document.getElementById(elementId);
|
||||
var text = el.value || el.textContent;
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
showCopyFeedback(btn);
|
||||
});
|
||||
} else {
|
||||
// Fallback for non-HTTPS: use temporary textarea (password inputs block select/copy)
|
||||
var tmp = document.createElement('textarea');
|
||||
tmp.value = text;
|
||||
tmp.style.position = 'fixed';
|
||||
tmp.style.opacity = '0';
|
||||
document.body.appendChild(tmp);
|
||||
tmp.focus();
|
||||
tmp.select();
|
||||
tmp.setSelectionRange(0, 99999);
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(tmp);
|
||||
showCopyFeedback(btn);
|
||||
}
|
||||
}
|
||||
function showCopyFeedback(btn) {
|
||||
var orig = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="ti ti-check"></i>';
|
||||
btn.classList.add('btn-success');
|
||||
btn.classList.remove('btn-outline-primary');
|
||||
setTimeout(function() {
|
||||
btn.innerHTML = orig;
|
||||
btn.classList.remove('btn-success');
|
||||
btn.classList.add('btn-outline-primary');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// --- CSRF Protection ---
|
||||
// Reads the _csrf cookie and injects a hidden field into every POST form.
|
||||
// Also provides a helper for fetch/AJAX calls.
|
||||
(function() {
|
||||
function getCsrfToken() {
|
||||
var m = document.cookie.match(/(?:^|;\s*)_csrf=([^;]*)/);
|
||||
return m ? decodeURIComponent(m[1]) : '';
|
||||
}
|
||||
// Inject hidden _csrf field into all POST forms
|
||||
document.querySelectorAll('form').forEach(function(form) {
|
||||
if ((form.method || 'get').toLowerCase() === 'post' && !form.querySelector('input[name="_csrf"]')) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = '_csrf';
|
||||
input.value = getCsrfToken();
|
||||
form.prepend(input);
|
||||
}
|
||||
});
|
||||
// Expose globally for fetch/AJAX calls
|
||||
window._csrfToken = getCsrfToken;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user