diff --git a/app/Controllers/ProfileController.php b/app/Controllers/ProfileController.php index e0750d1..6ba8f58 100644 --- a/app/Controllers/ProfileController.php +++ b/app/Controllers/ProfileController.php @@ -8,6 +8,7 @@ use App\Models\User; use App\Models\SessionManager; use App\Models\RememberToken; use App\Services\Logger; +use App\Services\TwoFactorService; use App\Helpers\AvatarHelper; class ProfileController extends Controller @@ -71,10 +72,30 @@ class ProfileController extends Controller // Format sessions for display (adds deviceIcon, browserInfo, timeAgo, sessionAge) $formattedSessions = \App\Helpers\SessionHelper::formatForDisplay($sessions); + // Avatar data + $avatar = AvatarHelper::getAvatar($user, 80); + + // 2FA status + $twoFactorService = new TwoFactorService(); + $twoFactorPolicy = $twoFactorService->getTwoFactorPolicy(); + + $backupCodes = !empty($user['two_factor_backup_codes']) + ? json_decode($user['two_factor_backup_codes'], true) + : []; + + $twoFactorStatus = [ + 'enabled' => !empty($user['two_factor_enabled']), + 'setup_at' => $user['two_factor_setup_at'] ?? null, + 'backup_codes_count' => is_array($backupCodes) ? count($backupCodes) : 0, + 'required' => $twoFactorService->isTwoFactorRequired($userId), + ]; + $this->view('profile/index', [ 'user' => $user, 'sessions' => $formattedSessions, - 'userModel' => $this->userModel, + 'avatar' => $avatar, + 'twoFactorStatus' => $twoFactorStatus, + 'twoFactorPolicy' => $twoFactorPolicy, 'title' => 'My Profile' ]); } diff --git a/app/Controllers/SettingsController.php b/app/Controllers/SettingsController.php index b6d5351..23be609 100644 --- a/app/Controllers/SettingsController.php +++ b/app/Controllers/SettingsController.php @@ -70,6 +70,72 @@ class SettingsController extends Controller // Status notification triggers $statusTriggers = $this->settingModel->getNotificationStatusTriggers(); + // Timezone lists for the Application tab + $popularTimezones = [ + 'UTC' => 'UTC', + 'America/New_York' => 'Eastern Time (US)', + 'America/Chicago' => 'Central Time (US)', + 'America/Denver' => 'Mountain Time (US)', + 'America/Los_Angeles' => 'Pacific Time (US)', + 'Europe/London' => 'London', + 'Europe/Paris' => 'Paris', + 'Asia/Tokyo' => 'Tokyo', + 'Australia/Sydney' => 'Sydney' + ]; + $allTimezones = timezone_identifiers_list(); + + // Determine which notification preset is selected + $currentNotificationDays = $settings['notification_days_before'] ?? '30,15,7,3,1'; + $selectedPreset = 'custom'; + foreach ($notificationPresets as $key => $preset) { + if ($preset['value'] === $currentNotificationDays) { + $selectedPreset = $key; + break; + } + } + + // Cron path for System tab + $cronPath = realpath(defined('PATH_ROOT') ? PATH_ROOT . 'cron/check_domains.php' : __DIR__ . '/../../cron/check_domains.php') ?: 'cron/check_domains.php'; + + // Cached update state for Updates tab + $cachedUpdateAvailable = false; + $cachedUpdateData = null; + $currentVer = $appSettings['app_version'] ?? '0'; + $latestVer = $updateSettings['latest_available_version'] ?? null; + $updateChannel = $updateSettings['update_channel'] ?? 'stable'; + $commitsBehind = (int)($updateSettings['commits_behind_count'] ?? 0); + $installedSha = $updateSettings['installed_commit_sha'] ?? ''; + $remoteSha = $updateSettings['latest_remote_sha'] ?? ''; + if ($installedSha !== '' && $remoteSha !== '' && str_starts_with($installedSha, $remoteSha)) { + $commitsBehind = 0; + } + if ($latestVer && version_compare($latestVer, $currentVer, '>')) { + $cachedUpdateAvailable = true; + $cachedUpdateData = [ + 'available' => true, + 'type' => 'release', + 'current_version' => $currentVer, + 'latest_version' => $latestVer, + 'release_notes' => $updateSettings['latest_release_notes'] ?? '', + 'release_url' => $updateSettings['latest_release_url'] ?? '', + 'published_at' => $updateSettings['latest_release_published_at'] ?? null, + 'channel' => $updateChannel, + ]; + } elseif ($updateChannel === 'latest' && $commitsBehind > 0) { + $cachedUpdateAvailable = true; + $cachedUpdateData = [ + 'available' => true, + 'type' => 'hotfix', + 'current_version' => $currentVer, + 'commits_behind' => $commitsBehind, + 'commit_messages' => [], + 'channel' => $updateChannel, + ]; + } + + // Rollback availability + $rollbackAvailable = !empty($updateSettings['update_backup_path']) && file_exists($updateSettings['update_backup_path']); + $this->view('settings/index', [ 'settings' => $settings, 'appSettings' => $appSettings, @@ -81,6 +147,13 @@ class SettingsController extends Controller 'notificationPresets' => $notificationPresets, 'checkIntervalPresets' => $checkIntervalPresets, 'statusTriggers' => $statusTriggers, + 'popularTimezones' => $popularTimezones, + 'allTimezones' => $allTimezones, + 'selectedPreset' => $selectedPreset, + 'cronPath' => $cronPath, + 'cachedUpdateAvailable' => $cachedUpdateAvailable, + 'cachedUpdateData' => $cachedUpdateData, + 'rollbackAvailable' => $rollbackAvailable, 'title' => 'Settings' ]); } diff --git a/app/Controllers/UserController.php b/app/Controllers/UserController.php index a3a3f31..3be9227 100644 --- a/app/Controllers/UserController.php +++ b/app/Controllers/UserController.php @@ -49,6 +49,11 @@ class UserController extends Controller // Get filtered users $users = $this->userModel->getFiltered($filters, $sort, strtoupper($order), $perPage, $offset); + + foreach ($users as &$u) { + $u['avatar'] = \App\Helpers\AvatarHelper::getAvatar($u, 40); + } + unset($u); $this->view('users/index', [ 'users' => $users, @@ -240,6 +245,17 @@ class UserController extends Controller // Get 2FA status $twoFactorStatus = $this->userModel->getTwoFactorStatus($userId); + // Avatar for profile header + $userAvatar = \App\Helpers\AvatarHelper::getAvatar($user, 64); + + // Registrar distribution + $registrarCounts = []; + foreach ($domains as $d) { + $reg = !empty($d['registrar']) ? $d['registrar'] : 'Unknown'; + $registrarCounts[$reg] = ($registrarCounts[$reg] ?? 0) + 1; + } + arsort($registrarCounts); + $this->view('users/show', [ 'title' => htmlspecialchars($user['full_name']) . ' - User Profile', 'user' => $user, @@ -248,6 +264,8 @@ class UserController extends Controller 'tags' => $tags, 'groups' => $groups, 'twoFactorStatus' => $twoFactorStatus, + 'userAvatar' => $userAvatar, + 'registrarCounts' => $registrarCounts, ]); } diff --git a/app/Services/ErrorHandler.php b/app/Services/ErrorHandler.php index a204812..2b57662 100644 --- a/app/Services/ErrorHandler.php +++ b/app/Services/ErrorHandler.php @@ -297,7 +297,29 @@ class ErrorHandler $session_data = json_decode($errorData['session_data'], true); // Display debug page in development, clean 500 in production - if ($this->isDevelopment) { + $twigTemplate = $this->isDevelopment ? 'errors/debug.twig' : 'errors/500.twig'; + $twigFile = __DIR__ . '/../Views/' . $twigTemplate; + + if (file_exists($twigFile)) { + try { + $memory_usage_mb = round(($memory_usage ?? 0) / 1024 / 1024, 2); + $peak_memory_mb = round(memory_get_peak_usage(true) / 1024 / 1024, 2); + $errorContext = compact( + 'error_id', 'error_type', 'error_message', 'error_file', 'error_line', + 'stack_trace', 'request_method', 'request_uri', 'user_agent', + 'ip_address', 'php_version', 'memory_usage', 'memory_usage_mb', + 'peak_memory_mb', 'occurred_at', 'user_info', 'request_data', 'session_data' + ); + echo \Core\TwigService::getInstance()->render($twigTemplate, $errorContext); + } catch (\Throwable $e) { + // Twig itself failed — fall back to raw PHP view + if ($this->isDevelopment) { + require __DIR__ . '/../Views/errors/debug.php'; + } else { + require __DIR__ . '/../Views/errors/500.php'; + } + } + } elseif ($this->isDevelopment) { require __DIR__ . '/../Views/errors/debug.php'; } else { require __DIR__ . '/../Views/errors/500.php'; diff --git a/app/Services/TwoFactorService.php b/app/Services/TwoFactorService.php index 2254934..2fca54d 100644 --- a/app/Services/TwoFactorService.php +++ b/app/Services/TwoFactorService.php @@ -38,14 +38,20 @@ class TwoFactorService */ public function generateQrCodeDataUri(string $email, string $secret, string $appName = 'Domain Monitor'): string { - $qrCode = new QrCode($this->google2fa->getQRCodeUrl($appName, $email, $secret)); - $qrCode->setSize(200); - $qrCode->setMargin(10); - - $writer = new PngWriter(); - $result = $writer->write($qrCode); - - return 'data:image/png;base64,' . base64_encode($result->getString()); + $previousLevel = error_reporting(error_reporting() & ~E_DEPRECATED); + + try { + $qrCode = new QrCode($this->google2fa->getQRCodeUrl($appName, $email, $secret)); + $qrCode->setSize(200); + $qrCode->setMargin(10); + + $writer = new PngWriter(); + $result = $writer->write($qrCode); + + return 'data:image/png;base64,' . base64_encode($result->getString()); + } finally { + error_reporting($previousLevel); + } } /** diff --git a/app/Views/2fa/backup-codes.php b/app/Views/2fa/backup-codes.php deleted file mode 100644 index 4075457..0000000 --- a/app/Views/2fa/backup-codes.php +++ /dev/null @@ -1,209 +0,0 @@ - - -
Save these codes in a safe place - they can be used to access your account if you lose your authenticator device
-- These backup codes are shown only once. Each code can only be used once. Store them securely and never share them with anyone. -
-= htmlspecialchars($code) ?>
-
- Save these codes in a safe place - they can be used to access your account if you lose your authenticator device
++ These backup codes are shown only once. Each code can only be used once. Store them securely and never share them with anyone. +
+{{ code }}
+
+ Download one of these apps on your mobile device:
- -Google Authenticator
-iOS & Android
-Authy
-iOS & Android
-Microsoft Authenticator
-iOS & Android
-Open your authenticator app and scan this QR code:
- -- Note: This QR code will remain the same even if you refresh the page. - Once you scan it, you can enter the verification code below. -
-Can't scan? Enter this code manually:
-= htmlspecialchars($secret) ?>
- Enter the 6-digit code from your authenticator app:
- - -Important Security Notice
-- Once 2FA is enabled, you'll need your authenticator app to log in. - Make sure to save your backup codes in a secure location. -
-Download one of these apps on your mobile device:
+ +Google Authenticator
+iOS & Android
+Authy
+iOS & Android
+Microsoft Authenticator
+iOS & Android
+Open your authenticator app and scan this QR code:
+ +Can't scan? Enter this code manually:
+{{ secret }}
+ Enter the 6-digit code from your authenticator app:
+ + +Important Security Notice
++ Once 2FA is enabled, you'll need your authenticator app to log in. + Make sure to save your backup codes in a secure location. +
+
- Hello, = htmlspecialchars($user['full_name'] ?? $user['username']) ?>!
- Please enter your 2FA code to complete login.
-
+ Hello, {{ user.full_name|default(user.username) }}!
+ Please enter your 2FA code to complete login.
+
- © = date('Y') ?> Domain Monitor. All rights reserved. -
-+ © {{ "now"|date("Y") }} {{ appSettings.app_name|default('Domain Monitor') }}. All rights reserved. +
+No worries, we'll send you reset instructions
-No worries, we'll send you reset instructions
+Sign in to access your account
+Sign in to access your account
+{% if registrationEnabled|default(false) %} +{# Sign Up Link #} +
Don't have an account? Create Account
Join Domain Monitor today
+Join Domain Monitor today
+{# Sign In Link #} +
Already have an account? Sign In
Enter your new password below
+Enter your new password below