Migrate many view templates from raw PHP to Twig and modernize UI/UX for 2FA and settings. Controllers updated to provide avatar data and two-factor info (ProfileController, UserController) and SettingsController now includes timezone lists, notification preset selection, cron path, cached update state and rollback availability. ErrorHandler now attempts to render error pages via a new Core\TwigService with a safe fallback to raw PHP views. TwoFactorService generation silences deprecated warnings during QR code creation. Numerous .php view files were removed and replaced with .twig equivalents (2fa setup/verify/backup-codes and many auth, dashboard, domains, errors, layout, users, tags, tld-registry, etc.), and core/TwigService was added. These changes move the app toward a Twig-based templating system, improve 2FA flows, surface avatar images in lists/profiles, and make error rendering more robust.
779 lines
49 KiB
Twig
779 lines
49 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 confirm('Are you sure you want to remove your avatar?')">
|
|
<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 confirm('Generate new backup codes? Your current codes will stop working.')">
|
|
{{ 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 confirm('Logout all other sessions?')" 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 confirm('Terminate this session?\n\nThat device will be logged out immediately.')" 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');
|
|
}
|
|
});
|
|
|
|
function confirmDelete() {
|
|
if (confirm('Are you absolutely sure you want to delete your account?\n\nThis action is PERMANENT and cannot be undone!')) {
|
|
if (confirm('FINAL WARNING: This will permanently delete all your data.\n\nClick OK to proceed.')) {
|
|
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 %}
|