Add domain status notifications & login alerts

Introduce richer notifications and domain status handling across the app.

- NotificationService: Add domain status alert formatting/sending, in-app notifications for available/registered/redemption/pending_delete, richer session_new and session_failed notifications (geolocation + UA parsing) and helpers for human-readable status labels.
- Auth/TwoFactor: Emit notifications for successful logins (including remember-me and 2FA) and failed login attempts; update last-login timestamp on various flows.
- DomainController: Wrap bulk domain create in try/catch to handle duplicate race conditions and log failures.
- WhoisService: Detect redemption_period and pending_delete statuses from WHOIS/EPP statuses.
- Settings/Setting: Add settings support for notification status triggers and bump default app_version to 1.1.2; persist/update status trigger values.
- Views/Layout/View helpers: Add parsing/formatting for login notification data, add new status labels/classes (available, redemption_period, pending_delete), update notification icons/colors mapping.
- Top-nav & Notifications UI: Enhance dropdown with rich login/failed-login display (flags, device icons), clickable domain redirects when marking read, badge IDs for dynamic updates.
- Error admin UI: Add copy error report button with robust clipboard fallback and toast UI reused from messages; improved copy UX in admin index/detail.
- Installer: Add new migration 024 to installer migration lists and adjust detected toVersion to 1.1.2.
- DB: Add migration file 024_add_status_notifications_v1.1.2.sql (new file).

These changes add user-facing alerts for domain lifecycle events and stronger login/security notifications while improving UI feedback and robustness during bulk operations.
This commit is contained in:
Hosteroid
2026-02-08 22:58:59 +02:00
parent f32de0a848
commit e334f7c9d6
24 changed files with 1597 additions and 200 deletions

View File

