feat: two factor authentication

This commit is contained in:
2025-12-24 12:17:44 +00:00
parent b5fa9ef6ae
commit 5037c1b578
12 changed files with 1052 additions and 19 deletions
+144
View File
@@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Account Settings - {{ NAME }} IPAM</title>
<link rel="icon" type="image/png" href="{{ LOGO_PNG }}">
<link href="/static/css/output.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
</head>
<body class="bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 min-h-screen flex flex-col">
{% include 'header.html' %}
<div class="flex-1 flex items-center justify-center mx-4">
<div class="container py-8 max-w-3xl pt-20">
<h1 class="text-3xl font-bold mb-6 text-center">
<i class="fas fa-user-cog mr-2"></i>
Account Settings
</h1>
{% if success %}
<div class="bg-green-200 dark:bg-green-900 text-green-800 dark:text-green-200 p-4 rounded-lg mb-6">
<i class="fas fa-check-circle mr-2"></i>{{ success }}
</div>
{% endif %}
{% if error %}
<div class="bg-red-200 dark:bg-red-900 text-red-800 dark:text-red-200 p-4 rounded-lg mb-6">
<i class="fas fa-exclamation-circle mr-2"></i>{{ error }}
</div>
{% endif %}
<div class="space-y-6">
<!-- Change Password Section -->
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<h2 class="text-2xl font-bold mb-4">
<i class="fas fa-key mr-2"></i>
Change Password
</h2>
<form method="POST" action="/account/change-password" class="space-y-4">
<div>
<label for="current_password" class="block text-sm font-medium mb-2">Current Password</label>
<input type="password" name="current_password" id="current_password"
class="w-full p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border border-gray-600" required>
</div>
<div>
<label for="new_password" class="block text-sm font-medium mb-2">New Password</label>
<input type="password" name="new_password" id="new_password"
class="w-full p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border border-gray-600" required>
</div>
<div>
<label for="confirm_password" class="block text-sm font-medium mb-2">Confirm New Password</label>
<input type="password" name="confirm_password" id="confirm_password"
class="w-full p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border border-gray-600" required>
</div>
<button type="submit" class="w-full bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center">
<i class="fas fa-save"></i>
<span>Change Password</span>
</button>
</form>
</div>
<!-- Two-Factor Authentication Section -->
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<h2 class="text-2xl font-bold mb-4">
<i class="fas fa-shield-alt mr-2"></i>
Two-Factor Authentication
</h2>
{% if totp_enabled %}
<div class="space-y-4">
<div class="bg-green-200 dark:bg-green-900 text-green-800 dark:text-green-200 p-4 rounded-lg">
<i class="fas fa-check-circle mr-2"></i>
2FA is currently <strong>enabled</strong> for your account.
</div>
<form method="POST" action="/account/disable-2fa" class="mt-4" onsubmit="return confirm('Are you sure you want to disable two-factor authentication? This will make your account less secure.');">
<input type="hidden" name="confirm_disable" value="true">
<button type="submit" class="w-full bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center">
<i class="fas fa-times"></i>
<span>Disable 2FA</span>
</button>
</form>
<div class="mt-6">
<h3 class="text-lg font-semibold mb-3">
<i class="fas fa-key mr-2"></i>
Backup Codes
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
Backup codes can be used to access your account if you lose your authenticator device.
Each code can only be used once.
</p>
{% if backup_codes %}
<div class="bg-gray-300 dark:bg-zinc-900 p-6 rounded-lg mb-4">
<div class="grid grid-cols-2 gap-4 font-mono text-center">
{% for code_pair in backup_codes %}
<div class="p-3 bg-gray-200 dark:bg-zinc-800 rounded border border-gray-400 dark:border-zinc-600">
{{ code_pair }}
</div>
{% endfor %}
</div>
</div>
<button onclick="window.print()" class="w-full bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center">
<i class="fas fa-print"></i>
<span>Print Backup Codes</span>
</button>
{% else %}
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
You don't have any backup codes. Generate new ones below.
</p>
{% endif %}
<form method="POST" action="/account/regenerate-backup-codes" class="mt-4" onsubmit="return confirm('This will invalidate your existing backup codes. Are you sure you want to generate new ones?');">
<button type="submit" class="w-full bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center">
<i class="fas fa-redo"></i>
<span>Regenerate Backup Codes</span>
</button>
</form>
</div>
</div>
{% else %}
<div class="space-y-4">
<div class="bg-yellow-200 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 p-4 rounded-lg">
<i class="fas fa-exclamation-triangle mr-2"></i>
2FA is currently <strong>disabled</strong> for your account.
{% if role_requires_2fa %}
<br><strong>Note:</strong> Your role requires 2FA. You should enable it now.
{% endif %}
</div>
<a href="/account/enable-2fa" class="block w-full bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center text-center">
<i class="fas fa-shield-alt"></i>
<span>Enable 2FA</span>
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</body>
</html>
+133
View File
@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enable Two-Factor Authentication - {{ NAME }} IPAM</title>
<link rel="icon" type="image/png" href="{{ LOGO_PNG }}">
<link href="/static/css/output.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
</head>
<body class="bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 min-h-screen flex flex-col">
{% include 'header.html' %}
<div class="flex-1 flex items-center justify-center mx-4">
<div class="container py-8 max-w-2xl pt-20">
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<div class="mb-4">
<a href="/account" class="text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200">
<i class="fas fa-arrow-left mr-2"></i>Back to Account Settings
</a>
</div>
<h1 class="text-3xl font-bold mb-6 text-center">
<i class="fas fa-shield-alt mr-2"></i>
Enable Two-Factor Authentication
</h1>
{% if step == 'generate' %}
<div class="space-y-4">
<p class="text-center text-gray-700 dark:text-gray-300">
Two-factor authentication adds an extra layer of security to your account.
</p>
<p class="text-center text-gray-600 dark:text-gray-400 text-sm">
You'll need an authenticator app like Google Authenticator, Authy, or Microsoft Authenticator.
</p>
<form method="POST" class="mt-6">
<input type="hidden" name="action" value="generate">
<button type="submit" class="w-full bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center">
<i class="fas fa-qrcode"></i>
<span>Generate QR Code</span>
</button>
</form>
</div>
{% elif step == 'verify' %}
<div class="space-y-6">
<div class="text-center">
<p class="mb-4 text-gray-700 dark:text-gray-300">
Scan this QR code with your authenticator app:
</p>
<div class="flex justify-center mb-4">
<img src="data:image/png;base64,{{ qr_code }}" alt="QR Code" class="border-4 border-gray-400 dark:border-zinc-600 rounded-lg p-2 bg-white">
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
Or enter this secret manually:
</p>
<div class="bg-gray-300 dark:bg-zinc-900 p-3 rounded-lg font-mono text-sm break-all text-center">
{{ secret }}
</div>
</div>
<form method="POST" class="space-y-4">
<input type="hidden" name="action" value="verify">
<div>
<label for="code" class="block text-sm font-medium mb-2">Enter the 6-digit code from your app:</label>
<input type="text" name="code" id="code" maxlength="6" pattern="[0-9]{6}"
class="w-full p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border border-gray-600 text-center text-2xl font-mono tracking-widest"
placeholder="000000" required autofocus>
</div>
{% if error %}
<div class="bg-red-200 dark:bg-red-900 text-red-800 dark:text-red-200 p-3 rounded-lg">
<i class="fas fa-exclamation-circle mr-2"></i>{{ error }}
</div>
{% endif %}
<button type="submit" class="w-full bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center">
<i class="fas fa-check"></i>
<span>Verify & Enable 2FA</span>
</button>
</form>
</div>
{% elif step == 'backup_codes' %}
<div class="space-y-6">
<div class="bg-yellow-200 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 p-4 rounded-lg">
<p class="font-semibold mb-2">
<i class="fas fa-exclamation-triangle mr-2"></i>
Important: Save Your Backup Codes
</p>
<p class="text-sm">
These codes can be used to access your account if you lose your authenticator device.
Store them in a safe place. Each code can only be used once.
</p>
</div>
<div class="bg-gray-300 dark:bg-zinc-900 p-6 rounded-lg">
<h2 class="text-xl font-bold mb-4 text-center">Your Backup Codes</h2>
<div class="grid grid-cols-2 gap-4 font-mono text-center">
{% for code_pair in backup_codes %}
<div class="p-3 bg-gray-200 dark:bg-zinc-800 rounded border border-gray-400 dark:border-zinc-600">
{{ code_pair }}
</div>
{% endfor %}
</div>
</div>
<div class="flex gap-4">
<button onclick="window.print()" class="flex-1 bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center">
<i class="fas fa-print"></i>
<span>Print Codes</span>
</button>
<a href="/account" class="flex-1 bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center text-center">
<i class="fas fa-check"></i>
<span>Continue</span>
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<script>
// Auto-focus code input and move cursor on input
const codeInput = document.getElementById('code');
if (codeInput) {
codeInput.addEventListener('input', function(e) {
this.value = this.value.replace(/[^0-9]/g, '');
if (this.value.length === 6) {
this.form.submit();
}
});
}
</script>
</body>
</html>
+56 -12
View File
@@ -18,22 +18,44 @@
</div>
<nav class="hidden lg:flex items-center space-x-6 flex-shrink-0" id="main-nav">
{% if has_permission('view_index') %}
<a href="/" class="text-gray-200 hover:text-gray-400 font-medium">Home</a>
<a href="/" class="text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
<i class="fas fa-home"></i>
<span>Home</span>
</a>
{% endif %}
{% if has_permission('view_devices') %}
<a href="/devices" class="text-gray-200 hover:text-gray-400 font-medium">Devices</a>
<a href="/devices" class="text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
<i class="fas fa-server"></i>
<span>Devices</span>
</a>
{% endif %}
{% if has_permission('view_racks') %}
<a href="/racks" class="text-gray-200 hover:text-gray-400 font-medium">Racks</a>
<a href="/racks" class="text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
<i class="fas fa-th"></i>
<span>Racks</span>
</a>
{% endif %}
{% if has_permission('view_admin') %}
<a href="/admin" class="text-gray-200 hover:text-gray-400 font-medium">Admin</a>
<a href="/admin" class="text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
<i class="fas fa-cog"></i>
<span>Admin</span>
</a>
{% endif %}
{% if has_permission('view_help') %}
<a href="/help" class="text-gray-200 hover:text-gray-400 font-medium">Help</a>
<a href="/help" class="text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
<i class="fas fa-question-circle"></i>
<span>Help</span>
</a>
{% endif %}
{% if current_user_name %}
<a href="/logout" class="text-gray-200 hover:text-gray-400 font-medium">Logout</a>
<a href="/account" class="text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
<i class="fas fa-user-cog"></i>
<span>Account</span>
</a>
<a href="/logout" class="text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
<i class="fas fa-sign-out-alt"></i>
<span>Logout</span>
</a>
{% endif %}
</nav>
<button class="lg:hidden flex items-center text-gray-200 hover:cursor-pointer focus:outline-none flex-shrink-0" id="nav-toggle" aria-label="Open navigation menu">
@@ -53,22 +75,44 @@
</div>
</form>
{% if has_permission('view_index') %}
<a href="/" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Home</a>
<a href="/" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
<i class="fas fa-home"></i>
<span>Home</span>
</a>
{% endif %}
{% if has_permission('view_devices') %}
<a href="/devices" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Devices</a>
<a href="/devices" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
<i class="fas fa-server"></i>
<span>Devices</span>
</a>
{% endif %}
{% if has_permission('view_racks') %}
<a href="/racks" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Racks</a>
<a href="/racks" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
<i class="fas fa-th"></i>
<span>Racks</span>
</a>
{% endif %}
{% if has_permission('view_admin') %}
<a href="/admin" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Admin</a>
<a href="/admin" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
<i class="fas fa-cog"></i>
<span>Admin</span>
</a>
{% endif %}
{% if has_permission('view_help') %}
<a href="/help" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Help</a>
<a href="/help" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
<i class="fas fa-question-circle"></i>
<span>Help</span>
</a>
{% endif %}
{% if current_user_name %}
<a href="/logout" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Logout</a>
<a href="/account" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
<i class="fas fa-user-cog"></i>
<span>Account</span>
</a>
<a href="/logout" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
<i class="fas fa-sign-out-alt"></i>
<span>Logout</span>
</a>
{% endif %}
</div>
<script src="/static/js/header.js"></script>
+65
View File
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Regenerate Backup Codes - {{ NAME }} IPAM</title>
<link rel="icon" type="image/png" href="{{ LOGO_PNG }}">
<link href="/static/css/output.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
</head>
<body class="bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 min-h-screen flex flex-col">
{% include 'header.html' %}
<div class="flex-1 flex items-center justify-center mx-4">
<div class="container py-8 max-w-2xl pt-20">
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<div class="mb-4">
<a href="/account" class="text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200">
<i class="fas fa-arrow-left mr-2"></i>Back to Account Settings
</a>
</div>
<h1 class="text-3xl font-bold mb-6 text-center">
<i class="fas fa-key mr-2"></i>
New Backup Codes
</h1>
<div class="space-y-6">
<div class="bg-yellow-200 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 p-4 rounded-lg">
<p class="font-semibold mb-2">
<i class="fas fa-exclamation-triangle mr-2"></i>
Important: Save Your New Backup Codes
</p>
<p class="text-sm">
Your old backup codes have been invalidated. These new codes can be used to access your account if you lose your authenticator device.
Store them in a safe place. Each code can only be used once.
</p>
</div>
<div class="bg-gray-300 dark:bg-zinc-900 p-6 rounded-lg">
<h2 class="text-xl font-bold mb-4 text-center">Your New Backup Codes</h2>
<div class="grid grid-cols-2 gap-4 font-mono text-center">
{% for code_pair in backup_codes %}
<div class="p-3 bg-gray-200 dark:bg-zinc-800 rounded border border-gray-400 dark:border-zinc-600">
{{ code_pair }}
</div>
{% endfor %}
</div>
</div>
<div class="flex gap-4">
<button onclick="window.print()" class="flex-1 bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center">
<i class="fas fa-print"></i>
<span>Print Codes</span>
</button>
<a href="/account" class="flex-1 bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center text-center">
<i class="fas fa-check"></i>
<span>Done</span>
</a>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
+128
View File
@@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Setup Two-Factor Authentication - {{ NAME }} IPAM</title>
<link rel="icon" type="image/png" href="{{ LOGO_PNG }}">
<link href="/static/css/output.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
</head>
<body class="bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 min-h-screen flex flex-col">
{% include 'header.html' %}
<div class="flex-1 flex items-center justify-center mx-4">
<div class="container py-8 max-w-2xl pt-20">
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<h1 class="text-3xl font-bold mb-6 text-center">
<i class="fas fa-shield-alt mr-2"></i>
Setup Two-Factor Authentication
</h1>
{% if step == 'generate' %}
<div class="space-y-4">
<p class="text-center text-gray-700 dark:text-gray-300">
Your role requires two-factor authentication. Let's set it up now.
</p>
<p class="text-center text-gray-600 dark:text-gray-400 text-sm">
You'll need an authenticator app like Google Authenticator, Authy, or Microsoft Authenticator.
</p>
<form method="POST" class="mt-6">
<input type="hidden" name="action" value="generate">
<button type="submit" class="w-full bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center">
<i class="fas fa-qrcode"></i>
<span>Generate QR Code</span>
</button>
</form>
</div>
{% elif step == 'verify' %}
<div class="space-y-6">
<div class="text-center">
<p class="mb-4 text-gray-700 dark:text-gray-300">
Scan this QR code with your authenticator app:
</p>
<div class="flex justify-center mb-4">
<img src="data:image/png;base64,{{ qr_code }}" alt="QR Code" class="border-4 border-gray-400 dark:border-zinc-600 rounded-lg p-2 bg-white">
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
Or enter this secret manually:
</p>
<div class="bg-gray-300 dark:bg-zinc-900 p-3 rounded-lg font-mono text-sm break-all text-center">
{{ secret }}
</div>
</div>
<form method="POST" class="space-y-4">
<input type="hidden" name="action" value="verify">
<div>
<label for="code" class="block text-sm font-medium mb-2">Enter the 6-digit code from your app:</label>
<input type="text" name="code" id="code" maxlength="6" pattern="[0-9]{6}"
class="w-full p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border border-gray-600 text-center text-2xl font-mono tracking-widest"
placeholder="000000" required autofocus>
</div>
{% if error %}
<div class="bg-red-200 dark:bg-red-900 text-red-800 dark:text-red-200 p-3 rounded-lg">
<i class="fas fa-exclamation-circle mr-2"></i>{{ error }}
</div>
{% endif %}
<button type="submit" class="w-full bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center">
<i class="fas fa-check"></i>
<span>Verify & Enable 2FA</span>
</button>
</form>
</div>
{% elif step == 'backup_codes' %}
<div class="space-y-6">
<div class="bg-yellow-200 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 p-4 rounded-lg">
<p class="font-semibold mb-2">
<i class="fas fa-exclamation-triangle mr-2"></i>
Important: Save Your Backup Codes
</p>
<p class="text-sm">
These codes can be used to access your account if you lose your authenticator device.
Store them in a safe place. Each code can only be used once.
</p>
</div>
<div class="bg-gray-300 dark:bg-zinc-900 p-6 rounded-lg">
<h2 class="text-xl font-bold mb-4 text-center">Your Backup Codes</h2>
<div class="grid grid-cols-2 gap-4 font-mono text-center">
{% for code_pair in backup_codes %}
<div class="p-3 bg-gray-200 dark:bg-zinc-800 rounded border border-gray-400 dark:border-zinc-600">
{{ code_pair }}
</div>
{% endfor %}
</div>
</div>
<div class="flex gap-4">
<button onclick="window.print()" class="flex-1 bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center">
<i class="fas fa-print"></i>
<span>Print Codes</span>
</button>
<a href="/" class="flex-1 bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center text-center">
<i class="fas fa-check"></i>
<span>Continue</span>
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<script>
// Auto-focus code input and move cursor on input
const codeInput = document.getElementById('code');
if (codeInput) {
codeInput.addEventListener('input', function(e) {
this.value = this.value.replace(/[^0-9]/g, '');
if (this.value.length === 6) {
this.form.submit();
}
});
}
</script>
</body>
</html>
+25 -1
View File
@@ -127,7 +127,7 @@
</div>
{% if can_manage_roles %}
<div class="flex space-x-2">
<button type="button" onclick="editRole({{ role[0] }}, '{{ role[1]|replace("'", "\\'") }}', '{{ (role[2] or '')|replace("'", "\\'") }}'); return false;" class="text-gray-900 hover:text-gray-600 dark:text-gray-100 dark:hover:text-gray-300 hover:cursor-pointer" title="Edit Role">
<button type="button" onclick="editRole({{ role[0] }}, '{{ role[1]|replace("'", "\\'") }}', '{{ (role[2] or '')|replace("'", "\\'") }}', {{ 'true' if role[3] else 'false' }}); return false;" class="text-gray-900 hover:text-gray-600 dark:text-gray-100 dark:hover:text-gray-300 hover:cursor-pointer" title="Edit Role">
<i class="fas fa-edit"></i>
</button>
<button type="button" onclick="deleteRole({{ role[0] }}, '{{ role[1]|replace("'", "\\'") }}'); return false;" class="text-gray-900 hover:text-gray-600 dark:text-gray-100 dark:hover:text-gray-300 hover:cursor-pointer" title="Delete Role">
@@ -211,6 +211,18 @@
<input type="text" name="role_name" placeholder="Role Name" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600" required>
<input type="text" name="role_description" placeholder="Description (optional)" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600">
</div>
<div class="mb-4">
<label class="flex items-center cursor-pointer">
<input type="checkbox" name="require_2fa" class="mr-2 w-4 h-4">
<span class="text-sm">
<i class="fas fa-shield-alt mr-1"></i>
Require Two-Factor Authentication for this role
</span>
</label>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1 ml-6">
Users with this role will be required to set up 2FA on their next login.
</p>
</div>
<div class="mb-4">
<h3 class="font-bold mb-3">Permissions</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 max-h-96 overflow-y-auto border border-gray-600 rounded-lg p-4 bg-gray-300 dark:bg-zinc-900">
@@ -322,6 +334,18 @@
<input type="text" name="role_name" id="edit-role-name" placeholder="Role Name" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600" required>
<input type="text" name="role_description" id="edit-role-description" placeholder="Description (optional)" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600">
</div>
<div class="mb-4">
<label class="flex items-center cursor-pointer">
<input type="checkbox" name="require_2fa" id="edit-role-require-2fa" class="mr-2 w-4 h-4">
<span class="text-sm">
<i class="fas fa-shield-alt mr-1"></i>
Require Two-Factor Authentication for this role
</span>
</label>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1 ml-6">
Users with this role will be required to set up 2FA on their next login.
</p>
</div>
<div class="mb-4">
<h3 class="font-bold mb-3">Permissions</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 max-h-96 overflow-y-auto border border-gray-600 rounded-lg p-4 bg-gray-300 dark:bg-zinc-900" id="edit-role-permissions">
+95
View File
@@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verify Two-Factor Authentication - {{ NAME }} IPAM</title>
<link rel="icon" type="image/png" href="{{ LOGO_PNG }}">
<link href="/static/css/output.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
</head>
<body class="bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 min-h-screen flex flex-col">
{% include 'header.html' %}
<div class="flex-1 flex items-center justify-center mx-4">
<div class="container py-8 max-w-md pt-20">
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<h1 class="text-3xl font-bold mb-6 text-center">
<i class="fas fa-shield-alt mr-2"></i>
Two-Factor Authentication
</h1>
<div class="space-y-4">
<p class="text-center text-gray-700 dark:text-gray-300">
Enter the 6-digit code from your authenticator app:
</p>
<form method="POST" class="space-y-4">
<div>
<input type="text" name="code" id="code" maxlength="10"
class="w-full p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border border-gray-600 text-center text-2xl font-mono tracking-widest"
placeholder="000000" required autofocus>
</div>
{% if error %}
<div class="bg-red-200 dark:bg-red-900 text-red-800 dark:text-red-200 p-3 rounded-lg">
<i class="fas fa-exclamation-circle mr-2"></i>{{ error }}
</div>
{% endif %}
<button type="submit" name="use_backup" value="false" class="w-full bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-3 rounded-lg flex items-center gap-2 justify-center">
<i class="fas fa-check"></i>
<span>Verify Code</span>
</button>
<div class="text-center">
<button type="button" onclick="toggleBackupMode()" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 underline">
Use backup code instead
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
let backupMode = false;
function toggleBackupMode() {
backupMode = !backupMode;
const codeInput = document.getElementById('code');
const form = codeInput.closest('form');
const submitBtn = form.querySelector('button[type="submit"]');
if (backupMode) {
codeInput.placeholder = "Enter backup code";
codeInput.maxLength = 20;
codeInput.pattern = "";
submitBtn.innerHTML = '<i class="fas fa-key"></i><span>Verify Backup Code</span>';
submitBtn.setAttribute('name', 'use_backup');
submitBtn.setAttribute('value', 'true');
} else {
codeInput.placeholder = "000000";
codeInput.maxLength = 10;
codeInput.pattern = "[0-9]{6}";
submitBtn.innerHTML = '<i class="fas fa-check"></i><span>Verify Code</span>';
submitBtn.removeAttribute('name');
submitBtn.removeAttribute('value');
}
codeInput.value = '';
codeInput.focus();
}
// Auto-submit on 6 digits for TOTP codes
const codeInput = document.getElementById('code');
codeInput.addEventListener('input', function(e) {
if (!backupMode) {
this.value = this.value.replace(/[^0-9]/g, '');
if (this.value.length === 6) {
this.form.submit();
}
}
});
</script>
</body>
</html>