Files
domnitor/app/Views/profile/index.twig
Hosteroid a265a58456 Enhance DNS discovery, validation & transfers
Add comprehensive DNS management and input validation, plus safer transfer and logging behavior.

- Add CronHelper utilities for cron scripts and unify logging/formatting.
- Improve InputValidator: sanitizeDomainInput and validateRootDomain (handles multi-level TLDs) and use throughout domain import/create flows to reject subdomains.
- DomainController: refactor DNS refresh to support quick/deep discovery (background deep scans), add endpoints to discover, add/delete/bulk-delete DNS records, import BIND zone files, enrich IP metadata via enrichIpDetails, and strengthen bulk import/reporting messages.
- DnsRecord model: add source column handling (discovered/manual/imported), avoid auto-deleting manual/imported records, and add helpers for deleting, bulk deleting, manual adding and importing zone records.
- Tag, NotificationGroup and Domain transfer logic: unlink groups when ownership changes, remove tags that belong to other users, add audit logging via Logger and improved bulk transfer reporting. TagController/View: show transferable users for admins and skip global tags on transfer.
- Notification channels (Discord, Mattermost, etc.) and EmailHelper: allow explicit subjects and improve payload fields based on notification type.
- Add new migration 029_add_dns_record_source.sql and wire it into the installer; update migrations detection.
- Add new views/partials for confirm/import/transfer modals, update various domain/group/tag templates, and update cron scripts and routes for discovery.

These changes preserve manual/imported DNS records, improve root-domain validation, enable background deep discovery, and add better logging/audit trails for transfers and imports.
2026-03-10 22:54:28 +02:00

778 lines
50 KiB
Twig

