From 6fbed15c7d8e3b6e4e697d333b5325ccdd0105c8 Mon Sep 17 00:00:00 2001 From: Hosteroid Date: Mon, 20 Oct 2025 17:04:13 +0300 Subject: [PATCH] Add user isolation mode and transfer features Introduces user isolation mode, allowing domains, groups, and tags to be visible only to their owners when enabled. Adds user_id fields to domains and notification_groups, updates models and controllers for isolation-aware queries, and provides admin UI and endpoints for transferring domains and groups between users (single and bulk). Includes migration, settings UI, and routes for toggling isolation mode and handling data migration. --- app/Controllers/DashboardController.php | 25 ++- app/Controllers/DomainController.php | 121 +++++++++++++- app/Controllers/InstallerController.php | 3 + .../NotificationGroupController.php | 147 +++++++++++++++++- app/Controllers/SearchController.php | 43 ++++- app/Controllers/SettingsController.php | 104 +++++++++++++ app/Models/Domain.php | 135 ++++++++++++---- app/Models/NotificationGroup.php | 57 +++++-- app/Views/groups/index.php | 118 ++++++++++++++ app/Views/settings/index.php | 77 ++++++++- .../migrations/000_initial_schema_v1.1.0.sql | 15 +- .../migrations/018_add_user_isolation.sql | 36 +++++ routes/web.php | 5 + 13 files changed, 825 insertions(+), 61 deletions(-) create mode 100644 database/migrations/018_add_user_isolation.sql diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php index af38d60..74c98fb 100644 --- a/app/Controllers/DashboardController.php +++ b/app/Controllers/DashboardController.php @@ -22,20 +22,35 @@ class DashboardController extends Controller public function index() { - $stats = $this->domainModel->getStatistics(); - $recentDomains = $this->domainModel->getRecent(5); // Get 5 most recent domains + // Get current user and isolation mode + $userId = \Core\Auth::id(); + $settingModel = new \App\Models\Setting(); + $isolationMode = $settingModel->getValue('user_isolation_mode', 'shared'); + + // Get user-specific or global statistics + if ($isolationMode === 'isolated') { + $stats = $this->domainModel->getStatistics($userId); + $recentDomains = $this->domainModel->getRecent(5, $userId); + $groups = $this->groupModel->getAllWithChannelCount($userId); + } else { + $stats = $this->domainModel->getStatistics(); + $recentDomains = $this->domainModel->getRecent(5); + $groups = $this->groupModel->getAllWithChannelCount(); + } // Get expiring threshold from settings - $settingModel = new \App\Models\Setting(); $notificationDays = $settingModel->getNotificationDays(); $expiringThreshold = !empty($notificationDays) ? max($notificationDays) : 30; // Get expiring domains limited to top 5 - $allExpiringDomains = $this->domainModel->getExpiringDomains($expiringThreshold); + if ($isolationMode === 'isolated') { + $allExpiringDomains = $this->domainModel->getExpiringDomains($expiringThreshold, $userId); + } else { + $allExpiringDomains = $this->domainModel->getExpiringDomains($expiringThreshold); + } $expiringThisMonth = array_slice($allExpiringDomains, 0, 5); $recentLogs = $this->logModel->getRecent(10); - $groups = $this->groupModel->all(); // Check system status $systemStatus = $this->checkSystemStatus(); diff --git a/app/Controllers/DomainController.php b/app/Controllers/DomainController.php index f32991a..a7cc60c 100644 --- a/app/Controllers/DomainController.php +++ b/app/Controllers/DomainController.php @@ -22,6 +22,11 @@ class DomainController extends Controller public function index() { + // Get current user and isolation mode + $userId = \Core\Auth::id(); + $settingModel = new \App\Models\Setting(); + $isolationMode = $settingModel->getValue('user_isolation_mode', 'shared'); + // Get filter parameters $search = \App\Helpers\InputValidator::sanitizeSearch($_GET['search'] ?? '', 100); $status = $_GET['status'] ?? ''; @@ -33,7 +38,6 @@ class DomainController extends Controller $perPage = max(10, min(100, (int)($_GET['per_page'] ?? 25))); // Between 10 and 100 // Get expiring threshold from settings - $settingModel = new \App\Models\Setting(); $notificationDays = $settingModel->getNotificationDays(); $expiringThreshold = !empty($notificationDays) ? max($notificationDays) : 30; @@ -46,12 +50,16 @@ class DomainController extends Controller ]; // Get filtered and paginated domains using model - $result = $this->domainModel->getFilteredPaginated($filters, $sortBy, $sortOrder, $page, $perPage, $expiringThreshold); + $result = $this->domainModel->getFilteredPaginated($filters, $sortBy, $sortOrder, $page, $perPage, $expiringThreshold, $isolationMode === 'isolated' ? $userId : null); - $groups = $this->groupModel->all(); - - // Get all unique tags for filter dropdown - $allTags = $this->domainModel->getAllTags(); + // Get groups and tags based on isolation mode + if ($isolationMode === 'isolated') { + $groups = $this->groupModel->getAllWithChannelCount($userId); + $allTags = $this->domainModel->getAllTags($userId); + } else { + $groups = $this->groupModel->getAllWithChannelCount(); + $allTags = $this->domainModel->getAllTags(); + } // Format domains for display $formattedDomains = \App\Helpers\DomainHelper::formatMultiple($result['domains']); @@ -756,5 +764,106 @@ class DomainController extends Controller $_SESSION['success'] = "Tags removed from $updated domain(s)"; $this->redirect('/domains'); } + + /** + * Transfer domain to another user (Admin only) + */ + public function transfer() + { + \Core\Auth::requireAdmin(); + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $this->redirect('/domains'); + return; + } + + $this->verifyCsrf('/domains'); + + $domainId = (int)($_POST['domain_id'] ?? 0); + $targetUserId = (int)($_POST['target_user_id'] ?? 0); + + if (!$domainId || !$targetUserId) { + $_SESSION['error'] = 'Invalid domain or user selected'; + $this->redirect('/domains'); + return; + } + + // Validate domain exists + $domain = $this->domainModel->find($domainId); + if (!$domain) { + $_SESSION['error'] = 'Domain not found'; + $this->redirect('/domains'); + return; + } + + // Validate target user exists + $userModel = new \App\Models\User(); + $targetUser = $userModel->find($targetUserId); + if (!$targetUser) { + $_SESSION['error'] = 'Target user not found'; + $this->redirect('/domains'); + return; + } + + try { + // Transfer domain + $this->domainModel->update($domainId, ['user_id' => $targetUserId]); + + $_SESSION['success'] = "Domain '{$domain['domain_name']}' transferred to {$targetUser['username']}"; + } catch (\Exception $e) { + $_SESSION['error'] = 'Failed to transfer domain. Please try again.'; + } + + $this->redirect('/domains'); + } + + /** + * Bulk transfer domains to another user (Admin only) + */ + public function bulkTransfer() + { + \Core\Auth::requireAdmin(); + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $this->redirect('/domains'); + return; + } + + $this->verifyCsrf('/domains'); + + $domainIds = $_POST['domain_ids'] ?? []; + $targetUserId = (int)($_POST['target_user_id'] ?? 0); + + if (empty($domainIds) || !$targetUserId) { + $_SESSION['error'] = 'No domains selected or invalid user'; + $this->redirect('/domains'); + return; + } + + // Validate target user exists + $userModel = new \App\Models\User(); + $targetUser = $userModel->find($targetUserId); + if (!$targetUser) { + $_SESSION['error'] = 'Target user not found'; + $this->redirect('/domains'); + return; + } + + $transferred = 0; + foreach ($domainIds as $domainId) { + $domainId = (int)$domainId; + if ($domainId > 0) { + try { + $this->domainModel->update($domainId, ['user_id' => $targetUserId]); + $transferred++; + } catch (\Exception $e) { + // Continue with other domains + } + } + } + + $_SESSION['success'] = "$transferred domain(s) transferred to {$targetUser['username']}"; + $this->redirect('/domains'); + } } diff --git a/app/Controllers/InstallerController.php b/app/Controllers/InstallerController.php index a3b8a3b..3a7b501 100644 --- a/app/Controllers/InstallerController.php +++ b/app/Controllers/InstallerController.php @@ -49,6 +49,7 @@ class InstallerController extends Controller '015_create_error_logs_table.sql', '016_add_tags_to_domains.sql', '017_add_two_factor_authentication.sql', + '018_add_user_isolation.sql', ]; try { @@ -268,6 +269,8 @@ class InstallerController extends Controller '014_add_captcha_settings.sql', '015_create_error_logs_table.sql', '016_add_tags_to_domains.sql', + '017_add_two_factor_authentication.sql', + '018_add_user_isolation.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 d4fd843..baba30f 100644 --- a/app/Controllers/NotificationGroupController.php +++ b/app/Controllers/NotificationGroupController.php @@ -19,10 +19,28 @@ class NotificationGroupController extends Controller public function index() { - $groups = $this->groupModel->getAllWithChannelCount(); + // Get current user and isolation mode + $userId = \Core\Auth::id(); + $settingModel = new \App\Models\Setting(); + $isolationMode = $settingModel->getValue('user_isolation_mode', 'shared'); + + // Get groups based on isolation mode + if ($isolationMode === 'isolated') { + $groups = $this->groupModel->getAllWithChannelCount($userId); + } else { + $groups = $this->groupModel->getAllWithChannelCount(); + } + + // Get users for transfer functionality (admin only) + $users = []; + if (\Core\Auth::user()['role'] === 'admin') { + $userModel = new \App\Models\User(); + $users = $userModel->all(); + } $this->view('groups/index', [ 'groups' => $groups, + 'users' => $users, 'title' => 'Notification Groups' ]); } @@ -69,10 +87,22 @@ class NotificationGroupController extends Controller } try { - $id = $this->groupModel->create([ + // Get current user and isolation mode + $userId = \Core\Auth::id(); + $settingModel = new \App\Models\Setting(); + $isolationMode = $settingModel->getValue('user_isolation_mode', 'shared'); + + $groupData = [ 'name' => $name, 'description' => $description - ]); + ]; + + // Assign to current user if in isolated mode + if ($isolationMode === 'isolated') { + $groupData['user_id'] = $userId; + } + + $id = $this->groupModel->create($groupData); $_SESSION['success'] = "Group '$name' created successfully"; $this->redirect("/groups/edit?id=$id"); @@ -492,5 +522,116 @@ class NotificationGroupController extends Controller $this->redirect('/groups'); } + + /** + * Transfer group to another user (Admin only) + */ + public function transfer() + { + \Core\Auth::requireAdmin(); + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $this->redirect('/groups'); + return; + } + + $this->verifyCsrf('/groups'); + + $groupId = (int)($_POST['group_id'] ?? 0); + $targetUserId = (int)($_POST['target_user_id'] ?? 0); + + if (!$groupId || !$targetUserId) { + $_SESSION['error'] = 'Invalid group or user selected'; + $this->redirect('/groups'); + return; + } + + // Validate group exists + $group = $this->groupModel->find($groupId); + if (!$group) { + $_SESSION['error'] = 'Group not found'; + $this->redirect('/groups'); + return; + } + + // Validate target user exists + $userModel = new \App\Models\User(); + $targetUser = $userModel->find($targetUserId); + if (!$targetUser) { + $_SESSION['error'] = 'Target user not found'; + $this->redirect('/groups'); + return; + } + + try { + // Transfer group + $this->groupModel->update($groupId, ['user_id' => $targetUserId]); + + // Also transfer all domains in this group + $domainModel = new \App\Models\Domain(); + $domainModel->updateWhere(['notification_group_id' => $groupId], ['user_id' => $targetUserId]); + + $_SESSION['success'] = "Group '{$group['name']}' and its domains transferred to {$targetUser['username']}"; + } catch (\Exception $e) { + $_SESSION['error'] = 'Failed to transfer group. Please try again.'; + } + + $this->redirect('/groups'); + } + + /** + * Bulk transfer groups to another user (Admin only) + */ + public function bulkTransfer() + { + \Core\Auth::requireAdmin(); + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $this->redirect('/groups'); + return; + } + + $this->verifyCsrf('/groups'); + + $groupIds = $_POST['group_ids'] ?? []; + $targetUserId = (int)($_POST['target_user_id'] ?? 0); + + if (empty($groupIds) || !$targetUserId) { + $_SESSION['error'] = 'No groups selected or invalid user'; + $this->redirect('/groups'); + return; + } + + // Validate target user exists + $userModel = new \App\Models\User(); + $targetUser = $userModel->find($targetUserId); + if (!$targetUser) { + $_SESSION['error'] = 'Target user not found'; + $this->redirect('/groups'); + return; + } + + $transferred = 0; + foreach ($groupIds as $groupId) { + $groupId = (int)$groupId; + if ($groupId > 0) { + try { + // Transfer group + $this->groupModel->update($groupId, ['user_id' => $targetUserId]); + + // Also transfer all domains in this group + $domainModel = new \App\Models\Domain(); + $domainModel->updateWhere(['notification_group_id' => $groupId], ['user_id' => $targetUserId]); + + $transferred++; + } catch (\Exception $e) { + // Continue with other groups + } + } + } + + $_SESSION['success'] = "$transferred group(s) and their domains transferred to {$targetUser['username']}"; + $this->redirect('/groups'); + } } diff --git a/app/Controllers/SearchController.php b/app/Controllers/SearchController.php index 694109d..2bb5165 100644 --- a/app/Controllers/SearchController.php +++ b/app/Controllers/SearchController.php @@ -27,12 +27,17 @@ class SearchController extends Controller return; } + // Get current user and isolation mode + $userId = \Core\Auth::id(); + $settingModel = new \App\Models\Setting(); + $isolationMode = $settingModel->getValue('user_isolation_mode', 'shared'); + // Pagination parameters $page = max(1, (int)($_GET['page'] ?? 1)); $perPage = max(10, min(100, (int)($_GET['per_page'] ?? 25))); // Search existing domains in database - $allResults = $this->searchDomains($query); + $allResults = $this->searchDomains($query, $isolationMode === 'isolated' ? $userId : null); $totalResults = count($allResults); // Calculate pagination @@ -144,25 +149,47 @@ class SearchController extends Controller /** * Search domains in database */ - private function searchDomains(string $query): array + private function searchDomains(string $query, ?int $userId = null): array { $db = \Core\Database::getConnection(); $sql = "SELECT d.*, ng.name as group_name FROM domains d LEFT JOIN notification_groups ng ON d.notification_group_id = ng.id - WHERE d.domain_name LIKE ? + WHERE (d.domain_name LIKE ? OR d.registrar LIKE ? - OR ng.name LIKE ? - ORDER BY d.domain_name ASC - LIMIT 50"; + OR ng.name LIKE ?)"; + + $params = ['%' . $query . '%', '%' . $query . '%', '%' . $query . '%']; + + if ($userId && !$this->isAdmin($userId)) { + $sql .= " AND d.user_id = ?"; + $params[] = $userId; + } + + $sql .= " ORDER BY d.domain_name ASC LIMIT 50"; - $searchTerm = '%' . $query . '%'; $stmt = $db->prepare($sql); - $stmt->execute([$searchTerm, $searchTerm, $searchTerm]); + $stmt->execute($params); return $stmt->fetchAll(); } + /** + * Check if user is admin + */ + private function isAdmin(?int $userId): bool + { + if (!$userId) { + return false; + } + + $db = \Core\Database::getConnection(); + $stmt = $db->prepare("SELECT role FROM users WHERE id = ?"); + $stmt->execute([$userId]); + $user = $stmt->fetch(); + return $user && $user['role'] === 'admin'; + } + /** * Check if string looks like a domain name */ diff --git a/app/Controllers/SettingsController.php b/app/Controllers/SettingsController.php index c62ef24..55b7fda 100644 --- a/app/Controllers/SettingsController.php +++ b/app/Controllers/SettingsController.php @@ -27,6 +27,7 @@ class SettingsController extends Controller $emailSettings = $this->settingModel->getEmailSettings(); $captchaSettings = $this->settingModel->getCaptchaSettings(); $twoFactorSettings = $this->settingModel->getTwoFactorSettings(); + $isolationSettings = $this->getIsolationSettings(); // Predefined notification day options $notificationPresets = [ @@ -71,6 +72,7 @@ class SettingsController extends Controller 'emailSettings' => $emailSettings, 'captchaSettings' => $captchaSettings, 'twoFactorSettings' => $twoFactorSettings, + 'isolationSettings' => $isolationSettings, 'notificationPresets' => $notificationPresets, 'checkIntervalPresets' => $checkIntervalPresets, 'title' => 'Settings' @@ -491,5 +493,107 @@ class SettingsController extends Controller $this->redirect('/settings#security'); } } + + /** + * Get isolation settings + */ + private function getIsolationSettings(): array + { + return [ + 'user_isolation_mode' => $this->settingModel->getValue('user_isolation_mode', 'shared') + ]; + } + + /** + * Toggle isolation mode + */ + public function toggleIsolationMode() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $this->redirect('/settings'); + return; + } + + $this->verifyCsrf('/settings#isolation'); + + $newMode = $_POST['user_isolation_mode'] ?? 'shared'; + + try { + if ($newMode === 'isolated') { + // Check if we have any admin users + $domainModel = new \App\Models\Domain(); + $adminUser = $domainModel->getFirstAdminUser(); + if (!$adminUser) { + $_SESSION['error'] = 'No admin users found. Please create an admin user first.'; + $this->redirect('/settings#isolation'); + return; + } + + // Run migration + $migrationResult = $this->migrateToIsolatedMode(); + if (!$migrationResult['success']) { + $_SESSION['error'] = 'Migration failed: ' . $migrationResult['error']; + $this->redirect('/settings#isolation'); + return; + } + + $_SESSION['success'] = "Isolation mode enabled. {$migrationResult['domains_assigned']} domains and {$migrationResult['groups_assigned']} groups assigned to admin."; + } else { + // Switching back to shared mode + $this->settingModel->setValue('user_isolation_mode', 'shared'); + $_SESSION['success'] = 'Switched to shared mode. All users can now see all domains and groups.'; + } + + $this->redirect('/settings#isolation'); + + } catch (\Exception $e) { + $_SESSION['error'] = 'Error updating isolation mode: ' . $e->getMessage(); + $this->redirect('/settings#isolation'); + } + } + + /** + * Migrate existing data to isolated mode + */ + private function migrateToIsolatedMode(): array + { + try { + // Get the first admin user + $domainModel = new \App\Models\Domain(); + $adminUser = $domainModel->getFirstAdminUser(); + + if (!$adminUser) { + throw new \Exception('No admin user found. Please create an admin user first.'); + } + + $adminId = $adminUser['id']; + + // Assign all domains to admin + $domainStmt = $this->settingModel->db->prepare("UPDATE domains SET user_id = ? WHERE user_id IS NULL"); + $domainStmt->execute([$adminId]); + $domainCount = $domainStmt->rowCount(); + + // Assign all groups to admin + $groupStmt = $this->settingModel->db->prepare("UPDATE notification_groups SET user_id = ? WHERE user_id IS NULL"); + $groupStmt->execute([$adminId]); + $groupCount = $groupStmt->rowCount(); + + // Set isolation mode + $this->settingModel->setValue('user_isolation_mode', 'isolated'); + + return [ + 'success' => true, + 'admin_id' => $adminId, + 'domains_assigned' => $domainCount, + 'groups_assigned' => $groupCount + ]; + + } catch (\Exception $e) { + return [ + 'success' => false, + 'error' => $e->getMessage() + ]; + } + } } diff --git a/app/Models/Domain.php b/app/Models/Domain.php index e99edf5..3c7528b 100644 --- a/app/Models/Domain.php +++ b/app/Models/Domain.php @@ -11,21 +11,28 @@ class Domain extends Model /** * Get all domains with their notification group */ - public function getAllWithGroups(): array + public function getAllWithGroups(?int $userId = null): array { $sql = "SELECT d.*, ng.name as group_name FROM domains d - LEFT JOIN notification_groups ng ON d.notification_group_id = ng.id - ORDER BY d.status DESC, d.expiration_date ASC"; - - $stmt = $this->db->query($sql); + LEFT JOIN notification_groups ng ON d.notification_group_id = ng.id"; + + if ($userId && !$this->isAdmin($userId)) { + $sql .= " WHERE d.user_id = ?"; + $stmt = $this->db->prepare($sql); + $stmt->execute([$userId]); + } else { + $sql .= " ORDER BY d.status DESC, d.expiration_date ASC"; + $stmt = $this->db->query($sql); + } + return $stmt->fetchAll(); } /** * Get domains expiring within days */ - public function getExpiringDomains(int $days): array + public function getExpiringDomains(int $days, ?int $userId = null): array { $sql = "SELECT d.*, ng.name as group_name FROM domains d @@ -33,27 +40,43 @@ class Domain extends Model WHERE d.is_active = 1 AND d.expiration_date IS NOT NULL AND d.expiration_date <= DATE_ADD(CURDATE(), INTERVAL ? DAY) - AND d.expiration_date >= CURDATE() - ORDER BY d.expiration_date ASC"; + AND d.expiration_date >= CURDATE()"; + + $params = [$days]; + + if ($userId && !$this->isAdmin($userId)) { + $sql .= " AND d.user_id = ?"; + $params[] = $userId; + } + + $sql .= " ORDER BY d.expiration_date ASC"; $stmt = $this->db->prepare($sql); - $stmt->execute([$days]); + $stmt->execute($params); return $stmt->fetchAll(); } /** * Get domains by status */ - public function getByStatus(string $status): array + public function getByStatus(string $status, ?int $userId = null): array { $sql = "SELECT d.*, ng.name as group_name FROM domains d LEFT JOIN notification_groups ng ON d.notification_group_id = ng.id - WHERE d.status = ? - ORDER BY d.expiration_date ASC"; + WHERE d.status = ?"; + + $params = [$status]; + + if ($userId && !$this->isAdmin($userId)) { + $sql .= " AND d.user_id = ?"; + $params[] = $userId; + } + + $sql .= " ORDER BY d.expiration_date ASC"; $stmt = $this->db->prepare($sql); - $stmt->execute([$status]); + $stmt->execute($params); return $stmt->fetchAll(); } @@ -100,24 +123,32 @@ class Domain extends Model /** * Get recent domains */ - public function getRecent(int $limit = 5): array + public function getRecent(int $limit = 5, ?int $userId = null): array { $sql = "SELECT d.*, ng.name as group_name FROM domains d LEFT JOIN notification_groups ng ON d.notification_group_id = ng.id - WHERE d.is_active = 1 - ORDER BY d.created_at DESC, d.id DESC - LIMIT ?"; + WHERE d.is_active = 1"; + + $params = []; + + if ($userId && !$this->isAdmin($userId)) { + $sql .= " AND d.user_id = ?"; + $params[] = $userId; + } + + $sql .= " ORDER BY d.created_at DESC, d.id DESC LIMIT ?"; + $params[] = $limit; $stmt = $this->db->prepare($sql); - $stmt->execute([$limit]); + $stmt->execute($params); return $stmt->fetchAll(); } /** * Get dashboard statistics */ - public function getStatistics(): array + public function getStatistics(?int $userId = null): array { $stats = [ 'total' => 0, @@ -127,9 +158,19 @@ class Domain extends Model 'inactive' => 0, ]; + // Build WHERE clause for user filtering + $whereClause = "WHERE is_active = 1"; + $params = []; + + if ($userId && !$this->isAdmin($userId)) { + $whereClause .= " AND user_id = ?"; + $params[] = $userId; + } + // Get status counts for active domains only - $sql = "SELECT status, COUNT(*) as count FROM domains WHERE is_active = 1 GROUP BY status"; - $stmt = $this->db->query($sql); + $sql = "SELECT status, COUNT(*) as count FROM domains $whereClause GROUP BY status"; + $stmt = $this->db->prepare($sql); + $stmt->execute($params); $results = $stmt->fetchAll(); $stats['total'] = array_sum(array_column($results, 'count')); @@ -139,7 +180,16 @@ class Domain extends Model } // Get count of inactive domains (is_active = 0) - $inactiveStmt = $this->db->query("SELECT COUNT(*) as count FROM domains WHERE is_active = 0"); + $inactiveWhereClause = "WHERE is_active = 0"; + $inactiveParams = []; + + if ($userId && !$this->isAdmin($userId)) { + $inactiveWhereClause .= " AND user_id = ?"; + $inactiveParams[] = $userId; + } + + $inactiveStmt = $this->db->prepare("SELECT COUNT(*) as count FROM domains $inactiveWhereClause"); + $inactiveStmt->execute($inactiveParams); $inactiveResult = $inactiveStmt->fetch(); $stats['inactive'] = $inactiveResult['count'] ?? 0; @@ -152,10 +202,10 @@ class Domain extends Model /** * Get filtered, sorted, and paginated domains */ - public function getFilteredPaginated(array $filters, string $sortBy, string $sortOrder, int $page, int $perPage, int $expiringThreshold = 30): array + public function getFilteredPaginated(array $filters, string $sortBy, string $sortOrder, int $page, int $perPage, int $expiringThreshold = 30, ?int $userId = null): array { // Get all domains with groups - $domains = $this->getAllWithGroups(); + $domains = $this->getAllWithGroups($userId); // Apply search filter if (!empty($filters['search'])) { @@ -242,9 +292,18 @@ class Domain extends Model /** * Get all unique tags from all domains */ - public function getAllTags(): array + public function getAllTags(?int $userId = null): array { - $stmt = $this->db->query("SELECT DISTINCT tags FROM domains WHERE tags IS NOT NULL AND tags != ''"); + $sql = "SELECT DISTINCT tags FROM domains WHERE tags IS NOT NULL AND tags != ''"; + $params = []; + + if ($userId && !$this->isAdmin($userId)) { + $sql .= " AND user_id = ?"; + $params[] = $userId; + } + + $stmt = $this->db->prepare($sql); + $stmt->execute($params); $results = $stmt->fetchAll(); $allTags = []; @@ -260,5 +319,29 @@ class Domain extends Model sort($allTags); return $allTags; } + + /** + * Check if user is admin + */ + private function isAdmin(?int $userId): bool + { + if (!$userId) { + return false; + } + + $stmt = $this->db->prepare("SELECT role FROM users WHERE id = ?"); + $stmt->execute([$userId]); + $user = $stmt->fetch(); + return $user && $user['role'] === 'admin'; + } + + /** + * Get first admin user + */ + public function getFirstAdminUser(): ?array + { + $stmt = $this->db->query("SELECT * FROM users WHERE role = 'admin' ORDER BY id ASC LIMIT 1"); + return $stmt->fetch() ?: null; + } } diff --git a/app/Models/NotificationGroup.php b/app/Models/NotificationGroup.php index 99cbe05..d6dc504 100644 --- a/app/Models/NotificationGroup.php +++ b/app/Models/NotificationGroup.php @@ -11,25 +11,31 @@ class NotificationGroup extends Model /** * Get all groups with channel count */ - public function getAllWithChannelCount(): array + public function getAllWithChannelCount(?int $userId = null): array { $sql = "SELECT ng.*, COUNT(DISTINCT nc.id) as channel_count, COUNT(DISTINCT d.id) as domain_count FROM notification_groups ng LEFT JOIN notification_channels nc ON ng.id = nc.notification_group_id - LEFT JOIN domains d ON ng.id = d.notification_group_id - GROUP BY ng.id - ORDER BY ng.name ASC"; - - $stmt = $this->db->query($sql); + LEFT JOIN domains d ON ng.id = d.notification_group_id"; + + if ($userId && !$this->isAdmin($userId)) { + $sql .= " WHERE ng.user_id = ?"; + $stmt = $this->db->prepare($sql); + $stmt->execute([$userId]); + } else { + $sql .= " GROUP BY ng.id ORDER BY ng.name ASC"; + $stmt = $this->db->query($sql); + } + return $stmt->fetchAll(); } /** * Get group with channels and domains */ - public function getWithDetails(int $id): ?array + public function getWithDetails(int $id, ?int $userId = null): ?array { $group = $this->find($id); @@ -37,13 +43,22 @@ class NotificationGroup extends Model return null; } + // Check if user has access to this group + if ($userId && !$this->isAdmin($userId) && $group['user_id'] != $userId) { + return null; + } + // Get channels $channelModel = new NotificationChannel(); $group['channels'] = $channelModel->getByGroupId($id); - // Get domains + // Get domains (filtered by user if needed) $domainModel = new Domain(); - $group['domains'] = $domainModel->where('notification_group_id', $id); + if ($userId && !$this->isAdmin($userId)) { + $group['domains'] = $domainModel->where('notification_group_id', $id, $userId); + } else { + $group['domains'] = $domainModel->where('notification_group_id', $id); + } return $group; } @@ -61,5 +76,29 @@ class NotificationGroup extends Model return $this->delete($id); } + + /** + * Check if user is admin + */ + private function isAdmin(?int $userId): bool + { + if (!$userId) { + return false; + } + + $stmt = $this->db->prepare("SELECT role FROM users WHERE id = ?"); + $stmt->execute([$userId]); + $user = $stmt->fetch(); + return $user && $user['role'] === 'admin'; + } + + /** + * Get first admin user + */ + public function getFirstAdminUser(): ?array + { + $stmt = $this->db->query("SELECT * FROM users WHERE role = 'admin' ORDER BY id ASC LIMIT 1"); + return $stmt->fetch() ?: null; + } } diff --git a/app/Views/groups/index.php b/app/Views/groups/index.php index 8ef620e..46b548b 100644 --- a/app/Views/groups/index.php +++ b/app/Views/groups/index.php @@ -37,6 +37,13 @@ ob_start();
+ + + + + ; + + if (users.length === 0) { + alert('No users available for transfer'); + return; + } + + const userOptions = users.map(user => + `` + ).join(''); + + const modal = document.createElement('div'); + modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'; + modal.innerHTML = ` +
+

Transfer Group

+

Transfer group "${groupName}" to another user:

+ +
+ + + +
+ + +
+ +
+ + +
+
+
+ `; + + document.body.appendChild(modal); +} + +// Bulk transfer groups +function bulkTransfer() { + const selectedCheckboxes = document.querySelectorAll('input[name="group_ids[]"]:checked'); + if (selectedCheckboxes.length === 0) { + alert('Please select groups to transfer'); + return; + } + + const users = ; + + if (users.length === 0) { + alert('No users available for transfer'); + return; + } + + const userOptions = users.map(user => + `` + ).join(''); + + const modal = document.createElement('div'); + modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'; + modal.innerHTML = ` +
+

Transfer Groups

+

Transfer ${selectedCheckboxes.length} selected group(s) to another user:

+ +
+ + ${Array.from(selectedCheckboxes).map(cb => + `` + ).join('')} + +
+ + +
+ +
+ + +
+
+
+ `; + + document.body.appendChild(modal); +} $preset) { Monitoring +
+ + +