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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user