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:
Hosteroid
2026-02-01 12:30:16 +02:00
parent 6f1316682d
commit 612a4bf790
8 changed files with 163 additions and 29 deletions

View File

@@ -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';

View File

@@ -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);

View File

@@ -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');

View File

@@ -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 -->

View File

@@ -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">

View File

@@ -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>

View File

@@ -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();
}
}
}

View File

@@ -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']);