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 = [])
|
public function delete($params = [])
|
||||||
{
|
{
|
||||||
|
// Ensure POST method
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
$this->redirect('/notifications');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRF Protection
|
||||||
|
$this->verifyCsrf('/notifications');
|
||||||
|
|
||||||
$userId = Auth::id();
|
$userId = Auth::id();
|
||||||
$notificationId = (int)($params['id'] ?? 0);
|
$notificationId = (int)($params['id'] ?? 0);
|
||||||
|
|
||||||
@@ -129,6 +138,15 @@ class NotificationController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function clearAll()
|
public function clearAll()
|
||||||
{
|
{
|
||||||
|
// Ensure POST method
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
$this->redirect('/notifications');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRF Protection
|
||||||
|
$this->verifyCsrf('/notifications');
|
||||||
|
|
||||||
$userId = Auth::id();
|
$userId = Auth::id();
|
||||||
$this->notificationModel->clearAll($userId);
|
$this->notificationModel->clearAll($userId);
|
||||||
$_SESSION['success'] = 'All notifications cleared';
|
$_SESSION['success'] = 'All notifications cleared';
|
||||||
|
|||||||
@@ -232,6 +232,15 @@ class ProfileController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function delete()
|
public function delete()
|
||||||
{
|
{
|
||||||
|
// Ensure POST method
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
$this->redirect('/profile');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRF Protection
|
||||||
|
$this->verifyCsrf('/profile');
|
||||||
|
|
||||||
$userId = Auth::id();
|
$userId = Auth::id();
|
||||||
$user = $this->userModel->find($userId);
|
$user = $this->userModel->find($userId);
|
||||||
|
|
||||||
@@ -243,14 +252,14 @@ class ProfileController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete user (cascade will handle related records)
|
// Delete user (cascade will handle related records)
|
||||||
$this->userModel->delete($userId);
|
$this->userModel->delete($userId);
|
||||||
|
|
||||||
// Logout
|
// Logout
|
||||||
session_destroy();
|
session_destroy();
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
$_SESSION['success'] = 'Your account has been deleted';
|
$_SESSION['success'] = 'Your account has been deleted';
|
||||||
$this->redirect('/login');
|
$this->redirect('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -93,7 +93,22 @@ if (!isset($appName)) {
|
|||||||
transition: transform 0.3s ease-in-out;
|
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 {
|
.sidebar {
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
}
|
}
|
||||||
@@ -120,12 +135,27 @@ if (!isset($appName)) {
|
|||||||
background: #374151;
|
background: #374151;
|
||||||
border-left: 4px solid #4A90E2;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-50">
|
<body class="bg-gray-50">
|
||||||
|
|
||||||
<?php include __DIR__ . '/top-nav.php'; ?>
|
<?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'; ?>
|
<?php include __DIR__ . '/sidebar.php'; ?>
|
||||||
|
|
||||||
<!-- Main Content Area -->
|
<!-- Main Content Area -->
|
||||||
@@ -145,8 +175,70 @@ if (!isset($appName)) {
|
|||||||
<script>
|
<script>
|
||||||
// Toggle sidebar on mobile
|
// Toggle sidebar on mobile
|
||||||
function toggleSidebar() {
|
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
|
// Toggle user dropdown
|
||||||
function toggleDropdown() {
|
function toggleDropdown() {
|
||||||
|
|||||||
@@ -3,13 +3,17 @@
|
|||||||
<div class="h-full overflow-y-auto flex flex-col">
|
<div class="h-full overflow-y-auto flex flex-col">
|
||||||
|
|
||||||
<!-- Logo Section -->
|
<!-- Logo Section -->
|
||||||
<div class="h-16 px-5 border-b border-gray-800 flex items-center">
|
<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">
|
<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">
|
<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>
|
<i class="fas fa-globe text-white text-sm"></i>
|
||||||
</div>
|
</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>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation Links -->
|
<!-- Navigation Links -->
|
||||||
|
|||||||
@@ -24,15 +24,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Center: Search Bar -->
|
<!-- Center: Search Bar -->
|
||||||
<div class="flex-1 max-w-2xl mx-8">
|
<div class="flex-1 max-w-2xl mx-2 sm:mx-4 lg:mx-8">
|
||||||
<form action="/search" method="GET" class="relative hidden md:block" id="globalSearchForm">
|
<form action="/search" method="GET" class="relative" id="globalSearchForm">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="q"
|
name="q"
|
||||||
placeholder="Search domains or lookup WHOIS..."
|
placeholder="Search..."
|
||||||
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"
|
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"
|
id="globalSearchInput"
|
||||||
autocomplete="off">
|
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 -->
|
<!-- 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">
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Right: Actions & User -->
|
<!-- 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 -->
|
<!-- 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">
|
<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>
|
<i class="fas fa-plus"></i>
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Notifications Dropdown -->
|
<!-- 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 -->
|
<!-- Header -->
|
||||||
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
|
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
|||||||
@@ -162,9 +162,12 @@ $offset = $pagination['showing_from'] - 1;
|
|||||||
<i class="fas fa-check text-xs"></i>
|
<i class="fas fa-check text-xs"></i>
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?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?')">
|
||||||
<i class="fas fa-times text-xs"></i>
|
<?= csrf_field() ?>
|
||||||
</a>
|
<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>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -266,6 +269,11 @@ $offset = $pagination['showing_from'] - 1;
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Hidden form for clear all -->
|
||||||
|
<form id="clearAllForm" method="POST" action="/notifications/clear-all" class="hidden">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
</form>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function markAllAsRead() {
|
function markAllAsRead() {
|
||||||
if (confirm('Mark all notifications as read?')) {
|
if (confirm('Mark all notifications as read?')) {
|
||||||
@@ -275,7 +283,7 @@ function markAllAsRead() {
|
|||||||
|
|
||||||
function clearAll() {
|
function clearAll() {
|
||||||
if (confirm('Clear all notifications? This action cannot be undone.')) {
|
if (confirm('Clear all notifications? This action cannot be undone.')) {
|
||||||
window.location.href = '/notifications/clear-all';
|
document.getElementById('clearAllForm').submit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -644,10 +644,13 @@ $avatar = \App\Helpers\AvatarHelper::getAvatar($user, 80);
|
|||||||
This action cannot be undone
|
This action cannot be undone
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
||||||
<i class="fas fa-trash-alt mr-2"></i>
|
<?= csrf_field() ?>
|
||||||
Delete Account
|
<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">
|
||||||
</button>
|
<i class="fas fa-trash-alt mr-2"></i>
|
||||||
|
Delete Account
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -734,7 +737,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
function confirmDelete() {
|
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('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.')) {
|
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->get('/profile', [ProfileController::class, 'index']);
|
||||||
$router->post('/profile/update', [ProfileController::class, 'update']);
|
$router->post('/profile/update', [ProfileController::class, 'update']);
|
||||||
$router->post('/profile/change-password', [ProfileController::class, 'changePassword']);
|
$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->get('/profile/resend-verification', [ProfileController::class, 'resendVerification']);
|
||||||
$router->post('/profile/logout-other-sessions', [ProfileController::class, 'logoutOtherSessions']);
|
$router->post('/profile/logout-other-sessions', [ProfileController::class, 'logoutOtherSessions']);
|
||||||
$router->post('/profile/logout-session/{sessionId}', [ProfileController::class, 'logoutSession']);
|
$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', [NotificationController::class, 'index']);
|
||||||
$router->get('/notifications/{id}/mark-read', [NotificationController::class, 'markAsRead']);
|
$router->get('/notifications/{id}/mark-read', [NotificationController::class, 'markAsRead']);
|
||||||
$router->get('/notifications/mark-all-read', [NotificationController::class, 'markAllAsRead']);
|
$router->get('/notifications/mark-all-read', [NotificationController::class, 'markAllAsRead']);
|
||||||
$router->get('/notifications/{id}/delete', [NotificationController::class, 'delete']);
|
$router->post('/notifications/{id}/delete', [NotificationController::class, 'delete']);
|
||||||
$router->get('/notifications/clear-all', [NotificationController::class, 'clearAll']);
|
$router->post('/notifications/clear-all', [NotificationController::class, 'clearAll']);
|
||||||
$router->get('/api/notifications/unread-count', [NotificationController::class, 'getUnreadCount']);
|
$router->get('/api/notifications/unread-count', [NotificationController::class, 'getUnreadCount']);
|
||||||
$router->get('/api/notifications/recent', [NotificationController::class, 'getRecent']);
|
$router->get('/api/notifications/recent', [NotificationController::class, 'getRecent']);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user