diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php index 71f5378..ea488a3 100644 --- a/app/Controllers/AuthController.php +++ b/app/Controllers/AuthController.php @@ -12,13 +12,11 @@ class AuthController extends Controller { private User $userModel; private Setting $settingModel; - private $db; public function __construct() { $this->userModel = new User(); $this->settingModel = new Setting(); - $this->db = \Core\Database::getConnection(); } /** @@ -290,11 +288,8 @@ class AuthController extends Controller // Generate verification token $token = bin2hex(random_bytes(32)); - // Save token to database - $stmt = $this->db->prepare( - "UPDATE users SET email_verification_token = ?, email_verification_sent_at = NOW() WHERE id = ?" - ); - $stmt->execute([$token, $userId]); + // Save token to database using model + $this->userModel->updateEmailVerificationToken($userId, $token); // Send verification email $this->sendVerificationEmail($email, $fullName, $token); @@ -303,9 +298,8 @@ class AuthController extends Controller $_SESSION['pending_verification_email'] = $email; $this->redirect('/verify-email'); } else { - // Mark as verified and log them in - $stmt = $this->db->prepare("UPDATE users SET email_verified = 1 WHERE id = ?"); - $stmt->execute([$userId]); + // Mark as verified and log them in using model + $this->userModel->markEmailAsVerified($userId); $_SESSION['success'] = 'Account created successfully! You can now log in.'; $this->redirect('/login'); @@ -344,11 +338,8 @@ class AuthController extends Controller private function verifyEmail($token) { try { - $stmt = $this->db->prepare( - "SELECT * FROM users WHERE email_verification_token = ? AND email_verified = 0" - ); - $stmt->execute([$token]); - $user = $stmt->fetch(); + // Find user by verification token using model + $user = $this->userModel->findByVerificationToken($token); if (!$user) { $this->view('auth/verify-email', [ @@ -370,11 +361,8 @@ class AuthController extends Controller return; } - // Mark email as verified - $stmt = $this->db->prepare( - "UPDATE users SET email_verified = 1, email_verification_token = NULL WHERE id = ?" - ); - $stmt->execute([$user['id']]); + // Mark email as verified using model + $this->userModel->verifyEmailByToken($user['id']); $this->view('auth/verify-email', [ 'title' => 'Email Verified', @@ -424,10 +412,8 @@ class AuthController extends Controller // Generate new verification token $token = bin2hex(random_bytes(32)); - $stmt = $this->db->prepare( - "UPDATE users SET email_verification_token = ?, email_verification_sent_at = NOW() WHERE id = ?" - ); - $stmt->execute([$token, $user['id']]); + // Update verification token using model + $this->userModel->updateEmailVerificationToken($user['id'], $token); // Send verification email $this->sendVerificationEmail($user['email'], $user['full_name'], $token); @@ -507,11 +493,8 @@ class AuthController extends Controller $token = bin2hex(random_bytes(32)); $expiresAt = date('Y-m-d H:i:s', strtotime('+1 hour')); - // Save token to database - $stmt = $this->db->prepare( - "INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES (?, ?, ?)" - ); - $stmt->execute([$user['id'], $token, $expiresAt]); + // Save token to database using model + $this->userModel->createPasswordResetToken($user['id'], $token, $expiresAt); // Send reset email $this->sendPasswordResetEmail($user['email'], $user['full_name'], $token); @@ -538,12 +521,8 @@ class AuthController extends Controller return; } - // Verify token exists and is not expired - $stmt = $this->db->prepare( - "SELECT * FROM password_reset_tokens WHERE token = ? AND used = 0 AND expires_at > NOW()" - ); - $stmt->execute([$token]); - $resetToken = $stmt->fetch(); + // Verify token exists and is not expired using model + $resetToken = $this->userModel->findPasswordResetToken($token); if (!$resetToken) { $_SESSION['error'] = 'Invalid or expired reset link'; @@ -612,12 +591,8 @@ class AuthController extends Controller } try { - // Verify token - $stmt = $this->db->prepare( - "SELECT * FROM password_reset_tokens WHERE token = ? AND used = 0 AND expires_at > NOW()" - ); - $stmt->execute([$token]); - $resetToken = $stmt->fetch(); + // Verify token using model + $resetToken = $this->userModel->findPasswordResetToken($token); if (!$resetToken) { $_SESSION['error'] = 'Invalid or expired reset link'; @@ -628,9 +603,8 @@ class AuthController extends Controller // Update password $this->userModel->changePassword($resetToken['user_id'], $password); - // Mark token as used - $stmt = $this->db->prepare("UPDATE password_reset_tokens SET used = 1 WHERE id = ?"); - $stmt->execute([$resetToken['id']]); + // Mark token as used using model + $this->userModel->markPasswordResetTokenAsUsed($resetToken['id']); $_SESSION['success'] = 'Password reset successfully! You can now log in.'; $this->redirect('/login'); @@ -651,10 +625,8 @@ class AuthController extends Controller $expiresAt = date('Y-m-d H:i:s', strtotime('+30 days')); $sessionId = session_id(); - $stmt = $this->db->prepare( - "INSERT INTO remember_tokens (user_id, session_id, token, expires_at) VALUES (?, ?, ?, ?)" - ); - $stmt->execute([$userId, $sessionId, $token, $expiresAt]); + // Create remember token using model + $this->userModel->createRememberToken($userId, $sessionId, $token, $expiresAt); // Set cookie setcookie('remember_token', $token, [ @@ -683,11 +655,8 @@ class AuthController extends Controller } try { - $stmt = $this->db->prepare( - "SELECT user_id FROM remember_tokens WHERE token = ? AND expires_at > NOW()" - ); - $stmt->execute([$token]); - $rememberToken = $stmt->fetch(); + // Find user by remember token using model + $rememberToken = $this->userModel->findByRememberToken($token); if ($rememberToken) { $user = $this->userModel->find($rememberToken['user_id']); @@ -817,8 +786,8 @@ class AuthController extends Controller $token = $_COOKIE['remember_token']; try { - $stmt = $this->db->prepare("DELETE FROM remember_tokens WHERE token = ?"); - $stmt->execute([$token]); + // Delete remember token using model + $this->userModel->deleteRememberToken($token); } catch (\Exception $e) { // Silently fail } diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php index 33c403a..3b69ca9 100644 --- a/app/Controllers/DashboardController.php +++ b/app/Controllers/DashboardController.php @@ -76,13 +76,12 @@ class DashboardController extends Controller $status['database'] = ['status' => 'offline', 'color' => 'red']; } - // Check WHOIS service (test with a known TLD) + // Check TLD Registry (WHOIS service) try { - $whoisService = new \App\Services\WhoisService(); - // Quick test - just check if we can discover TLD servers $tldModel = new \App\Models\TldRegistry(); - $testTld = $tldModel->find(1); // Get first TLD - if ($testTld) { + // Check if ANY TLDs exist in registry (not just id=1) + $tldStats = $tldModel->getStatistics(); + if ($tldStats['total'] > 0) { $status['whois'] = ['status' => 'active', 'color' => 'green']; } else { $status['whois'] = ['status' => 'no data', 'color' => 'yellow']; diff --git a/app/Controllers/DomainController.php b/app/Controllers/DomainController.php index 1c16c38..a7c2b62 100644 --- a/app/Controllers/DomainController.php +++ b/app/Controllers/DomainController.php @@ -36,61 +36,20 @@ class DomainController extends Controller $notificationDays = $settingModel->getNotificationDays(); $expiringThreshold = !empty($notificationDays) ? max($notificationDays) : 30; - // Get all domains with groups - $domains = $this->domainModel->getAllWithGroups(); + // Prepare filters array + $filters = [ + 'search' => $search, + 'status' => $status, + 'group' => $groupId + ]; - // Apply filters - if (!empty($search)) { - $domains = array_filter($domains, function($domain) use ($search) { - return stripos($domain['domain_name'], $search) !== false || - stripos($domain['registrar'] ?? '', $search) !== false; - }); - } - - if (!empty($status)) { - $domains = array_filter($domains, function($domain) use ($status, $expiringThreshold) { - if ($status === 'expiring_soon') { - // Check if domain expires within configured threshold - if (!empty($domain['expiration_date'])) { - $daysLeft = floor((strtotime($domain['expiration_date']) - time()) / 86400); - return $daysLeft <= $expiringThreshold && $daysLeft >= 0; - } - return false; - } - return $domain['status'] === $status; - }); - } - - if (!empty($groupId)) { - $domains = array_filter($domains, function($domain) use ($groupId) { - return $domain['notification_group_id'] == $groupId; - }); - } - - // Get total count after filtering - $totalDomains = count($domains); - - // Apply sorting - usort($domains, function($a, $b) use ($sortBy, $sortOrder) { - $aVal = $a[$sortBy] ?? ''; - $bVal = $b[$sortBy] ?? ''; - - $comparison = strcasecmp($aVal, $bVal); - return $sortOrder === 'desc' ? -$comparison : $comparison; - }); - - // Calculate pagination - $totalPages = ceil($totalDomains / $perPage); - $page = min($page, max(1, $totalPages)); // Ensure page is within valid range - $offset = ($page - 1) * $perPage; - - // Slice array for current page - $paginatedDomains = array_slice($domains, $offset, $perPage); + // Get filtered and paginated domains using model + $result = $this->domainModel->getFilteredPaginated($filters, $sortBy, $sortOrder, $page, $perPage, $expiringThreshold); $groups = $this->groupModel->all(); // Format domains for display - $formattedDomains = \App\Helpers\DomainHelper::formatMultiple($paginatedDomains); + $formattedDomains = \App\Helpers\DomainHelper::formatMultiple($result['domains']); $this->view('domains/index', [ 'domains' => $formattedDomains, @@ -102,14 +61,7 @@ class DomainController extends Controller 'sort' => $sortBy, 'order' => $sortOrder ], - 'pagination' => [ - 'current_page' => $page, - 'per_page' => $perPage, - 'total' => $totalDomains, - 'total_pages' => $totalPages, - 'showing_from' => $totalDomains > 0 ? $offset + 1 : 0, - 'showing_to' => min($offset + $perPage, $totalDomains) - ], + 'pagination' => $result['pagination'], 'title' => 'Domains' ]); } diff --git a/app/Controllers/ErrorLogController.php b/app/Controllers/ErrorLogController.php new file mode 100644 index 0000000..217e04c --- /dev/null +++ b/app/Controllers/ErrorLogController.php @@ -0,0 +1,208 @@ +errorLogModel = new ErrorLog(); + } + + /** + * Display list of errors with filters + */ + public function index() + { + // Get filters from query params + $filters = [ + 'resolved' => $_GET['resolved'] ?? '', + 'type' => $_GET['type'] ?? '', + 'sort' => $_GET['sort'] ?? 'last_occurred_at', + 'order' => $_GET['order'] ?? 'desc' + ]; + + // Pagination + $page = isset($_GET['page']) ? (int)$_GET['page'] : 1; + $perPage = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 25; + $offset = ($page - 1) * $perPage; + + // Get total count using model + $totalErrors = $this->errorLogModel->countUniqueErrors($filters); + + // Get paginated errors using model + $errors = $this->errorLogModel->getPaginatedErrors($filters, $perPage, $offset); + + // Get statistics using model + $stats = $this->errorLogModel->getAdminStats(); + + // Pagination data + $totalPages = ceil($totalErrors / $perPage); + $pagination = [ + 'current_page' => $page, + 'total_pages' => $totalPages, + 'per_page' => $perPage, + 'total' => $totalErrors, + 'showing_from' => $totalErrors > 0 ? $offset + 1 : 0, + 'showing_to' => min($offset + $perPage, $totalErrors) + ]; + + $this->view('errors/admin-index', compact('errors', 'stats', 'filters', 'pagination')); + } + + /** + * Show error details + */ + public function show($params = []) + { + $errorId = $params['id'] ?? ''; + + // Get all occurrences using model + $errorOccurrences = $this->errorLogModel->getOccurrencesByErrorId($errorId); + + if (empty($errorOccurrences)) { + $_SESSION['error'] = 'Error not found'; + header('Location: /errors'); + exit; + } + + // Get the most recent occurrence for display + $error = $errorOccurrences[0]; + + // Parse JSON fields + $error['stack_trace_array'] = json_decode($error['stack_trace'], true) ?? []; + $error['request_data'] = json_decode($error['request_data'], true) ?? []; + $error['session_data'] = json_decode($error['session_data'], true) ?? []; + + $this->view('errors/admin-detail', compact('error', 'errorOccurrences')); + } + + /** + * Mark error as resolved + */ + public function markResolved($params = []) + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /errors'); + exit; + } + + $this->verifyCsrf('/errors'); + + $errorId = $params['id'] ?? ''; + $notes = $_POST['notes'] ?? null; + + // Mark error as resolved using model + $this->errorLogModel->markErrorResolved($errorId, $_SESSION['user_id'], $notes); + + $_SESSION['success'] = 'Error marked as resolved'; + header('Location: /errors/' . urlencode($errorId)); + exit; + } + + /** + * Mark error as unresolved + */ + public function markUnresolved($params = []) + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /errors'); + exit; + } + + $this->verifyCsrf('/errors'); + + $errorId = $params['id'] ?? ''; + + // Mark error as unresolved using model + $this->errorLogModel->markErrorUnresolved($errorId); + + $_SESSION['success'] = 'Error marked as unresolved'; + header('Location: /errors/' . urlencode($errorId)); + exit; + } + + /** + * Delete error and all its occurrences + */ + public function delete($params = []) + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /errors'); + exit; + } + + $this->verifyCsrf('/errors'); + + $errorId = $params['id'] ?? ''; + + // Delete error using model + $this->errorLogModel->deleteByErrorId($errorId); + + $_SESSION['success'] = 'Error deleted successfully'; + header('Location: /errors'); + exit; + } + + /** + * Clear old resolved errors + */ + public function clearResolved() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /errors'); + exit; + } + + $this->verifyCsrf('/errors'); + + $daysOld = isset($_POST['days']) ? (int)$_POST['days'] : 30; + + // Clear old errors using model + $deletedCount = $this->errorLogModel->clearOldResolved($daysOld); + + $_SESSION['success'] = "Deleted $deletedCount resolved error(s) older than $daysOld days"; + header('Location: /errors'); + exit; + } + + /** + * Bulk delete errors + */ + public function bulkDelete() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /errors'); + exit; + } + + $this->verifyCsrf('/errors'); + + $errorIdsJson = $_POST['error_ids'] ?? '[]'; + $errorIds = json_decode($errorIdsJson, true); + + if (empty($errorIds) || !is_array($errorIds)) { + $_SESSION['error'] = 'No errors selected for deletion'; + header('Location: /errors'); + exit; + } + + $deletedCount = 0; + foreach ($errorIds as $errorId) { + if ($this->errorLogModel->deleteByErrorId($errorId)) { + $deletedCount++; + } + } + + $_SESSION['success'] = "Successfully deleted $deletedCount error(s)"; + header('Location: /errors'); + exit; + } +} diff --git a/app/Controllers/InstallerController.php b/app/Controllers/InstallerController.php index 38a3ba6..a0611ec 100644 --- a/app/Controllers/InstallerController.php +++ b/app/Controllers/InstallerController.php @@ -46,6 +46,7 @@ class InstallerController extends Controller '012_link_remember_tokens_to_sessions.sql', '013_create_user_notifications_table.sql', '014_add_captcha_settings.sql', + '015_create_error_logs_table.sql', ]; try { @@ -105,7 +106,8 @@ class InstallerController extends Controller '011_create_sessions_table.sql', '012_link_remember_tokens_to_sessions.sql', '013_create_user_notifications_table.sql', - '014_add_captcha_settings.sql' + '014_add_captcha_settings.sql', + '015_create_error_logs_table.sql' ]; } @@ -261,7 +263,8 @@ class InstallerController extends Controller '011_create_sessions_table.sql', '012_link_remember_tokens_to_sessions.sql', '013_create_user_notifications_table.sql', - '014_add_captcha_settings.sql' + '014_add_captcha_settings.sql', + '015_create_error_logs_table.sql' ]; $stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?) ON DUPLICATE KEY UPDATE migration=migration"); diff --git a/app/Controllers/NotificationGroupController.php b/app/Controllers/NotificationGroupController.php index 953880f..4d4470d 100644 --- a/app/Controllers/NotificationGroupController.php +++ b/app/Controllers/NotificationGroupController.php @@ -245,5 +245,41 @@ class NotificationGroupController extends Controller return null; } } + + /** + * Bulk delete notification groups + */ + public function bulkDelete() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $this->redirect('/groups'); + return; + } + + $this->verifyCsrf('/groups'); + + $groupIdsJson = $_POST['group_ids'] ?? '[]'; + $groupIds = json_decode($groupIdsJson, true); + + if (empty($groupIds) || !is_array($groupIds)) { + $_SESSION['error'] = 'No groups selected for deletion'; + $this->redirect('/groups'); + return; + } + + $deletedCount = 0; + + foreach ($groupIds as $groupId) { + try { + $this->groupModel->deleteWithRelations((int)$groupId); + $deletedCount++; + } catch (\Exception $e) { + // Continue with next group + } + } + + $_SESSION['success'] = "Successfully deleted $deletedCount notification group(s)"; + $this->redirect('/groups'); + } } diff --git a/app/Controllers/SettingsController.php b/app/Controllers/SettingsController.php index 3672a93..4aa3172 100644 --- a/app/Controllers/SettingsController.php +++ b/app/Controllers/SettingsController.php @@ -3,6 +3,7 @@ namespace App\Controllers; use Core\Controller; +use Core\Auth; use App\Models\Setting; class SettingsController extends Controller @@ -11,14 +12,8 @@ class SettingsController extends Controller public function __construct() { + Auth::requireAdmin(); $this->settingModel = new Setting(); - - // Ensure only admins can access settings - if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') { - $_SESSION['error'] = 'Access denied. Admin privileges required.'; - $this->redirect('/'); - exit; - } } public function index() diff --git a/app/Controllers/TldRegistryController.php b/app/Controllers/TldRegistryController.php index 2b9f85d..b2de78d 100644 --- a/app/Controllers/TldRegistryController.php +++ b/app/Controllers/TldRegistryController.php @@ -3,6 +3,7 @@ namespace App\Controllers; use Core\Controller; +use Core\Auth; use App\Models\TldRegistry; use App\Models\TldImportLog; use App\Services\TldRegistryService; @@ -22,19 +23,6 @@ class TldRegistryController extends Controller $this->tldService = new TldRegistryService(); $this->logger = new Logger('tld_registry_controller'); } - - /** - * Check if current user is admin - */ - private function requireAdmin() - { - if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') { - $_SESSION['error'] = 'Access denied. Admin privileges required.'; - $this->redirect('/tld-registry'); - exit; - } - } - /** * Display TLD registry dashboard */ @@ -91,7 +79,7 @@ class TldRegistryController extends Controller */ public function importTldList() { - $this->requireAdmin(); + Auth::requireAdmin(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/tld-registry'); @@ -129,7 +117,7 @@ class TldRegistryController extends Controller */ public function importRdap() { - $this->requireAdmin(); + Auth::requireAdmin(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/tld-registry'); @@ -167,7 +155,7 @@ class TldRegistryController extends Controller */ public function importWhois() { - $this->requireAdmin(); + Auth::requireAdmin(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/tld-registry'); @@ -209,7 +197,7 @@ class TldRegistryController extends Controller */ public function checkUpdates() { - $this->requireAdmin(); + Auth::requireAdmin(); try { $updateInfo = $this->tldService->checkForUpdates(); @@ -251,7 +239,7 @@ class TldRegistryController extends Controller */ public function startProgressiveImport() { - $this->requireAdmin(); + Auth::requireAdmin(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/tld-registry'); @@ -464,7 +452,7 @@ class TldRegistryController extends Controller */ public function bulkDelete() { - $this->requireAdmin(); + Auth::requireAdmin(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/tld-registry'); @@ -512,7 +500,7 @@ class TldRegistryController extends Controller */ public function toggleActive($params = []) { - $this->requireAdmin(); + Auth::requireAdmin(); $id = $params['id'] ?? 0; $tld = $this->tldModel->find($id); @@ -536,7 +524,7 @@ class TldRegistryController extends Controller */ public function refresh($params = []) { - $this->requireAdmin(); + Auth::requireAdmin(); $id = $params['id'] ?? 0; $tld = $this->tldModel->find($id); diff --git a/app/Controllers/UserController.php b/app/Controllers/UserController.php index 7d00cb9..53c446d 100644 --- a/app/Controllers/UserController.php +++ b/app/Controllers/UserController.php @@ -12,14 +12,8 @@ class UserController extends Controller public function __construct() { + Auth::requireAdmin(); $this->userModel = new User(); - - // Ensure only admins can access user management - if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') { - $_SESSION['error'] = 'Access denied. Admin privileges required.'; - $this->redirect('/'); - exit; - } } /** @@ -378,5 +372,112 @@ class UserController extends Controller $this->redirect('/users'); } } + + /** + * Bulk toggle user status (activate or deactivate) + */ + public function bulkToggleStatus() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $this->redirect('/users'); + return; + } + + $this->verifyCsrf('/users'); + + $userIdsJson = $_POST['user_ids'] ?? '[]'; + $userIds = json_decode($userIdsJson, true); + $action = $_POST['action'] ?? 'activate'; // 'activate' or 'deactivate' + + if (empty($userIds) || !is_array($userIds)) { + $_SESSION['error'] = 'No users selected'; + $this->redirect('/users'); + return; + } + + $newStatus = ($action === 'activate') ? 1 : 0; + $updatedCount = 0; + $skippedSelf = false; + + foreach ($userIds as $userId) { + // Prevent modifying your own account + if ($userId == Auth::id()) { + $skippedSelf = true; + continue; + } + + try { + $this->userModel->update((int)$userId, ['is_active' => $newStatus]); + $updatedCount++; + } catch (\Exception $e) { + // Continue with next user + } + } + + $message = $action === 'activate' ? "Activated $updatedCount user(s)" : "Deactivated $updatedCount user(s)"; + if ($skippedSelf) { + $message .= ' (skipped your own account)'; + } + + $_SESSION['success'] = $message; + $this->redirect('/users'); + } + + /** + * Bulk delete users + */ + public function bulkDelete() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $this->redirect('/users'); + return; + } + + $this->verifyCsrf('/users'); + + $userIdsJson = $_POST['user_ids'] ?? '[]'; + $userIds = json_decode($userIdsJson, true); + + if (empty($userIds) || !is_array($userIds)) { + $_SESSION['error'] = 'No users selected for deletion'; + $this->redirect('/users'); + return; + } + + $deletedCount = 0; + $skippedSelf = false; + + foreach ($userIds as $userId) { + // Prevent deleting your own account + if ($userId == Auth::id()) { + $skippedSelf = true; + continue; + } + + // Prevent deleting if this is the last admin + $user = $this->userModel->find((int)$userId); + if ($user && $user['role'] === 'admin') { + $allAdmins = $this->userModel->where('role', 'admin'); + if (count($allAdmins) <= 1) { + continue; // Skip - can't delete last admin + } + } + + try { + $this->userModel->delete((int)$userId); + $deletedCount++; + } catch (\Exception $e) { + // Continue with next user + } + } + + $message = "Successfully deleted $deletedCount user(s)"; + if ($skippedSelf) { + $message .= ' (skipped your own account)'; + } + + $_SESSION['success'] = $message; + $this->redirect('/users'); + } } diff --git a/app/Helpers/ViewHelper.php b/app/Helpers/ViewHelper.php new file mode 100644 index 0000000..868a323 --- /dev/null +++ b/app/Helpers/ViewHelper.php @@ -0,0 +1,163 @@ +'; + } + + $icon = $currentOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down'; + return ''; + } + + /** + * Generate pagination URL + */ + public static function paginationUrl(int $page, array $filters, int $perPage): string + { + $params = $filters; + $params['page'] = $page; + $params['per_page'] = $perPage; + + $currentPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + return $currentPath . '?' . http_build_query($params); + } + + /** + * Format status badge + */ + public static function statusBadge(string $status): string + { + $statusClasses = [ + 'active' => 'bg-green-100 text-green-800 border-green-200', + 'expiring_soon' => 'bg-orange-100 text-orange-800 border-orange-200', + 'expired' => 'bg-red-100 text-red-800 border-red-200', + 'inactive' => 'bg-gray-100 text-gray-800 border-gray-200', + ]; + + $statusLabels = [ + 'active' => 'Active', + 'expiring_soon' => 'Expiring Soon', + 'expired' => 'Expired', + 'inactive' => 'Inactive', + ]; + + $class = $statusClasses[$status] ?? $statusClasses['inactive']; + $label = $statusLabels[$status] ?? ucfirst($status); + + return '' . htmlspecialchars($label) . ''; + } + + /** + * Truncate text with ellipsis + */ + public static function truncate(string $text, int $length = 50, string $suffix = '...'): string + { + if (mb_strlen($text) <= $length) { + return htmlspecialchars($text); + } + + return htmlspecialchars(mb_substr($text, 0, $length)) . $suffix; + } + + /** + * Format bytes to human readable size + */ + public static function formatBytes(int $bytes, int $precision = 2): string + { + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + + for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) { + $bytes /= 1024; + } + + return round($bytes, $precision) . ' ' . $units[$i]; + } + + /** + * Generate breadcrumb navigation + */ + public static function breadcrumbs(array $items): string + { + $html = ''; + return $html; + } + + /** + * Generate alert message HTML + */ + public static function alert(string $type, string $message): string + { + $classes = [ + 'success' => 'bg-green-50 border-green-200 text-green-800', + 'error' => 'bg-red-50 border-red-200 text-red-800', + 'warning' => 'bg-orange-50 border-orange-200 text-orange-800', + 'info' => 'bg-blue-50 border-blue-200 text-blue-800', + ]; + + $icons = [ + 'success' => 'fa-check-circle', + 'error' => 'fa-exclamation-circle', + 'warning' => 'fa-exclamation-triangle', + 'info' => 'fa-info-circle', + ]; + + $class = $classes[$type] ?? $classes['info']; + $icon = $icons[$type] ?? $icons['info']; + + return '