{% extends 'layout/base.twig' %}
{% set title = 'My Profile' %}
{% set pageTitle = 'My Profile' %}
{% set pageDescription = 'Manage your account settings and preferences' %}
{% set pageIcon = 'fas fa-user-circle' %}
{% block content %}
<!-- Main Profile Layout -->
<div class="grid grid-cols-12 gap-6">
<!-- Sidebar Navigation -->
<div class="col-span-12 lg:col-span-3">
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden sticky top-6">
<!-- User Info Section -->
<div class="p-6 border-b border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-900">
<div class="flex flex-col items-center text-center">
<div class="relative">
{% if avatar.type == 'uploaded' or avatar.type == 'gravatar' %}
<img src="{{ avatar.url }}"
alt="{{ avatar.alt }}"
class="w-20 h-20 rounded-full object-cover border-2 border-white dark:border-slate-700 shadow-sm"
loading="lazy">
{% else %}
<div class="w-20 h-20 rounded-full bg-primary flex items-center justify-center text-white text-2xl font-bold border-2 border-white dark:border-slate-700 shadow-sm">
{{ avatar.initials }}
</div>
{% endif %}
<!-- Avatar type indicator -->
<div class="absolute -bottom-1 -right-1 w-6 h-6 bg-white dark:bg-slate-800 rounded-full border-2 border-gray-200 dark:border-slate-600 flex items-center justify-center">
{% if avatar.type == 'uploaded' %}
<i class="fas fa-image text-xs text-blue-600 dark:text-blue-400" title="Uploaded avatar"></i>
{% elseif avatar.type == 'gravatar' %}
<i class="fas fa-globe text-xs text-green-600 dark:text-green-400" title="Gravatar"></i>
{% else %}
<i class="fas fa-user text-xs text-gray-500 dark:text-slate-400" title="Initials"></i>
{% endif %}
</div>
</div>
<h3 class="mt-4 text-base font-semibold text-gray-900 dark:text-white">{{ user.full_name|default(user.username) }}</h3>
<p class="text-sm text-gray-500 dark:text-slate-400 mt-1">@{{ user.username|default('') }}</p>
<!-- Role Badge -->
<div class="mt-3">{{ role_badge(user.role|default('user')) }}</div>
<!-- Stats -->
<div class="grid grid-cols-2 gap-3 mt-4 w-full">
<div class="bg-white dark:bg-slate-800 rounded-lg p-2 border border-gray-200 dark:border-slate-600">
<div class="text-xs text-gray-500 dark:text-slate-400">Member Since</div>
<div class="text-xs font-semibold text-gray-900 dark:text-white mt-0.5">
{{ (user.created_at|default('now'))|date('M Y') }}
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-lg p-2 border border-gray-200 dark:border-slate-600">
<div class="text-xs text-gray-500 dark:text-slate-400">Status</div>
<div class="text-xs font-semibold text-green-600 dark:text-green-400 mt-0.5">
<i class="fas fa-circle text-xs"></i> Active
</div>
</div>
</div>
</div>
</div>
<!-- Navigation Links -->
<nav class="p-3">
<button onclick="showSection('profile')" id="nav-profile" class="nav-item active w-full flex items-center px-4 py-2.5 text-sm font-medium rounded-lg transition-colors mb-1">
<i class="fas fa-user-circle w-5 mr-3 text-sm"></i>
<span>Profile Information</span>
</button>
<button onclick="showSection('security')" id="nav-security" class="nav-item w-full flex items-center px-4 py-2.5 text-sm font-medium rounded-lg transition-colors mb-1">
<i class="fas fa-shield-alt w-5 mr-3 text-sm"></i>
<span>Security</span>
</button>
<button onclick="showSection('twofactor')" id="nav-twofactor" class="nav-item w-full flex items-center px-4 py-2.5 text-sm font-medium rounded-lg transition-colors mb-1">
<i class="fas fa-key w-5 mr-3 text-sm"></i>
<span>Two-Factor Auth</span>
</button>
<button onclick="showSection('sessions')" id="nav-sessions" class="nav-item w-full flex items-center px-4 py-2.5 text-sm font-medium rounded-lg transition-colors mb-1">
<i class="fas fa-laptop w-5 mr-3 text-sm"></i>
<span>Active Sessions</span>
</button>
{% if user.role != 'admin' %}
<hr class="my-3 border-gray-200 dark:border-slate-700">
<button onclick="showSection('danger')" id="nav-danger" class="nav-item w-full flex items-center px-4 py-2.5 text-sm font-medium rounded-lg transition-colors text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10">
<i class="fas fa-exclamation-triangle w-5 mr-3 text-sm"></i>
<span>Danger Zone</span>
</button>
{% endif %}
</nav>
</div>
</div>
<!-- Main Content Area -->
<div class="col-span-12 lg:col-span-9">
<!-- Profile Information Section -->
<div id="section-profile" class="content-section">
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-900">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Profile Information</h3>
<p class="text-sm text-gray-600 dark:text-slate-400 mt-1">Update your personal details and account information</p>
</div>
<!-- Avatar Upload Section -->
<div class="px-6 py-4 border-b border-gray-200 dark:border-slate-700">
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-3">Profile Picture</h4>
<div class="flex items-center space-x-4">
<!-- Current Avatar Display -->
<div class="relative">
{% if avatar.type == 'uploaded' or avatar.type == 'gravatar' %}
<img src="{{ avatar.url }}"
alt="{{ avatar.alt }}"
class="w-16 h-16 rounded-full object-cover border-2 border-gray-200 dark:border-slate-600"
loading="lazy">
{% else %}
<div class="w-16 h-16 rounded-full bg-primary flex items-center justify-center text-white text-lg font-bold border-2 border-gray-200 dark:border-slate-600">
{{ avatar.initials }}
</div>
{% endif %}
<!-- Avatar type indicator -->
<div class="absolute -bottom-1 -right-1 w-5 h-5 bg-white dark:bg-slate-800 rounded-full border-2 border-gray-200 dark:border-slate-600 flex items-center justify-center">
{% if avatar.type == 'uploaded' %}
<i class="fas fa-image text-xs text-blue-600 dark:text-blue-400" title="Uploaded avatar"></i>
{% elseif avatar.type == 'gravatar' %}
<i class="fas fa-globe text-xs text-green-600 dark:text-green-400" title="Gravatar"></i>
{% else %}
<i class="fas fa-user text-xs text-gray-500 dark:text-slate-400" title="Initials"></i>
{% endif %}
</div>
</div>
<!-- Avatar Controls -->
<div class="flex-1">
<div class="space-y-2">
<!-- Upload Form -->
<form method="POST" action="/profile/upload-avatar" enctype="multipart/form-data" class="inline-block">
{{ csrf_field() }}
<div class="flex items-center space-x-2">
<input type="file"
id="avatar"
name="avatar"
accept="image/jpeg,image/png,image/gif,image/webp"
class="hidden"
onchange="this.form.submit()">
<label for="avatar"
class="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg text-sm font-medium text-gray-700 dark:text-slate-300 bg-white dark:bg-slate-700 hover:bg-gray-50 dark:hover:bg-slate-600 cursor-pointer">
<i class="fas fa-upload mr-2"></i>
Upload New
</label>
</div>
</form>
<!-- Delete Avatar Button -->
{% if avatar.type == 'uploaded' %}
<form method="POST" action="/profile/delete-avatar" class="inline-block ml-2">
{{ csrf_field() }}
<button type="submit"
class="inline-flex items-center px-3 py-2 border border-red-300 dark:border-red-500/30 rounded-lg text-sm font-medium text-red-700 dark:text-red-400 bg-white dark:bg-slate-700 hover:bg-red-50 dark:hover:bg-red-500/10"
onclick="return confirmClick(event, 'Are you sure you want to remove your avatar?', { title: 'Remove Avatar', icon: 'fa-user-circle text-red-500' })">
<i class="fas fa-trash mr-2"></i>
Remove
</button>
</form>
{% endif %}
</div>
<!-- Avatar Info -->
<div class="mt-2 text-xs text-gray-500 dark:text-slate-400">
{% if avatar.type == 'uploaded' %}
Using uploaded image
{% elseif avatar.type == 'gravatar' %}
Using Gravatar from {{ user.email|default('') }}
{% else %}
Using initials (upload an image or set up Gravatar)
{% endif %}
</div>
<!-- Gravatar Info -->
{% if avatar.type != 'gravatar' and user.email %}
<div class="mt-1 text-xs text-gray-400 dark:text-slate-500">
<a href="https://gravatar.com" target="_blank" class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300">
Set up Gravatar for automatic avatar
</a>
</div>
{% endif %}
</div>
</div>
</div>
<form method="POST" action="/profile/update" class="p-6">
{{ csrf_field() }}
<div class="space-y-5">
<!-- Full Name -->
<div>
<label for="full_name" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
Full Name
</label>
<input type="text" id="full_name" name="full_name"
value="{{ user.full_name|default('') }}"
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-slate-700 text-gray-900 dark:text-white">
</div>
<!-- Email -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
Email Address
</label>
<input type="email" id="email" name="email"
value="{{ user.email|default('') }}"
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-slate-700 text-gray-900 dark:text-white">
{% if user.email_verified %}
<p class="text-xs text-green-600 dark:text-green-400 mt-1.5">
<i class="fas fa-check-circle mr-1"></i>
Email verified
</p>
{% else %}
<div class="mt-3 bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 rounded-lg p-3">
<div class="flex items-start justify-between">
<div class="flex items-start">
<i class="fas fa-exclamation-triangle text-amber-600 dark:text-amber-400 mt-0.5 mr-2"></i>
<div>
<p class="text-xs font-semibold text-amber-900 dark:text-amber-400">Email Not Verified</p>
<p class="text-xs text-amber-700 dark:text-amber-300 mt-0.5">Verify your email to unlock all features</p>
</div>
</div>
<a href="/profile/resend-verification" class="ml-3 inline-flex items-center px-3 py-1.5 bg-amber-600 hover:bg-amber-700 text-white text-xs rounded-lg transition-colors font-medium whitespace-nowrap">
<i class="fas fa-paper-plane mr-1.5"></i>
Resend
</a>
</div>
</div>
{% endif %}
</div>
<!-- Username (Read-only) -->
<div>
<label for="username" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
Username
</label>
<input type="text" id="username" name="username"
value="{{ user.username|default('') }}"
readonly
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-gray-50 dark:bg-slate-700 text-gray-500 dark:text-slate-400 cursor-not-allowed">
<p class="text-xs text-gray-500 dark:text-slate-400 mt-1">Username cannot be changed</p>
</div>
<!-- Account Details Grid -->
<div class="pt-4 border-t border-gray-200 dark:border-slate-700">
<h4 class="text-sm font-semibold text-gray-700 dark:text-slate-300 mb-3">Account Information</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-gray-50 dark:bg-slate-700 rounded-lg p-3 border border-gray-200 dark:border-slate-600">
<label class="block text-xs font-medium text-gray-500 dark:text-slate-400 mb-1">Member Since</label>
<p class="text-sm font-semibold text-gray-900 dark:text-white">
{{ (user.created_at|default('now'))|date('F j, Y') }}
</p>
</div>
<div class="bg-gray-50 dark:bg-slate-700 rounded-lg p-3 border border-gray-200 dark:border-slate-600">
<label class="block text-xs font-medium text-gray-500 dark:text-slate-400 mb-1">Last Login</label>
<p class="text-sm font-semibold text-gray-900 dark:text-white">
{{ user.last_login ? user.last_login|date('M j, Y g:i A') : 'Never' }}
</p>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-end pt-6 mt-6 border-t border-gray-200 dark:border-slate-700 space-x-2">
<button type="button" onclick="location.reload()" class="px-4 py-2 border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-slate-300 text-sm rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors font-medium">
Cancel
</button>
<button type="submit" class="inline-flex items-center px-4 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-save mr-2"></i>
Save Changes
</button>
</div>
</form>
</div>
</div>
<!-- Two-Factor Authentication Section -->
<div id="section-twofactor" class="content-section hidden">
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-900">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Two-Factor Authentication</h3>
<p class="text-sm text-gray-600 dark:text-slate-400 mt-1">Add an extra layer of security to your account</p>
</div>
<div class="p-6">
{% if twoFactorPolicy == 'disabled' %}
<!-- 2FA Disabled by Admin -->
<div class="bg-gray-50 dark:bg-slate-700 border border-gray-200 dark:border-slate-600 rounded-lg p-4">
<div class="flex items-center">
<i class="fas fa-ban text-gray-400 dark:text-slate-500 text-xl mr-3"></i>
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white">Two-Factor Authentication Disabled</p>
<p class="text-sm text-gray-600 dark:text-slate-400 mt-1">2FA has been disabled by the administrator.</p>
</div>
</div>
</div>
{% elseif not user.email_verified %}
<!-- Email Not Verified -->
<div class="bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 rounded-lg p-4">
<div class="flex items-center">
<i class="fas fa-exclamation-triangle text-amber-600 dark:text-amber-400 text-xl mr-3"></i>
<div>
<p class="text-sm font-medium text-amber-900 dark:text-amber-400">Email Verification Required</p>
<p class="text-sm text-amber-700 dark:text-amber-300 mt-1">You must verify your email address before enabling 2FA.</p>
</div>
</div>
</div>
{% elseif twoFactorStatus.enabled %}
<!-- 2FA Enabled -->
<div class="space-y-4">
<div class="bg-green-50 dark:bg-green-500/10 border border-green-200 dark:border-green-500/20 rounded-lg p-4">
<div class="flex items-center">
<i class="fas fa-shield-alt text-green-600 dark:text-green-400 text-xl mr-3"></i>
<div>
<p class="text-sm font-medium text-green-900 dark:text-green-400">Two-Factor Authentication Enabled</p>
<p class="text-sm text-green-700 dark:text-green-300 mt-1">
Your account is protected with 2FA since
{{ twoFactorStatus.setup_at|date('M j, Y') }}.
</p>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-gray-50 dark:bg-slate-700 rounded-lg p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white">Backup Codes</p>
<p class="text-sm text-gray-600 dark:text-slate-400">{{ twoFactorStatus.backup_codes_count }} remaining</p>
</div>
<i class="fas fa-key text-gray-400 dark:text-slate-500"></i>
</div>
</div>
<div class="bg-gray-50 dark:bg-slate-700 rounded-lg p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white">Authenticator App</p>
<p class="text-sm text-gray-600 dark:text-slate-400">Active</p>
</div>
<i class="fas fa-mobile-alt text-gray-400 dark:text-slate-500"></i>
</div>
</div>
</div>
<div class="flex flex-col sm:flex-row gap-3">
{% if twoFactorStatus.backup_codes_count < 3 %}
<form method="POST" action="/2fa/regenerate-backup-codes" onsubmit="return confirmSubmit(event, 'Generate new backup codes? Your current codes will stop working.', { title: 'Regenerate Codes', icon: 'fa-key text-blue-500', confirmText: 'Generate', confirmClass: 'bg-blue-600 hover:bg-blue-700' })">
{{ csrf_field() }}
<button type="submit" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors font-medium">
<i class="fas fa-refresh mr-2"></i>
Generate New Backup Codes
</button>
</form>
{% endif %}
{% if twoFactorPolicy != 'forced' %}
<button type="button" onclick="showDisable2FAModal()" class="inline-flex items-center px-4 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors font-medium">
<i class="fas fa-ban mr-2"></i>
Disable 2FA
</button>
{% endif %}
</div>
</div>
{% elseif twoFactorStatus.required %}
<!-- 2FA Required -->
<div class="bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/20 rounded-lg p-4 mb-4">
<div class="flex items-center">
<i class="fas fa-exclamation-circle text-red-600 dark:text-red-400 text-xl mr-3"></i>
<div>
<p class="text-sm font-medium text-red-900 dark:text-red-400">Two-Factor Authentication Required</p>
<p class="text-sm text-red-700 dark:text-red-300 mt-1">You must enable 2FA to continue using your account.</p>
</div>
</div>
</div>
<div class="text-center">
<a href="/2fa/setup" class="inline-flex items-center px-6 py-3 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-shield-alt mr-2"></i>
Enable Two-Factor Authentication
</a>
</div>
{% else %}
<!-- 2FA Optional -->
<div class="space-y-4">
<div class="bg-blue-50 dark:bg-blue-500/10 border border-blue-200 dark:border-blue-500/20 rounded-lg p-4">
<div class="flex items-center">
<i class="fas fa-info-circle text-blue-600 dark:text-blue-400 text-xl mr-3"></i>
<div>
<p class="text-sm font-medium text-blue-900 dark:text-blue-400">Enhanced Security Available</p>
<p class="text-sm text-blue-700 dark:text-blue-300 mt-1">
Enable two-factor authentication to add an extra layer of security to your account.
</p>
</div>
</div>
</div>
<div class="text-center">
<a href="/2fa/setup" class="inline-flex items-center px-6 py-3 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-shield-alt mr-2"></i>
Enable Two-Factor Authentication
</a>
</div>
<div class="bg-gray-50 dark:bg-slate-700 border border-gray-200 dark:border-slate-600 rounded-lg p-4">
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-2">How 2FA Works</h4>
<ul class="text-sm text-gray-700 dark:text-slate-300 space-y-1">
<li>• Generate time-based codes using an authenticator app</li>
<li>• Use backup codes if you lose access to your device</li>
<li>• Receive email codes as an alternative method</li>
<li>• Enhanced protection against unauthorized access</li>
</ul>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Security Section -->
<div id="section-security" class="content-section hidden">
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-900">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Security Settings</h3>
<p class="text-sm text-gray-600 dark:text-slate-400 mt-1">Manage your password and security preferences</p>
</div>
<form method="POST" action="/profile/change-password" class="p-6">
{{ csrf_field() }}
<div class="space-y-4">
<!-- Current Password -->
<div>
<label for="current_password" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
Current Password
</label>
<input type="password" id="current_password" name="current_password" required
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
placeholder="Enter your current password">
</div>
<!-- New Password -->
<div>
<label for="new_password" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
New Password
</label>
<input type="password" id="new_password" name="new_password" required minlength="8"
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
placeholder="Enter a strong password">
<p class="text-xs text-gray-500 dark:text-slate-400 mt-1">Minimum 8 characters</p>
</div>
<!-- Confirm New Password -->
<div>
<label for="new_password_confirm" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
Confirm New Password
</label>
<input type="password" id="new_password_confirm" name="new_password_confirm" required minlength="8"
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
placeholder="Re-enter your new password">
</div>
<!-- Password Tips -->
<div class="bg-blue-50 dark:bg-blue-500/10 border border-blue-200 dark:border-blue-500/20 rounded-lg p-3">
<p class="text-xs text-gray-600 dark:text-slate-400">
<i class="fas fa-info-circle text-blue-500 dark:text-blue-400 mr-1"></i>
Use at least 8 characters with a mix of letters, numbers, and symbols for better security.
</p>
</div>
</div>
<div class="flex items-center justify-end pt-6 mt-6 border-t border-gray-200 dark:border-slate-700">
<button type="submit" class="inline-flex items-center px-4 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-key mr-2"></i>
Update Password
</button>
</div>
</form>
</div>
</div>
<!-- Active Sessions Section -->
<div id="section-sessions" class="content-section hidden">
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-900">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Active Sessions</h3>
<p class="text-sm text-gray-600 dark:text-slate-400 mt-1">Manage devices and sessions where you're logged in ({{ sessions|default([])|length }} active)</p>
</div>
{% if sessions|default([])|length > 1 %}
<form method="POST" action="/profile/logout-other-sessions" onsubmit="return confirmSubmit(event, 'Logout all other sessions?', { title: 'Logout Sessions', icon: 'fa-sign-out-alt text-red-500' })" class="inline">
{{ csrf_field() }}
<button type="submit" class="inline-flex items-center px-3 py-2 bg-red-600 text-white text-xs rounded-lg hover:bg-red-700 transition-colors font-medium">
<i class="fas fa-sign-out-alt mr-1.5"></i>
Logout Others
</button>
</form>
{% endif %}
</div>
</div>
<div class="p-6">
{% if sessions is not empty %}
<div class="space-y-3">
{% for session in sessions %}
{% set isCurrent = session.is_current|default(false) %}
{% set deviceColor = isCurrent ? 'green' : 'gray' %}
{% set bgClass = isCurrent ? 'bg-green-50 dark:bg-green-500/10 border-green-200 dark:border-green-500/20' : 'bg-gray-50 dark:bg-slate-700 border-gray-200 dark:border-slate-600' %}
<div class="flex items-start justify-between p-4 {{ bgClass }} border rounded-lg">
<div class="flex items-start space-x-3 flex-1">
<!-- Device Icon -->
<div class="w-10 h-10 bg-{{ deviceColor }}-100 dark:bg-{{ deviceColor }}-500/20 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas {{ session.deviceIcon }} text-{{ deviceColor }}-600 dark:text-{{ deviceColor }}-400"></i>
</div>
<div class="flex-1 min-w-0">
<!-- Header -->
<div class="flex items-center flex-wrap gap-2">
{% if session.country_code and session.country_code != 'xx' %}
<span class="fi fi-{{ session.country_code|lower }} text-base"></span>
{% endif %}
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
{{ session.city|default('Unknown') }}, {{ session.country|default('Unknown') }}
</h4>
{% if isCurrent %}
<span class="px-2 py-0.5 bg-green-500 text-white text-xs font-semibold rounded">
Current
</span>
{% endif %}
{% if session.has_remember_token %}
<span class="px-2 py-0.5 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 text-xs font-semibold rounded" title="Remember me enabled">
<i class="fas fa-cookie-bite"></i>
</span>
{% endif %}
</div>
<!-- Browser & OS -->
<p class="text-xs text-gray-600 dark:text-slate-400 mt-1">
<i class="fas fa-globe mr-1"></i>
{{ session.browserInfo }}
{% if session.user_agent %}
- {{ session.user_agent|slice(0, 60) }}{{ session.user_agent|length > 60 ? '...' : '' }}
{% endif %}
</p>
<!-- IP & ISP -->
<div class="flex flex-wrap items-center gap-3 text-xs text-gray-500 dark:text-slate-400 mt-1">
<span>
<i class="fas fa-map-marker-alt mr-1"></i>
{{ session.ip_address }}
</span>
{% if session.isp %}
<span>
<i class="fas fa-network-wired mr-1"></i>
{{ session.isp }}
</span>
{% endif %}
</div>
<!-- Session Age & Last Activity -->
<div class="flex flex-wrap items-center gap-3 text-xs text-gray-400 dark:text-slate-500 mt-1">
<span title="Session started: {{ session.created_at|date('M j, Y H:i') }}">
<i class="fas fa-hourglass-start mr-1"></i>
{{ session.sessionAge }}
</span>
<span>
<i class="fas fa-clock mr-1"></i>
Active {{ session.timeAgo }}
</span>
</div>
</div>
</div>
<!-- Delete Button (only for non-current sessions) -->
{% if not isCurrent %}
<form method="POST" action="/profile/logout-session/{{ session.id }}" onsubmit="return confirmSubmit(event, 'Terminate this session? That device will be logged out immediately.', { title: 'Terminate Session', icon: 'fa-sign-out-alt text-red-500' })" class="ml-3">
{{ csrf_field() }}
<button type="submit" class="flex items-center justify-center w-8 h-8 bg-red-100 dark:bg-red-500/20 text-red-600 dark:text-red-400 rounded-lg hover:bg-red-600 hover:text-white dark:hover:bg-red-600 transition-colors" title="Terminate session">
<i class="fas fa-times text-sm"></i>
</button>
</form>
{% endif %}
</div>
{% endfor %}
</div>
<!-- Info Box -->
<div class="bg-blue-50 dark:bg-blue-500/10 border border-blue-200 dark:border-blue-500/20 rounded-lg p-3 mt-4">
<p class="text-xs text-gray-600 dark:text-slate-400">
<i class="fas fa-info-circle text-blue-500 dark:text-blue-400 mr-1"></i>
If you see any suspicious sessions or don't recognize a device, logout other sessions immediately and change your password.
</p>
</div>
{% else %}
<div class="text-center py-8">
<i class="fas fa-laptop text-gray-300 dark:text-slate-600 text-4xl mb-3"></i>
<p class="text-sm text-gray-600 dark:text-slate-400">No active sessions found</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Danger Zone Section -->
{% if user.role != 'admin' %}
<div id="section-danger" class="content-section hidden">
<div class="bg-white dark:bg-slate-800 rounded-lg border border-red-200 dark:border-red-500/20 overflow-hidden">
<div class="px-6 py-4 border-b border-red-200 dark:border-red-500/20 bg-red-50 dark:bg-red-500/10">
<h3 class="text-lg font-semibold text-red-900 dark:text-red-400">Danger Zone</h3>
<p class="text-sm text-red-700 dark:text-red-300 mt-1">Irreversible and destructive actions</p>
</div>
<div class="p-6">
<div class="bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/20 rounded-lg p-4">
<div class="flex items-start justify-between">
<div class="flex-1">
<h4 class="text-sm font-bold text-red-900 dark:text-red-400">Delete Account Permanently</h4>
<p class="text-sm text-red-700 dark:text-red-300 mt-2">
Once you delete your account, there is no going back. This will permanently delete all your profile information and account settings.
</p>
<p class="text-xs text-red-800 dark:text-red-400 font-semibold mt-3 bg-red-100 dark:bg-red-500/20 inline-block px-2 py-1 rounded">
This action cannot be undone
</p>
</div>
<form id="deleteAccountForm" method="POST" action="/profile/delete" class="inline">
{{ csrf_field() }}
<button type="button" onclick="confirmDelete()" class="ml-4 inline-flex items-center px-4 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors font-medium whitespace-nowrap">
<i class="fas fa-trash-alt mr-2"></i>
Delete Account
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<style>
.nav-item {
color: #6b7280;
text-align: left;
}
.dark .nav-item {
color: #94a3b8;
}
.nav-item:hover {
background-color: #f3f4f6;
color: #1f2937;
}
.dark .nav-item:hover {
background-color: #334155;
color: #f1f5f9;
}
.nav-item.active {
background-color: #EFF6FF;
color: #4A90E2;
font-weight: 600;
}
.dark .nav-item.active {
background-color: rgba(74, 144, 226, 0.15);
color: #60a5fa;
}
.content-section {
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
<script>
function showSection(section) {
document.querySelectorAll('.content-section').forEach(el => {
el.classList.add('hidden');
});
document.querySelectorAll('.nav-item').forEach(el => {
el.classList.remove('active');
});
document.getElementById('section-' + section).classList.remove('hidden');
document.getElementById('nav-' + section).classList.add('active');
window.location.hash = section;
window.scrollTo({ top: 0, behavior: 'smooth' });
}
document.addEventListener('DOMContentLoaded', function() {
const hash = window.location.hash.substring(1);
const validSections = ['profile', 'security', 'twofactor', 'sessions'{% if user.role != 'admin' %}, 'danger'{% endif %}];
if (hash && validSections.includes(hash)) {
showSection(hash);
} else {
showSection('profile');
}
});
async function confirmDelete() {
var ok = await confirmAction({ message: 'Are you absolutely sure you want to delete your account? This action is PERMANENT and cannot be undone!', title: 'Delete Account', icon: 'fa-skull-crossbones text-red-600' });
if (!ok) return;
var ok2 = await confirmAction({ message: 'FINAL WARNING: This will permanently delete all your data. Click Confirm to proceed.', title: 'Final Confirmation', icon: 'fa-exclamation-circle text-red-600' });
if (ok2) document.getElementById('deleteAccountForm').submit();
}
function showDisable2FAModal() {
document.getElementById('disable2FAModal').classList.remove('hidden');
document.getElementById('disable2FACode').focus();
}
function hideDisable2FAModal() {
document.getElementById('disable2FAModal').classList.add('hidden');
document.getElementById('disable2FAForm').reset();
}
</script>
<!-- Disable 2FA Modal -->
<div id="disable2FAModal" class="hidden fixed inset-0 bg-gray-600/50 dark:bg-black/60 overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border border-gray-200 dark:border-slate-700 w-96 shadow-lg rounded-md bg-white dark:bg-slate-800">
<div class="mt-3">
<div class="flex items-center justify-center w-12 h-12 mx-auto bg-red-100 dark:bg-red-500/20 rounded-full mb-4">
<i class="fas fa-ban text-red-600 dark:text-red-400 text-xl"></i>
</div>
<h3 class="text-lg font-medium text-gray-900 dark:text-white text-center mb-2">Disable Two-Factor Authentication</h3>
<p class="text-sm text-gray-500 dark:text-slate-400 text-center mb-6">
This will make your account less secure. Enter your 2FA code to confirm.
</p>
<form id="disable2FAForm" method="POST" action="/2fa/disable" class="space-y-4">
{{ csrf_field() }}
<div>
<label for="disable2FACode" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
Verification Code
</label>
<input type="text"
id="disable2FACode"
name="verification_code"
maxlength="8"
placeholder="Enter 2FA code"
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-red-500 transition-colors text-sm bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
required>
<p class="text-xs text-gray-500 dark:text-slate-400 mt-1">Enter your authenticator code, email code, or backup code</p>
</div>
<div class="flex space-x-3 pt-4">
<button type="submit"
class="flex-1 bg-red-600 hover:bg-red-700 text-white py-2.5 rounded-lg font-medium transition-colors text-sm">
Disable 2FA
</button>
<button type="button"
onclick="hideDisable2FAModal()"
class="flex-1 bg-gray-300 dark:bg-slate-600 hover:bg-gray-400 dark:hover:bg-slate-500 text-gray-700 dark:text-slate-300 py-2.5 rounded-lg font-medium transition-colors text-sm">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}