175 lines
8.4 KiB
HTML
175 lines
8.4 KiB
HTML
<!doctype html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
|
<title>Login - {{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 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: #1a2234; color-scheme: dark; }
|
|
html[data-bs-theme="light"],
|
|
html[data-bs-theme="light"] body { background-color: #f1f5f9; color-scheme: light; }
|
|
/* 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; }
|
|
/* Dimmed subtitle & footer on login page */
|
|
.login-subtitle { opacity: 0.55; }
|
|
.login-footer { opacity: 0.55; }
|
|
/* Consistent spacing between Tabler icons and adjacent text */
|
|
i.ti { margin-right: 0.25em; }
|
|
.btn-icon > i.ti, .input-icon-addon > i.ti { margin-right: 0; }
|
|
{{if loginBgImage}}
|
|
/* Custom background image */
|
|
body {
|
|
background-image: url('{{loginBgImage}}') !important;
|
|
background-size: cover !important;
|
|
background-position: center center !important;
|
|
background-repeat: no-repeat !important;
|
|
background-attachment: fixed !important;
|
|
}
|
|
{{if eq (loginTextColor) "dark"}}
|
|
/* Dark text for light background images */
|
|
.login-heading { text-shadow: 0 1px 6px rgba(255,255,255,0.7); color: #0a0f1a !important; }
|
|
.login-subtitle.text-secondary { text-shadow: 0 1px 4px rgba(255,255,255,0.6); color: rgba(10,15,26,0.85) !important; opacity: 0.75; }
|
|
.login-footer.text-secondary { text-shadow: 0 1px 4px rgba(255,255,255,0.6); color: rgba(10,15,26,0.80) !important; opacity: 0.70; }
|
|
.login-footer.text-secondary a.text-secondary { color: rgba(10,15,26,0.80) !important; }
|
|
{{else}}
|
|
/* Light text for dark background images (default) */
|
|
.login-heading { text-shadow: 0 2px 10px rgba(0,0,0,0.7); color: #fff !important; }
|
|
.login-subtitle.text-secondary { text-shadow: 0 2px 6px rgba(0,0,0,0.6); color: rgba(255,255,255,0.95) !important; opacity: 0.80; }
|
|
.login-footer.text-secondary { text-shadow: 0 2px 6px rgba(0,0,0,0.6); color: rgba(255,255,255,0.90) !important; opacity: 0.75; }
|
|
.login-footer.text-secondary a.text-secondary { color: rgba(255,255,255,0.90) !important; }
|
|
{{end}}
|
|
{{end}}
|
|
/* Glass card effect (always active) */
|
|
.card {
|
|
background: rgba(255, 255, 255, 0.15) !important;
|
|
backdrop-filter: blur(16px) saturate(180%);
|
|
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
|
border: 1px solid rgba(255, 255, 255, 0.2) !important;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
}
|
|
[data-bs-theme="dark"] .card {
|
|
background: rgba(26, 34, 52, 0.6) !important;
|
|
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
|
|
}
|
|
.card .form-control { background: rgba(255,255,255,0.55); }
|
|
[data-bs-theme="dark"] .card .form-control { background: rgba(0,0,0,0.3); }
|
|
.card .form-label, .card .h2, .card h2 { text-shadow: none; }
|
|
</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="d-flex flex-column">
|
|
<div class="page page-center">
|
|
<div class="container container-tight py-4">
|
|
<div class="text-center mb-4">
|
|
<h1 class="login-heading"><i class="ti ti-key"></i> {{appName}}</h1>
|
|
<p class="text-secondary login-subtitle">Centralized SSH Key Management and Deployment</p>
|
|
</div>
|
|
<div class="card card-md">
|
|
<div class="card-body">
|
|
{{if .MFAPending}}
|
|
<!-- MFA Verification Step -->
|
|
<h2 class="h2 text-center mb-4">
|
|
<i class="ti ti-shield-lock"></i> MFA Verification
|
|
</h2>
|
|
{{if .Error}}
|
|
<div class="alert alert-danger">
|
|
<i class="ti ti-alert-circle"></i> {{.Error}}
|
|
</div>
|
|
{{end}}
|
|
<p class="text-secondary text-center mb-3">Enter the 6-digit code from your authenticator app.</p>
|
|
<form action="/login/mfa" method="post" autocomplete="off">
|
|
<input type="hidden" name="mfa_token" value="{{.MFAToken}}">
|
|
<div class="mb-3">
|
|
<label class="form-label">MFA Code</label>
|
|
<div class="input-icon">
|
|
<span class="input-icon-addon"><i class="ti ti-shield-lock"></i></span>
|
|
<input type="text" name="mfa_code" class="form-control" placeholder="000000"
|
|
required pattern="[0-9]{6}" maxlength="6" autofocus
|
|
style="font-size: 1.5rem; letter-spacing: 0.5rem; text-align: center;">
|
|
</div>
|
|
</div>
|
|
<div class="form-footer">
|
|
<button type="submit" class="btn btn-primary w-100">
|
|
<i class="ti ti-shield-check"></i> Verify
|
|
</button>
|
|
</div>
|
|
</form>
|
|
<div class="text-center mt-3">
|
|
<a href="/login" class="text-secondary"><i class="ti ti-arrow-left"></i> Back to login</a>
|
|
</div>
|
|
{{else}}
|
|
<!-- Normal Login -->
|
|
<h2 class="h2 text-center mb-4">Login</h2>
|
|
{{if .Error}}
|
|
<div class="alert alert-danger">
|
|
<i class="ti ti-alert-circle"></i> {{.Error}}
|
|
</div>
|
|
{{end}}
|
|
<form action="/login" method="post" autocomplete="off">
|
|
<div class="mb-3">
|
|
<label class="form-label">Username</label>
|
|
<div class="input-icon">
|
|
<span class="input-icon-addon"><i class="ti ti-user"></i></span>
|
|
<input type="text" name="username" class="form-control" placeholder="Username" required autofocus>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Password</label>
|
|
<div class="input-icon">
|
|
<span class="input-icon-addon"><i class="ti ti-lock"></i></span>
|
|
<input type="password" name="password" class="form-control" placeholder="Password" required>
|
|
</div>
|
|
</div>
|
|
<div class="form-footer">
|
|
<button type="submit" class="btn btn-primary w-100">
|
|
<i class="ti ti-login"></i> Sign in
|
|
</button>
|
|
</div>
|
|
</form>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
<div class="text-center text-secondary login-footer mt-3">
|
|
© 2026 <a href="https://keywarden.app" target="_blank" rel="noopener noreferrer" class="text-secondary text-decoration-none">Keywarden</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script src="/static/js/tabler.min.js"></script>
|
|
<script>
|
|
// --- CSRF Protection ---
|
|
(function() {
|
|
var m = document.cookie.match(/(?:^|;\s*)_csrf=([^;]*)/);
|
|
var token = m ? decodeURIComponent(m[1]) : '';
|
|
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 = token;
|
|
form.prepend(input);
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|