feat: ✨ add ability to change username, password and setup totp
Release / release (pull_request) Successful in 20s
Release / release (pull_request) Successful in 20s
This commit is contained in:
+16
-2
@@ -32,8 +32,15 @@
|
||||
<a href="{{ url_for('prune_page') }}" class="text-neutral-300 hover:text-orange-500 px-3 py-2 rounded-md text-sm font-medium transition-colors">
|
||||
Prune
|
||||
</a>
|
||||
{% if session.is_admin %}
|
||||
<a href="{{ url_for('users') }}" class="text-neutral-300 hover:text-orange-500 px-3 py-2 rounded-md text-sm font-medium transition-colors">
|
||||
Users
|
||||
</a>
|
||||
{% endif %}
|
||||
<span class="text-neutral-400">|</span>
|
||||
<span class="text-neutral-300">{{ session.username }}</span>
|
||||
<a href="{{ url_for('profile') }}" class="text-neutral-300 hover:text-orange-500 px-3 py-2 rounded-md text-sm font-medium transition-colors">
|
||||
{{ session.username }}
|
||||
</a>
|
||||
<a href="{{ url_for('logout') }}" class="bg-neutral-700 hover:bg-neutral-600 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
|
||||
Logout
|
||||
</a>
|
||||
@@ -65,8 +72,15 @@
|
||||
<a href="{{ url_for('prune_page') }}" class="block text-neutral-300 hover:text-orange-500 px-3 py-2 rounded-md text-sm font-medium transition-colors hover:bg-neutral-700">
|
||||
Prune
|
||||
</a>
|
||||
{% if session.is_admin %}
|
||||
<a href="{{ url_for('users') }}" class="block text-neutral-300 hover:text-orange-500 px-3 py-2 rounded-md text-sm font-medium transition-colors hover:bg-neutral-700">
|
||||
Users
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="border-t border-neutral-700 pt-2 mt-2">
|
||||
<div class="px-3 py-2 text-sm text-neutral-400">{{ session.username }}</div>
|
||||
<a href="{{ url_for('profile') }}" class="block px-3 py-2 text-sm text-neutral-400 hover:text-orange-500 rounded-md transition-colors hover:bg-neutral-700">
|
||||
{{ session.username }}
|
||||
</a>
|
||||
<a href="{{ url_for('logout') }}" class="block text-neutral-300 hover:text-orange-500 px-3 py-2 rounded-md text-sm font-medium transition-colors hover:bg-neutral-700">
|
||||
Logout
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Two-Factor Verification - OPNsense Backup Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-[calc(100vh-12rem)] flex items-center justify-center">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="bg-neutral-800 rounded-lg shadow-lg p-8 border border-neutral-700">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-orange-500 mb-2">Two-Factor Verification</h1>
|
||||
<p class="text-neutral-400">Enter the code from your authenticator app.</p>
|
||||
{% if username %}
|
||||
<p class="text-sm text-neutral-500 mt-2">Signing in as <span class="text-neutral-300">{{ username }}</span></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('login_totp') }}" class="space-y-6">
|
||||
<div>
|
||||
<label for="otp_code" class="block text-sm font-medium text-neutral-300 mb-2">
|
||||
TOTP Code
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="otp_code"
|
||||
name="otp_code"
|
||||
required
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
class="w-full px-4 py-2 bg-neutral-700 border border-neutral-600 rounded-md text-neutral-100 placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
placeholder="6-digit code"
|
||||
>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200 hover:cursor-pointer">
|
||||
Verify and Sign In
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<a href="{{ url_for('logout') }}" class="text-sm text-neutral-400 hover:text-orange-500 transition-colors">
|
||||
Cancel and return to login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,121 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Profile - OPNsense Backup Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-neutral-100 mb-2">Profile</h1>
|
||||
<p class="text-neutral-400">Manage your username, password, and multi-factor authentication.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
||||
<h2 class="text-xl font-bold text-neutral-100 mb-4">Change Username</h2>
|
||||
<form method="post" action="{{ url_for('profile') }}" class="space-y-4">
|
||||
<input type="hidden" name="action" value="update_username">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-neutral-300 mb-2">Current Username</label>
|
||||
<input type="text" disabled value="{{ user.username }}" class="w-full px-4 py-2 bg-neutral-700 border border-neutral-600 rounded-md text-neutral-400">
|
||||
</div>
|
||||
<div>
|
||||
<label for="new_username" class="block text-sm font-medium text-neutral-300 mb-2">New Username</label>
|
||||
<input id="new_username" name="new_username" type="text" required class="w-full px-4 py-2 bg-neutral-700 border border-neutral-600 rounded-md text-neutral-100 focus:outline-none focus:border-orange-500 transition-colors">
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-2 bg-orange-600 hover:bg-orange-500 text-white rounded-md text-sm font-medium transition-colors hover:cursor-pointer">Update Username</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
||||
<h2 class="text-xl font-bold text-neutral-100 mb-4">Change Password</h2>
|
||||
<form method="post" action="{{ url_for('profile') }}" class="space-y-4">
|
||||
<input type="hidden" name="action" value="update_password">
|
||||
<div>
|
||||
<label for="current_password" class="block text-sm font-medium text-neutral-300 mb-2">Current Password</label>
|
||||
<input id="current_password" name="current_password" type="password" required class="w-full px-4 py-2 bg-neutral-700 border border-neutral-600 rounded-md text-neutral-100 focus:outline-none focus:border-orange-500 transition-colors">
|
||||
</div>
|
||||
<div>
|
||||
<label for="new_password" class="block text-sm font-medium text-neutral-300 mb-2">New Password</label>
|
||||
<input id="new_password" name="new_password" type="password" required class="w-full px-4 py-2 bg-neutral-700 border border-neutral-600 rounded-md text-neutral-100 focus:outline-none focus:border-orange-500 transition-colors">
|
||||
</div>
|
||||
<div>
|
||||
<label for="confirm_password" class="block text-sm font-medium text-neutral-300 mb-2">Confirm New Password</label>
|
||||
<input id="confirm_password" name="confirm_password" type="password" required class="w-full px-4 py-2 bg-neutral-700 border border-neutral-600 rounded-md text-neutral-100 focus:outline-none focus:border-orange-500 transition-colors">
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-2 bg-orange-600 hover:bg-orange-500 text-white rounded-md text-sm font-medium transition-colors hover:cursor-pointer">Update Password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6 space-y-5">
|
||||
<h2 class="text-xl font-bold text-neutral-100">Two-Factor Authentication (TOTP)</h2>
|
||||
<p class="text-neutral-400 text-sm">
|
||||
Use an authenticator app (Aegis, Authy, 1Password, Google Authenticator, etc.) to secure your account.
|
||||
</p>
|
||||
|
||||
{% if user.totp_enabled %}
|
||||
<div class="bg-green-900/30 border border-green-700 rounded-md p-4 text-sm text-green-300">
|
||||
TOTP is currently enabled for your account.
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('profile') }}">
|
||||
<input type="hidden" name="action" value="disable_totp">
|
||||
<button type="submit" class="px-4 py-2 bg-red-700 hover:bg-red-600 text-white rounded-md text-sm font-medium transition-colors hover:cursor-pointer">
|
||||
Disable TOTP
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
{% if not user.totp_secret %}
|
||||
<form method="post" action="{{ url_for('profile') }}">
|
||||
<input type="hidden" name="action" value="generate_totp_secret">
|
||||
<button type="submit" class="px-4 py-2 bg-orange-600 hover:bg-orange-500 text-white rounded-md text-sm font-medium transition-colors hover:cursor-pointer">
|
||||
Generate TOTP Secret
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="bg-neutral-900 border border-neutral-700 rounded-md p-4 space-y-2">
|
||||
<p class="text-sm text-neutral-300"><strong>Scan QR Code:</strong></p>
|
||||
<div id="totp-qrcode" class="bg-white p-2 rounded-md inline-block"></div>
|
||||
<p class="text-sm text-neutral-300"><strong>Secret:</strong> <code class="text-orange-400">{{ user.totp_secret }}</code></p>
|
||||
<p class="text-sm text-neutral-300"><strong>OTPAuth URI:</strong></p>
|
||||
<code id="totp-uri" class="text-xs text-orange-400 break-all block">{{ totp_uri }}</code>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('profile') }}" class="space-y-4">
|
||||
<input type="hidden" name="action" value="enable_totp">
|
||||
<div>
|
||||
<label for="otp_code" class="block text-sm font-medium text-neutral-300 mb-2">Enter a code from your authenticator app</label>
|
||||
<input id="otp_code" name="otp_code" type="text" inputmode="numeric" required class="w-full max-w-xs px-4 py-2 bg-neutral-700 border border-neutral-600 rounded-md text-neutral-100 focus:outline-none focus:border-orange-500 transition-colors" placeholder="123456">
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button type="submit" class="px-4 py-2 bg-orange-600 hover:bg-orange-500 text-white rounded-md text-sm font-medium transition-colors hover:cursor-pointer">
|
||||
Verify and Enable
|
||||
</button>
|
||||
<button formaction="{{ url_for('profile') }}" name="action" value="generate_totp_secret" class="px-4 py-2 bg-neutral-700 hover:bg-neutral-600 text-neutral-300 rounded-md text-sm font-medium transition-colors hover:cursor-pointer">
|
||||
Regenerate Secret
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if totp_uri %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
const qrContainer = document.getElementById('totp-qrcode');
|
||||
const uriElement = document.getElementById('totp-uri');
|
||||
if (!qrContainer || !uriElement) return;
|
||||
|
||||
const otpUri = uriElement.textContent.trim();
|
||||
if (!otpUri) return;
|
||||
|
||||
new QRCode(qrContainer, {
|
||||
text: otpUri,
|
||||
width: 192,
|
||||
height: 192,
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -170,10 +170,10 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="flex justify-end gap-3 flex-wrap">
|
||||
<button type="submit" name="action" value="run_now" class="px-4 py-2 bg-neutral-700 hover:bg-neutral-600 text-neutral-300 rounded-md text-sm font-medium transition-colors">
|
||||
<button type="submit" name="action" value="run_now" class="px-4 py-2 bg-neutral-700 hover:bg-neutral-600 text-neutral-300 rounded-md text-sm font-medium transition-colors hover:cursor-pointer">
|
||||
Save & Run Now
|
||||
</button>
|
||||
<button type="submit" class="px-5 py-2 bg-orange-600 hover:bg-orange-500 text-white rounded-md text-sm font-medium transition-colors">
|
||||
<button type="submit" class="px-5 py-2 bg-orange-600 hover:bg-orange-500 text-white rounded-md text-sm font-medium transition-colors hover:cursor-pointer">
|
||||
Save Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Users - OPNsense Backup Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-neutral-100 mb-2">Users</h1>
|
||||
<p class="text-neutral-400">Create and manage user accounts.</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 p-6">
|
||||
<h2 class="text-xl font-bold text-neutral-100 mb-4">Create User</h2>
|
||||
<form method="post" action="{{ url_for('create_user') }}" class="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
|
||||
<div class="md:col-span-1">
|
||||
<label for="username" class="block text-sm font-medium text-neutral-300 mb-2">Username</label>
|
||||
<input id="username" name="username" type="text" required class="w-full px-4 py-2 bg-neutral-700 border border-neutral-600 rounded-md text-neutral-100 focus:outline-none focus:border-orange-500 transition-colors">
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label for="password" class="block text-sm font-medium text-neutral-300 mb-2">Password</label>
|
||||
<input id="password" name="password" type="password" required class="w-full px-4 py-2 bg-neutral-700 border border-neutral-600 rounded-md text-neutral-100 focus:outline-none focus:border-orange-500 transition-colors">
|
||||
</div>
|
||||
<div class="md:col-span-1 flex items-center gap-3">
|
||||
<label class="flex items-center gap-2 text-sm text-neutral-300">
|
||||
<input type="checkbox" name="is_admin">
|
||||
Admin
|
||||
</label>
|
||||
<button type="submit" class="px-4 py-2 bg-orange-600 hover:bg-orange-500 text-white rounded-md text-sm font-medium transition-colors hover:cursor-pointer">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="bg-neutral-800 rounded-lg border border-neutral-700 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-neutral-700">
|
||||
<h2 class="text-xl font-bold text-neutral-100">Existing Users</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-neutral-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Username</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Role</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">TOTP</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Created</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-neutral-300 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-neutral-700">
|
||||
{% for user in users %}
|
||||
<tr class="hover:bg-neutral-700/50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-100">{{ user.username }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-300">
|
||||
{% if user.is_admin %}Admin{% else %}User{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-300">
|
||||
{% if user.totp_enabled %}Enabled{% else %}Disabled{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-400">
|
||||
{{ user.created_at.strftime('%Y-%m-%d %H:%M') if user.created_at else 'N/A' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<div class="flex gap-4">
|
||||
<form method="post" action="{{ url_for('toggle_user_admin', user_id=user.id) }}">
|
||||
<button type="submit" class="text-orange-500 hover:text-orange-400 hover:cursor-pointer">
|
||||
{% if user.is_admin %}Remove Admin{% else %}Make Admin{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('delete_user', user_id=user.id) }}" onsubmit="return confirm('Delete user {{ user.username }}?');">
|
||||
<button type="submit" class="text-red-500 hover:text-red-400 hover:cursor-pointer">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user