@@ -56,11 +56,16 @@ $offset = $pagination['showing_from'] - 1;
<optgroup label="Domain">
<option value="domain_expiring" <?= $filterType === 'domain_expiring' ? 'selected' : '' ?>>Domain Expiring</option>
<option value="domain_expired" <?= $filterType === 'domain_expired' ? 'selected' : '' ?>>Domain Expired</option>
<option value="domain_available" <?= $filterType === 'domain_available' ? 'selected' : '' ?>>Domain Available</option>
<option value="domain_registered" <?= $filterType === 'domain_registered' ? 'selected' : '' ?>>Domain Registered</option>
<option value="domain_redemption" <?= $filterType === 'domain_redemption' ? 'selected' : '' ?>>Redemption Period</option>
<option value="domain_pending_delete" <?= $filterType === 'domain_pending_delete' ? 'selected' : '' ?>>Pending Delete</option>
<option value="domain_updated" <?= $filterType === 'domain_updated' ? 'selected' : '' ?>>Domain Updated</option>
<option value="whois_failed" <?= $filterType === 'whois_failed' ? 'selected' : '' ?>>WHOIS Failed</option>
</optgroup>
<optgroup label="System">
<option value="session_new" <?= $filterType === 'session_new' ? 'selected' : '' ?>>New Login</option>
<option value="session_failed" <?= $filterType === 'session_failed' ? 'selected' : '' ?>>Failed Login</option>
<option value="system_welcome" <?= $filterType === 'system_welcome' ? 'selected' : '' ?>>Welcome</option>
<option value="system_upgrade" <?= $filterType === 'system_upgrade' ? 'selected' : '' ?>>System Upgrade</option>
</optgroup>
@@ -129,34 +134,178 @@ $offset = $pagination['showing_from'] - 1;
$bgClass = $notification['is_read'] ? '' : 'bg-blue-50';
$iconBgClass = "bg-{$notification['color']}-100";
$iconTextClass = "text-{$notification['color']}-600";
$hasDomain = !empty($notification['domain_id']);
$domainUrl = $hasDomain ? '/domains/' . $notification['domain_id'] : null;
$clickUrl = null;
if ($hasDomain && !$notification['is_read']) {
$clickUrl = '/notifications/' . $notification['id'] . '/mark-read?redirect=domain&domain_id=' . $notification['domain_id'];
} elseif ($hasDomain) {
$clickUrl = $domainUrl;
}
$loginData = $notification['login_data'] ?? null;
$isLogin = ($notification['type'] === 'session_new' && $loginData);
$isFailedLogin = ($notification['type'] === 'session_failed' && $loginData);
?>
<div class="px-4 py-3 hover:bg-gray-50 transition-colors <?= $bgClass ?>">
<div class="flex items-center gap-3">
<div class="flex items-start gap-3">
<!-- Icon -->
<div class="w-8 h-8 <?= $iconBgClass ?> rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-<?= $notification['icon'] ?> <?= $iconTextClass ?> text-xs"></i>
</div>
<?php if ($isFailedLogin): ?>
<div class="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-shield-alt text-red-600"></i>
</div>
<?php elseif ($isLogin): ?>
<div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-sign-in-alt text-blue-600"></i>
</div>
<?php elseif ($clickUrl): ?>
<a href="<?= $clickUrl ?>" class="w-10 h-10 <?= $iconBgClass ?> rounded-lg flex items-center justify-center flex-shrink-0 hover:opacity-80 transition-opacity">
<i class="fas fa-<?= $notification['icon'] ?> <?= $iconTextClass ?> text-sm"></i>
</a>
<?php else: ?>
<div class="w-10 h-10 <?= $iconBgClass ?> rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-<?= $notification['icon'] ?> <?= $iconTextClass ?> text-sm"></i>
</div>
<?php endif; ?>
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<h3 class="text-sm font-medium text-gray-900"><?= htmlspecialchars($notification['title']) ?></h3>
<?php if ($clickUrl): ?>
<a href="<?= $clickUrl ?>" class="text-sm font-medium text-gray-900 hover:text-primary transition-colors"><?= htmlspecialchars($notification['title']) ?></a>
<?php else: ?>
<h3 class="text-sm font-medium text-gray-900"><?= htmlspecialchars($notification['title']) ?></h3>
<?php endif; ?>
<?php if (!$notification['is_read']): ?>
<span class="flex h-1.5 w-1.5">
<span class="flex h-1.5 w-1.5 relative">
<span class="animate-ping absolute inline-flex h-1.5 w-1.5 rounded-full bg-blue-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-blue-500"></span>
</span>
<?php endif; ?>
<span class="text-xs text-gray-400 ml-auto">
<span class="text-xs text-gray-400 ml-auto flex-shrink-0">
<i class="fas fa-clock mr-1"></i>
<?= $notification['time_ago'] ?>
</span>
</div>
<p class="text-xs text-gray-600 mt-0.5"><?= htmlspecialchars($notification['message']) ?></p>
<?php if ($isFailedLogin): ?>
<!-- Rich failed login details (mirrors successful login layout) -->
<div class="mt-1.5 bg-red-50 rounded-lg p-3 border border-red-200">
<div class="grid grid-cols-2 md:grid-cols-3 gap-x-4 gap-y-2 text-xs">
<!-- Location -->
<div class="flex items-center gap-1.5">
<i class="fas fa-map-marker-alt text-red-400 w-3.5 text-center"></i>
<span class="text-gray-500">Location:</span>
<span class="text-gray-800 font-medium">
<?php if (($loginData['country_code'] ?? 'xx') !== 'xx'): ?>
<span class="fi fi-<?= strtolower($loginData['country_code']) ?> text-xs mr-0.5 rounded-sm"></span>
<?php endif; ?>
<?= htmlspecialchars($loginData['location'] ?? 'Unknown') ?>
</span>
</div>
<!-- IP Address -->
<div class="flex items-center gap-1.5">
<i class="fas fa-network-wired text-blue-400 w-3.5 text-center"></i>
<span class="text-gray-500">IP:</span>
<span class="text-gray-800 font-medium font-mono text-[11px]"><?= htmlspecialchars($loginData['ip'] ?? 'unknown') ?></span>
</div>
<!-- Browser -->
<div class="flex items-center gap-1.5">
<i class="fas fa-globe text-green-400 w-3.5 text-center"></i>
<span class="text-gray-500">Browser:</span>
<span class="text-gray-800 font-medium"><?= htmlspecialchars($loginData['browser'] ?? 'Unknown') ?></span>
</div>
<!-- Device -->
<div class="flex items-center gap-1.5">
<i class="fas fa-<?= htmlspecialchars($loginData['device_icon'] ?? 'desktop') ?> text-purple-400 w-3.5 text-center"></i>
<span class="text-gray-500">Device:</span>
<span class="text-gray-800 font-medium"><?= htmlspecialchars($loginData['device'] ?? 'Unknown') ?></span>
</div>
<!-- OS -->
<div class="flex items-center gap-1.5">
<i class="fas fa-laptop-code text-indigo-400 w-3.5 text-center"></i>
<span class="text-gray-500">OS:</span>
<span class="text-gray-800 font-medium"><?= htmlspecialchars($loginData['os'] ?? 'Unknown') ?></span>
</div>
<!-- ISP -->
<div class="flex items-center gap-1.5">
<i class="fas fa-server text-amber-400 w-3.5 text-center"></i>
<span class="text-gray-500">ISP:</span>
<span class="text-gray-800 font-medium truncate"><?= htmlspecialchars($loginData['isp'] ?? 'Unknown') ?></span>
</div>
</div>
<!-- Reason (mirrors Method row) -->
<div class="mt-2 pt-2 border-t border-red-200 flex items-center gap-1.5 text-xs">
<i class="fas fa-exclamation-triangle text-gray-400 w-3.5 text-center"></i>
<span class="text-gray-500">Reason:</span>
<span class="inline-flex items-center px-1.5 py-0.5 bg-red-100 text-red-700 rounded font-medium text-[11px]"><?= htmlspecialchars($loginData['reason'] ?? 'Unknown') ?></span>
</div>
</div>
<?php elseif ($isLogin): ?>
<!-- Rich login details -->
<div class="mt-1.5 bg-gray-50 rounded-lg p-3 border border-gray-100">
<div class="grid grid-cols-2 md:grid-cols-3 gap-x-4 gap-y-2 text-xs">
<!-- Location -->
<div class="flex items-center gap-1.5">
<i class="fas fa-map-marker-alt text-red-400 w-3.5 text-center"></i>
<span class="text-gray-500">Location:</span>
<span class="text-gray-800 font-medium">
<?php if ($loginData['country_code'] !== 'xx'): ?>
<span class="fi fi-<?= strtolower($loginData['country_code']) ?> text-xs mr-0.5 rounded-sm"></span>
<?php endif; ?>
<?= htmlspecialchars($loginData['location'] ?? 'Unknown') ?>
</span>
</div>
<!-- IP Address -->
<div class="flex items-center gap-1.5">
<i class="fas fa-network-wired text-blue-400 w-3.5 text-center"></i>
<span class="text-gray-500">IP:</span>
<span class="text-gray-800 font-medium font-mono text-[11px]"><?= htmlspecialchars($loginData['ip'] ?? 'unknown') ?></span>
</div>
<!-- Browser -->
<div class="flex items-center gap-1.5">
<i class="fas fa-globe text-green-400 w-3.5 text-center"></i>
<span class="text-gray-500">Browser:</span>
<span class="text-gray-800 font-medium"><?= htmlspecialchars($loginData['browser'] ?? 'Unknown') ?></span>
</div>
<!-- Device -->
<div class="flex items-center gap-1.5">
<i class="fas fa-<?= htmlspecialchars($loginData['device_icon'] ?? 'desktop') ?> text-purple-400 w-3.5 text-center"></i>
<span class="text-gray-500">Device:</span>
<span class="text-gray-800 font-medium"><?= htmlspecialchars($loginData['device'] ?? 'Unknown') ?></span>
</div>
<!-- OS -->
<div class="flex items-center gap-1.5">
<i class="fas fa-laptop-code text-indigo-400 w-3.5 text-center"></i>
<span class="text-gray-500">OS:</span>
<span class="text-gray-800 font-medium"><?= htmlspecialchars($loginData['os'] ?? 'Unknown') ?></span>
</div>
<!-- ISP -->
<div class="flex items-center gap-1.5">
<i class="fas fa-server text-amber-400 w-3.5 text-center"></i>
<span class="text-gray-500">ISP:</span>
<span class="text-gray-800 font-medium truncate"><?= htmlspecialchars($loginData['isp'] ?? 'Unknown') ?></span>
</div>
</div>
<!-- Login method -->
<div class="mt-2 pt-2 border-t border-gray-200 flex items-center gap-1.5 text-xs">
<i class="fas fa-key text-gray-400 w-3.5 text-center"></i>
<span class="text-gray-500">Method:</span>
<span class="inline-flex items-center px-1.5 py-0.5 bg-blue-100 text-blue-700 rounded font-medium text-[11px]"><?= htmlspecialchars($loginData['method'] ?? 'Login') ?></span>
</div>
</div>
<?php else: ?>
<!-- Standard notification message -->
<p class="text-xs text-gray-600 mt-0.5"><?= htmlspecialchars($notification['message']) ?></p>
<?php if ($hasDomain && $clickUrl): ?>
<a href="<?= $clickUrl ?>" class="text-xs text-primary mt-0.5 hover:underline inline-block">
<i class="fas fa-external-link-alt text-[10px] mr-1"></i>View domain
</a>
<?php endif; ?>
<?php endif; ?>
</div>
<!-- Actions -->
<div class="flex items-center gap-1 ml-2">
<div class="flex items-center gap-1 ml-2 flex-shrink-0">
<?php if (!$notification['is_read']): ?>
<a href="/notifications/<?= $notification['id'] ?>/mark-read" class="w-7 h-7 flex items-center justify-center text-gray-400 hover:text-green-600 hover:bg-green-50 rounded transition-colors" title="Mark as read">
<i class="fas fa-check text-xs"></i>