Use POST for destructive actions & mobile UI tweaks
Require POST and CSRF verification for destructive endpoints (profile delete, notification delete, clear-all) and update routes accordingly. Replace GET-based delete links with POST forms (including csrf_field()) and add hidden form submission for "clear all" and account deletion via JS. Add server-side request method checks and verifyCsrf() calls in NotificationController and ProfileController. Improve mobile UX: add sidebar overlay, open/close controls (including swipe-to-close), close button, prevent body scroll when sidebar open, responsive search placeholder and adjusted search/top-nav styling, and minor layout tweaks (truncate app name, adjust notification dropdown width). Also minor whitespace/formatting cleanups.
This commit is contained in:
@@ -110,6 +110,15 @@ class NotificationController extends Controller
|
||||
*/
|
||||
public function delete($params = [])
|
||||
{
|
||||
// Ensure POST method
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/notifications');
|
||||
return;
|
||||
}
|
||||
|
||||
// CSRF Protection
|
||||
$this->verifyCsrf('/notifications');
|
||||
|
||||
$userId = Auth::id();
|
||||
$notificationId = (int)($params['id'] ?? 0);
|
||||
|
||||
@@ -129,6 +138,15 @@ class NotificationController extends Controller
|
||||
*/
|
||||
public function clearAll()
|
||||
{
|
||||
// Ensure POST method
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/notifications');
|
||||
return;
|
||||
}
|
||||
|
||||
// CSRF Protection
|
||||
$this->verifyCsrf('/notifications');
|
||||
|
||||
$userId = Auth::id();
|
||||
$this->notificationModel->clearAll($userId);
|
||||
$_SESSION['success'] = 'All notifications cleared';
|
||||
|
||||
@@ -232,6 +232,15 @@ class ProfileController extends Controller
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
// Ensure POST method
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/profile');
|
||||
return;
|
||||
}
|
||||
|
||||
// CSRF Protection
|
||||
$this->verifyCsrf('/profile');
|
||||
|
||||
$userId = Auth::id();
|
||||
$user = $this->userModel->find($userId);
|
||||
|
||||
|
||||
@@ -93,7 +93,22 @@ if (!isset($appName)) {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
/* Mobile sidebar overlay */
|
||||
.sidebar-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 25;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
|
||||
}
|
||||
.sidebar-overlay.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
@@ -120,12 +135,27 @@ if (!isset($appName)) {
|
||||
background: #374151;
|
||||
border-left: 4px solid #4A90E2;
|
||||
}
|
||||
|
||||
/* Mobile-friendly dropdown widths */
|
||||
@media (max-width: 480px) {
|
||||
#notificationsDropdown {
|
||||
width: calc(100vw - 2rem);
|
||||
right: -0.5rem;
|
||||
}
|
||||
#userDropdown {
|
||||
width: calc(100vw - 2rem);
|
||||
right: -0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
|
||||
<?php include __DIR__ . '/top-nav.php'; ?>
|
||||
|
||||
<!-- Mobile Sidebar Overlay -->
|
||||
<div id="sidebarOverlay" class="sidebar-overlay md:hidden" onclick="closeSidebar()"></div>
|
||||
|
||||
<?php include __DIR__ . '/sidebar.php'; ?>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
@@ -145,9 +175,71 @@ if (!isset($appName)) {
|
||||
<script>
|
||||
// Toggle sidebar on mobile
|
||||
function toggleSidebar() {
|
||||
document.getElementById('sidebar').classList.toggle('open');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
sidebar.classList.toggle('open');
|
||||
overlay.classList.toggle('show');
|
||||
// Prevent body scroll when sidebar is open
|
||||
document.body.style.overflow = sidebar.classList.contains('open') ? 'hidden' : '';
|
||||
}
|
||||
|
||||
// Close sidebar (for overlay click and link clicks)
|
||||
function closeSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
sidebar.classList.remove('open');
|
||||
overlay.classList.remove('show');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// Close sidebar when clicking a link (mobile only)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const sidebarLinks = document.querySelectorAll('#sidebar a');
|
||||
sidebarLinks.forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
if (window.innerWidth < 768) {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle swipe to close sidebar on mobile
|
||||
let touchStartX = 0;
|
||||
let touchEndX = 0;
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
|
||||
sidebar.addEventListener('touchstart', function(e) {
|
||||
touchStartX = e.changedTouches[0].screenX;
|
||||
}, { passive: true });
|
||||
|
||||
sidebar.addEventListener('touchend', function(e) {
|
||||
touchEndX = e.changedTouches[0].screenX;
|
||||
handleSwipe();
|
||||
}, { passive: true });
|
||||
|
||||
function handleSwipe() {
|
||||
const swipeThreshold = 50;
|
||||
if (touchStartX - touchEndX > swipeThreshold) {
|
||||
// Swiped left - close sidebar
|
||||
closeSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive search placeholder
|
||||
const searchInput = document.getElementById('globalSearchInput');
|
||||
if (searchInput) {
|
||||
function updateSearchPlaceholder() {
|
||||
if (window.innerWidth < 640) {
|
||||
searchInput.placeholder = 'Search...';
|
||||
} else {
|
||||
searchInput.placeholder = 'Search domains or lookup WHOIS...';
|
||||
}
|
||||
}
|
||||
updateSearchPlaceholder();
|
||||
window.addEventListener('resize', updateSearchPlaceholder);
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle user dropdown
|
||||
function toggleDropdown() {
|
||||
const dropdown = document.getElementById('userDropdown');
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
<div class="h-full overflow-y-auto flex flex-col">
|
||||
|
||||
<!-- Logo Section -->
|
||||
<div class="h-16 px-5 border-b border-gray-800 flex items-center">
|
||||
<div class="flex items-center">
|
||||
<div class="w-9 h-9 bg-primary rounded-lg flex items-center justify-center mr-3">
|
||||
<div class="h-16 px-4 sm:px-5 border-b border-gray-800 flex items-center justify-between flex-shrink-0">
|
||||
<div class="flex items-center min-w-0">
|
||||
<div class="w-9 h-9 bg-primary rounded-lg flex items-center justify-center mr-3 flex-shrink-0">
|
||||
<i class="fas fa-globe text-white text-sm"></i>
|
||||
</div>
|
||||
<h1 class="text-sm font-semibold text-white"><?= $appName ?? 'Domain Monitor' ?></h1>
|
||||
<h1 class="text-sm font-semibold text-white truncate"><?= $appName ?? 'Domain Monitor' ?></h1>
|
||||
</div>
|
||||
<!-- Close button for mobile -->
|
||||
<button onclick="closeSidebar()" class="md:hidden w-9 h-9 flex items-center justify-center text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors flex-shrink-0 ml-2">
|
||||
<i class="fas fa-times text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
|
||||
@@ -24,15 +24,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Center: Search Bar -->
|
||||
<div class="flex-1 max-w-2xl mx-8">
|
||||
<form action="/search" method="GET" class="relative hidden md:block" id="globalSearchForm">
|
||||
<div class="flex-1 max-w-2xl mx-2 sm:mx-4 lg:mx-8">
|
||||
<form action="/search" method="GET" class="relative" id="globalSearchForm">
|
||||
<input type="text"
|
||||
name="q"
|
||||
placeholder="Search domains or lookup WHOIS..."
|
||||
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent text-sm"
|
||||
placeholder="Search..."
|
||||
class="w-full pl-9 sm:pl-10 pr-3 sm:pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent text-sm"
|
||||
id="globalSearchInput"
|
||||
autocomplete="off">
|
||||
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
|
||||
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-sm"></i>
|
||||
|
||||
<!-- Search Results Dropdown -->
|
||||
<div id="searchDropdown" class="hidden absolute top-full left-0 right-0 mt-2 bg-white rounded-lg shadow-xl border border-gray-200 max-h-96 overflow-y-auto z-50">
|
||||
@@ -49,7 +49,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Right: Actions & User -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex items-center space-x-1 sm:space-x-2">
|
||||
<!-- Quick Add Domain -->
|
||||
<a href="/domains/create" title="Add Domain" class="flex items-center justify-center w-9 h-9 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors duration-150">
|
||||
<i class="fas fa-plus"></i>
|
||||
@@ -68,7 +68,7 @@
|
||||
</button>
|
||||
|
||||
<!-- Notifications Dropdown -->
|
||||
<div id="notificationsDropdown" class="dropdown-menu absolute right-0 mt-2 w-96 bg-white rounded-lg shadow-xl border border-gray-200 max-h-[32rem] overflow-hidden">
|
||||
<div id="notificationsDropdown" class="dropdown-menu absolute right-0 mt-2 w-80 sm:w-96 bg-white rounded-lg shadow-xl border border-gray-200 max-h-[32rem] overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
@@ -162,9 +162,12 @@ $offset = $pagination['showing_from'] - 1;
|
||||
<i class="fas fa-check text-xs"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<a href="/notifications/<?= $notification['id'] ?>/delete" onclick="return confirm('Delete this notification?')" class="w-7 h-7 flex items-center justify-center text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors" title="Delete">
|
||||
<form method="POST" action="/notifications/<?= $notification['id'] ?>/delete" class="inline" onsubmit="return confirm('Delete this notification?')">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="w-7 h-7 flex items-center justify-center text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors" title="Delete">
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
</a>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -266,6 +269,11 @@ $offset = $pagination['showing_from'] - 1;
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Hidden form for clear all -->
|
||||
<form id="clearAllForm" method="POST" action="/notifications/clear-all" class="hidden">
|
||||
<?= csrf_field() ?>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function markAllAsRead() {
|
||||
if (confirm('Mark all notifications as read?')) {
|
||||
@@ -275,7 +283,7 @@ function markAllAsRead() {
|
||||
|
||||
function clearAll() {
|
||||
if (confirm('Clear all notifications? This action cannot be undone.')) {
|
||||
window.location.href = '/notifications/clear-all';
|
||||
document.getElementById('clearAllForm').submit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -644,10 +644,13 @@ $avatar = \App\Helpers\AvatarHelper::getAvatar($user, 80);
|
||||
This action cannot be undone
|
||||
</p>
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
@@ -734,7 +737,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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.')) {
|
||||
window.location.href = '/profile/delete';
|
||||
document.getElementById('deleteAccountForm').submit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ $router->post('/settings/toggle-isolation', [SettingsController::class, 'toggleI
|
||||
$router->get('/profile', [ProfileController::class, 'index']);
|
||||
$router->post('/profile/update', [ProfileController::class, 'update']);
|
||||
$router->post('/profile/change-password', [ProfileController::class, 'changePassword']);
|
||||
$router->get('/profile/delete', [ProfileController::class, 'delete']);
|
||||
$router->post('/profile/delete', [ProfileController::class, 'delete']);
|
||||
$router->get('/profile/resend-verification', [ProfileController::class, 'resendVerification']);
|
||||
$router->post('/profile/logout-other-sessions', [ProfileController::class, 'logoutOtherSessions']);
|
||||
$router->post('/profile/logout-session/{sessionId}', [ProfileController::class, 'logoutSession']);
|
||||
@@ -154,8 +154,8 @@ $router->post('/2fa/regenerate-backup-codes', [TwoFactorController::class, 'rege
|
||||
$router->get('/notifications', [NotificationController::class, 'index']);
|
||||
$router->get('/notifications/{id}/mark-read', [NotificationController::class, 'markAsRead']);
|
||||
$router->get('/notifications/mark-all-read', [NotificationController::class, 'markAllAsRead']);
|
||||
$router->get('/notifications/{id}/delete', [NotificationController::class, 'delete']);
|
||||
$router->get('/notifications/clear-all', [NotificationController::class, 'clearAll']);
|
||||
$router->post('/notifications/{id}/delete', [NotificationController::class, 'delete']);
|
||||
$router->post('/notifications/clear-all', [NotificationController::class, 'clearAll']);
|
||||
$router->get('/api/notifications/unread-count', [NotificationController::class, 'getUnreadCount']);
|
||||
$router->get('/api/notifications/recent', [NotificationController::class, 'getRecent']);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user