From 5f5de248c658862871dfa14ca174327e20258407 Mon Sep 17 00:00:00 2001 From: Agent Date: Sun, 7 Jun 2026 06:49:12 -0400 Subject: [PATCH] Add server ownership, team access, role-based UI, and default admin role - New migration 002_server_access.sql: created_by column + server_user table - Server model: ownership, permission checks, team management methods - Routes: /team routes, RoleMiddleware on /servers/create and /admin - ServerController: permission checks via server_user, team CRUD methods - TerminalController: server access checks - DashboardController: filter servers by accessible ones - ApiController: server access checks - Views: team.php, team_server.php, sidebar Team link, hide actions by role - Default user role changed from operator to admin on registration - Admin user form defaults to admin role --- database/migrations/002_server_access.sql | 24 ++ routes/web.php | 12 +- src/Controllers/AdminController.php | 7 +- src/Controllers/ApiController.php | 27 ++ src/Controllers/AuthController.php | 4 +- src/Controllers/DashboardController.php | 7 + src/Controllers/ServerController.php | 336 ++++++++++++++++++---- src/Controllers/TerminalController.php | 10 + src/Models/Server.php | 164 +++++++++++ views/admin/users.php | 4 +- views/dashboard/index.php | 2 + views/layouts/main.php | 6 + views/servers/index.php | 6 + views/servers/show.php | 16 ++ views/servers/team.php | 60 ++++ views/servers/team_server.php | 199 +++++++++++++ 16 files changed, 813 insertions(+), 71 deletions(-) create mode 100644 database/migrations/002_server_access.sql create mode 100644 views/servers/team.php create mode 100644 views/servers/team_server.php diff --git a/database/migrations/002_server_access.sql b/database/migrations/002_server_access.sql new file mode 100644 index 0000000..cb4c054 --- /dev/null +++ b/database/migrations/002_server_access.sql @@ -0,0 +1,24 @@ +-- ============================================================================ +-- ServerManager Server Access Control +-- Adds ownership + team-based permissions per server +-- ============================================================================ + +ALTER TABLE `servers` + ADD COLUMN `created_by` INT UNSIGNED DEFAULT NULL AFTER `group_name`, + ADD KEY `idx_created_by` (`created_by`); + +CREATE TABLE IF NOT EXISTS `server_user` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `server_id` INT UNSIGNED NOT NULL, + `user_id` INT UNSIGNED NOT NULL, + `role` ENUM('viewer', 'manager') NOT NULL DEFAULT 'viewer', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_server_user` (`server_id`, `user_id`), + KEY `idx_server_id` (`server_id`), + KEY `idx_user_id` (`user_id`), + CONSTRAINT `fk_server_user_server` FOREIGN KEY (`server_id`) + REFERENCES `servers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_server_user_user` FOREIGN KEY (`user_id`) + REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/routes/web.php b/routes/web.php index 13892ec..1f0cd54 100755 --- a/routes/web.php +++ b/routes/web.php @@ -31,8 +31,8 @@ $router->get('/profile', [AuthController::class, 'profile'], [AuthMiddleware::cl $router->post('/profile', [AuthController::class, 'updateProfile'], [AuthMiddleware::class, CSRFMiddleware::class]); $router->get('/servers', [ServerController::class, 'index'], [AuthMiddleware::class]); -$router->get('/servers/create', [ServerController::class, 'create'], [AuthMiddleware::class]); -$router->post('/servers/create', [ServerController::class, 'store'], [AuthMiddleware::class, CSRFMiddleware::class]); +$router->get('/servers/create', [ServerController::class, 'create'], [AuthMiddleware::class, RoleMiddleware::class]); +$router->post('/servers/create', [ServerController::class, 'store'], [AuthMiddleware::class, CSRFMiddleware::class, RoleMiddleware::class]); $router->get('/servers/:id', [ServerController::class, 'show'], [AuthMiddleware::class]); $router->get('/servers/:id/edit', [ServerController::class, 'edit'], [AuthMiddleware::class]); $router->post('/servers/:id/edit', [ServerController::class, 'update'], [AuthMiddleware::class, CSRFMiddleware::class]); @@ -65,12 +65,18 @@ $router->get('/servers/:id/logs', [ServerController::class, 'logs'], [AuthMiddle $router->get('/servers/:id/metrics', [ServerController::class, 'metrics'], [AuthMiddleware::class]); $router->get('/sidebar/servers', [ServerController::class, 'sidebarServers'], [AuthMiddleware::class]); +$router->get('/team', [ServerController::class, 'team'], [AuthMiddleware::class]); +$router->get('/team/:id', [ServerController::class, 'teamServer'], [AuthMiddleware::class]); +$router->post('/team/:id/add-user', [ServerController::class, 'addTeamUser'], [AuthMiddleware::class, CSRFMiddleware::class]); +$router->post('/team/:id/remove-user', [ServerController::class, 'removeTeamUser'], [AuthMiddleware::class, CSRFMiddleware::class]); +$router->post('/team/:id/update-role', [ServerController::class, 'updateTeamUserRole'], [AuthMiddleware::class, CSRFMiddleware::class]); + $router->get('/terminal/:id', [TerminalController::class, 'index'], [AuthMiddleware::class]); $router->post('/terminal/:id/execute', [TerminalController::class, 'execute'], [AuthMiddleware::class, CSRFMiddleware::class]); $router->get('/terminal/:id/history', [TerminalController::class, 'history'], [AuthMiddleware::class]); $router->post('/terminal/:id/clear-history', [TerminalController::class, 'clearHistory'], [AuthMiddleware::class, CSRFMiddleware::class]); -$router->group(['prefix' => 'admin', 'middleware' => [AuthMiddleware::class]], function (ServerManager\Core\Router $router) { +$router->group(['prefix' => 'admin', 'middleware' => [AuthMiddleware::class, RoleMiddleware::class]], function (ServerManager\Core\Router $router) { $router->get('/users', [AdminController::class, 'users']); $router->post('/users/create', [AdminController::class, 'createUser'], [CSRFMiddleware::class]); $router->post('/users/:id/update', [AdminController::class, 'updateUser'], [CSRFMiddleware::class]); diff --git a/src/Controllers/AdminController.php b/src/Controllers/AdminController.php index 084dfdb..4eadbc3 100755 --- a/src/Controllers/AdminController.php +++ b/src/Controllers/AdminController.php @@ -62,7 +62,7 @@ class AdminController 'username' => $_POST['username'], 'email' => $_POST['email'], 'password' => $_POST['password'], - 'role' => $_POST['role'], + 'role' => $_POST['role'] ?? 'admin', 'is_active' => 1, ]); @@ -159,6 +159,11 @@ class AdminController public function securitySettings(): void { + if (Session::get('user_role') !== 'super_admin') { + Session::setFlash('error', 'You do not have permission to access this page.'); + $this->view->redirect('/dashboard'); + } + $this->view->display('admin.security', [ 'title' => 'Security Settings - ServerManager', ]); diff --git a/src/Controllers/ApiController.php b/src/Controllers/ApiController.php index 80fa9de..29a95d9 100755 --- a/src/Controllers/ApiController.php +++ b/src/Controllers/ApiController.php @@ -76,6 +76,11 @@ class ApiController $filters = []; if ($search) $filters['search'] = $search; + $accessibleIds = $serverModel->getAccessibleServerIds((int) $user['id']); + if (!empty($accessibleIds)) { + $filters['accessible_ids'] = $accessibleIds; + } + $servers = $serverModel->getAll($page, $perPage, $filters); $this->view->json([ @@ -101,6 +106,10 @@ class ApiController $this->view->json(['error' => 'Server not found'], 404); } + if (!$serverModel->canView($id, (int) $user['id'])) { + $this->view->json(['error' => 'Server not found'], 404); + } + $monitoringService = new MonitoringService(); $metrics = $monitoringService->getLatestMetrics($id); @@ -154,6 +163,10 @@ class ApiController $this->view->json(['error' => 'Server not found'], 404); } + if (!$serverModel->canManage($id, (int) $user['id'])) { + $this->view->json(['error' => 'Forbidden'], 403); + } + $serverModel->update($id, $input); $this->view->json([ @@ -173,6 +186,10 @@ class ApiController $this->view->json(['error' => 'Server not found'], 404); } + if (!$serverModel->canManage($id, (int) $user['id'])) { + $this->view->json(['error' => 'Forbidden'], 403); + } + $serverModel->delete($id); $this->view->json([ @@ -185,6 +202,11 @@ class ApiController { $user = $this->authenticateRequest(); + $serverModel = new Server(); + if (!$serverModel->canView($id, (int) $user['id'])) { + $this->view->json(['error' => 'Server not found'], 404); + } + $monitoringService = new MonitoringService(); $hours = (int) ($_GET['hours'] ?? 24); @@ -202,6 +224,11 @@ class ApiController { $user = $this->authenticateRequest(); + $serverModel = new Server(); + if (!$serverModel->canView($id, (int) $user['id'])) { + $this->view->json(['error' => 'Server not found'], 404); + } + $input = json_decode(file_get_contents('php://input'), true) ?? $_POST; $command = $input['command'] ?? ''; diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php index 85939f4..9239e2b 100755 --- a/src/Controllers/AuthController.php +++ b/src/Controllers/AuthController.php @@ -126,13 +126,13 @@ class AuthController 'username' => $username, 'email' => $email, 'password' => $_POST['password'], - 'role' => 'operator', + 'role' => 'admin', 'is_active' => 1, ]); $this->auditService->log('user_registered', 'user', $id, [ 'username' => $username, - 'role' => 'operator', + 'role' => 'admin', ]); Session::setFlash('success', 'Account created. You can now log in.'); diff --git a/src/Controllers/DashboardController.php b/src/Controllers/DashboardController.php index e096c15..cfa0fcb 100755 --- a/src/Controllers/DashboardController.php +++ b/src/Controllers/DashboardController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace ServerManager\Controllers; use ServerManager\Core\App; +use ServerManager\Core\Session; use ServerManager\Models\Server; use ServerManager\Services\MonitoringService; use ServerManager\Services\AuditService; @@ -28,7 +29,13 @@ class DashboardController $recentActivity = $this->auditService->getRecentActivity(10); $serverModel = new Server(); + $userId = (int) Session::get('user_id'); + $accessibleIds = $serverModel->getAccessibleServerIds($userId); $servers = $serverModel->findAll(true); + $servers = array_filter($servers, function ($s) use ($accessibleIds) { + return in_array((int) $s['id'], $accessibleIds, true); + }); + $servers = array_values($servers); $this->view->display('dashboard.index', [ 'title' => 'Dashboard - ServerManager', diff --git a/src/Controllers/ServerController.php b/src/Controllers/ServerController.php index 3f384a7..ad015a1 100755 --- a/src/Controllers/ServerController.php +++ b/src/Controllers/ServerController.php @@ -8,6 +8,7 @@ use ServerManager\Core\App; use ServerManager\Core\Session; use ServerManager\Core\Validator; use ServerManager\Models\Server; +use ServerManager\Models\User; use ServerManager\Services\SSHService; use ServerManager\Services\MonitoringService; use ServerManager\Services\AuditService; @@ -26,18 +27,47 @@ class ServerController $this->auditService = new AuditService(); } + private function requireServerAccess(int $serverId, string $minRole = 'viewer'): ?array + { + $userId = (int) Session::get('user_id'); + $server = $this->serverModel->findById($serverId); + + if (!$server) { + $this->view->error(404); + } + + $role = $this->serverModel->getUserRole($serverId, $userId); + + if ($role === null) { + $this->view->error(404); + } + + if ($minRole === 'manager' && $role !== 'owner' && $role !== 'manager') { + Session::setFlash('error', 'You do not have permission to perform this action.'); + $this->view->redirect("/servers/{$serverId}"); + } + + return $server; + } + public function index(): void { $page = (int) ($_GET['page'] ?? 1); $search = $_GET['search'] ?? ''; $status = $_GET['status'] ?? ''; $group = $_GET['group'] ?? ''; + $userId = (int) Session::get('user_id'); $filters = []; if ($search) $filters['search'] = $search; if ($status) $filters['status'] = $status; if ($group) $filters['group_name'] = $group; + $accessibleIds = $this->serverModel->getAccessibleServerIds($userId); + if (!empty($accessibleIds)) { + $filters['accessible_ids'] = $accessibleIds; + } + $servers = $this->serverModel->getAll($page, 15, $filters); $groups = $this->serverModel->getGroups(); @@ -107,11 +137,9 @@ class ServerController public function show(int $id): void { - $server = $this->serverModel->findById($id); + $server = $this->requireServerAccess($id, 'viewer'); - if (!$server) { - $this->view->error(404); - } + $role = $this->serverModel->getUserRole($id, (int) Session::get('user_id')); $monitoringService = new MonitoringService(); $latestMetrics = $monitoringService->getLatestMetrics($id); @@ -122,16 +150,13 @@ class ServerController 'server' => $server, 'metrics' => $latestMetrics, 'historicalMetrics' => $historicalMetrics, + 'server_role' => $role, ]); } public function edit(int $id): void { - $server = $this->serverModel->findById($id); - - if (!$server) { - $this->view->error(404); - } + $server = $this->requireServerAccess($id, 'manager'); $groups = $this->serverModel->getGroups(); @@ -144,11 +169,7 @@ class ServerController public function update(int $id): void { - $server = $this->serverModel->findById($id); - - if (!$server) { - $this->view->error(404); - } + $server = $this->requireServerAccess($id, 'manager'); $validator = new Validator(); $rules = [ @@ -194,11 +215,7 @@ class ServerController public function delete(int $id): void { - $server = $this->serverModel->findById($id); - - if (!$server) { - $this->view->error(404); - } + $server = $this->requireServerAccess($id, 'manager'); $this->serverModel->delete($id); @@ -218,6 +235,12 @@ class ServerController $this->view->json(['success' => false, 'message' => 'Server not found'], 404); } + $userId = (int) Session::get('user_id'); + $role = $this->serverModel->getUserRole($id, $userId); + if ($role !== 'owner' && $role !== 'manager') { + $this->view->json(['success' => false, 'message' => 'Forbidden'], 403); + } + $newState = $this->serverModel->toggleActive($id); $this->auditService->log( @@ -236,11 +259,7 @@ class ServerController public function testConnection(int $id): void { - $server = $this->serverModel->findById($id); - - if (!$server) { - $this->view->json(['success' => false, 'message' => 'Server not found'], 404); - } + $server = $this->requireServerAccess($id, 'viewer'); $ssh = new SSHService(); $result = $ssh->testConnection($server); @@ -255,11 +274,7 @@ class ServerController public function processes(int $id): void { - $server = $this->serverModel->findById($id); - - if (!$server) { - $this->view->error(404); - } + $server = $this->requireServerAccess($id, 'viewer'); try { $ssh = new SSHService(); @@ -306,11 +321,7 @@ class ServerController public function systemInfo(int $id): void { - $server = $this->serverModel->findById($id); - - if (!$server) { - $this->view->error(404); - } + $server = $this->requireServerAccess($id, 'viewer'); try { $ssh = new SSHService(); @@ -335,11 +346,7 @@ class ServerController public function logs(int $id): void { - $server = $this->serverModel->findById($id); - - if (!$server) { - $this->view->error(404); - } + $server = $this->requireServerAccess($id, 'viewer'); $logType = $_GET['type'] ?? 'syslog'; $lines = (int) ($_GET['lines'] ?? 100); @@ -393,6 +400,12 @@ class ServerController $this->view->json(['success' => false, 'message' => 'Server not found'], 404); } + $userId = (int) Session::get('user_id'); + $role = $this->serverModel->getUserRole($id, $userId); + if ($role !== 'owner' && $role !== 'manager') { + $this->view->json(['success' => false, 'message' => 'Forbidden'], 403); + } + try { $ssh = new SSHService(); if (!$ssh->connect($server)) { @@ -425,6 +438,12 @@ class ServerController $this->view->json(['success' => false, 'message' => 'Server not found'], 404); } + $userId = (int) Session::get('user_id'); + $role = $this->serverModel->getUserRole($id, $userId); + if ($role !== 'owner' && $role !== 'manager') { + $this->view->json(['success' => false, 'message' => 'Forbidden'], 403); + } + try { $ssh = new SSHService(); if (!$ssh->connect($server)) { @@ -457,6 +476,12 @@ class ServerController $this->view->json(['success' => false, 'message' => 'Server not found'], 404); } + $userId = (int) Session::get('user_id'); + $role = $this->serverModel->getUserRole($id, $userId); + if ($role !== 'owner' && $role !== 'manager') { + $this->view->json(['success' => false, 'message' => 'Forbidden'], 403); + } + $service = $_POST['service'] ?? ''; if (empty($service)) { $this->view->json(['success' => false, 'message' => 'Service name is required']); @@ -501,6 +526,12 @@ class ServerController $this->view->json(['success' => false, 'message' => 'Server not found'], 404); } + $userId = (int) Session::get('user_id'); + $role = $this->serverModel->getUserRole($id, $userId); + if ($role !== 'owner' && $role !== 'manager') { + $this->view->json(['success' => false, 'message' => 'Forbidden'], 403); + } + $pid = (int) ($_POST['pid'] ?? 0); $signal = (int) ($_POST['signal'] ?? 15); @@ -542,11 +573,7 @@ class ServerController public function nginx(int $id): void { - $server = $this->serverModel->findById($id); - - if (!$server) { - $this->view->error(404); - } + $server = $this->requireServerAccess($id, 'viewer'); try { $ssh = new SSHService(); @@ -601,11 +628,7 @@ class ServerController public function nginxGetFile(int $id): void { - $server = $this->serverModel->findById($id); - - if (!$server) { - $this->view->json(['success' => false, 'message' => 'Server not found'], 404); - } + $server = $this->requireServerAccess($id, 'viewer'); $file = $_GET['file'] ?? ''; if (empty($file) || str_contains($file, '..')) { @@ -658,6 +681,12 @@ class ServerController $this->view->json(['success' => false, 'message' => 'Server not found'], 404); } + $userId = (int) Session::get('user_id'); + $role = $this->serverModel->getUserRole($id, $userId); + if ($role !== 'owner' && $role !== 'manager') { + $this->view->json(['success' => false, 'message' => 'Forbidden'], 403); + } + $file = $_POST['file'] ?? ''; $content = $_POST['content'] ?? ''; @@ -712,6 +741,12 @@ class ServerController $this->view->json(['success' => false, 'message' => 'Server not found'], 404); } + $userId = (int) Session::get('user_id'); + $role = $this->serverModel->getUserRole($id, $userId); + if ($role !== 'owner' && $role !== 'manager') { + $this->view->json(['success' => false, 'message' => 'Forbidden'], 403); + } + $site = $_POST['site'] ?? ''; if (empty($site) || str_contains($site, '..') || str_contains($site, '/')) { $this->view->json(['success' => false, 'message' => 'Invalid site name']); @@ -750,6 +785,12 @@ class ServerController $this->view->json(['success' => false, 'message' => 'Server not found'], 404); } + $userId = (int) Session::get('user_id'); + $role = $this->serverModel->getUserRole($id, $userId); + if ($role !== 'owner' && $role !== 'manager') { + $this->view->json(['success' => false, 'message' => 'Forbidden'], 403); + } + $siteName = trim($_POST['site_name'] ?? ''); $siteRoot = trim($_POST['site_root'] ?? ''); $serverName = trim($_POST['server_name'] ?? ''); @@ -836,6 +877,12 @@ class ServerController $this->view->json(['success' => false, 'message' => 'Server not found'], 404); } + $userId = (int) Session::get('user_id'); + $role = $this->serverModel->getUserRole($id, $userId); + if ($role !== 'owner' && $role !== 'manager') { + $this->view->json(['success' => false, 'message' => 'Forbidden'], 403); + } + $site = $_POST['site'] ?? ''; $enable = (bool) ($_POST['enable'] ?? false); @@ -879,6 +926,12 @@ class ServerController $this->view->json(['success' => false, 'message' => 'Server not found'], 404); } + $userId = (int) Session::get('user_id'); + $role = $this->serverModel->getUserRole($id, $userId); + if ($role !== 'owner' && $role !== 'manager') { + $this->view->json(['success' => false, 'message' => 'Forbidden'], 403); + } + $action = $_POST['action'] ?? ''; $allowed = ['restart', 'reload', 'test', 'start', 'stop']; if (!in_array($action, $allowed, true)) { @@ -921,11 +974,7 @@ class ServerController public function ssl(int $id): void { - $server = $this->serverModel->findById($id); - - if (!$server) { - $this->view->error(404); - } + $server = $this->requireServerAccess($id, 'viewer'); try { $ssh = new SSHService(); @@ -994,6 +1043,12 @@ class ServerController $this->view->json(['success' => false, 'message' => 'Server not found'], 404); } + $userId = (int) Session::get('user_id'); + $role = $this->serverModel->getUserRole($id, $userId); + if ($role !== 'owner' && $role !== 'manager') { + $this->view->json(['success' => false, 'message' => 'Forbidden'], 403); + } + $action = $_POST['action'] ?? ''; $allowed = ['install', 'request', 'renew', 'delete']; if (!in_array($action, $allowed, true)) { @@ -1086,11 +1141,7 @@ class ServerController public function database(int $id): void { - $server = $this->serverModel->findById($id); - - if (!$server) { - $this->view->error(404); - } + $server = $this->requireServerAccess($id, 'viewer'); try { $ssh = new SSHService(); @@ -1198,6 +1249,12 @@ class ServerController $this->view->json(['success' => false, 'message' => 'Server not found'], 404); } + $userId = (int) Session::get('user_id'); + $role = $this->serverModel->getUserRole($id, $userId); + if ($role !== 'owner' && $role !== 'manager') { + $this->view->json(['success' => false, 'message' => 'Forbidden'], 403); + } + $action = $_POST['action'] ?? ''; $allowed = ['query', 'tables', 'structure', 'browse', 'users', 'create_user', 'grant', 'revoke', 'drop_user', 'change_pass']; if (!in_array($action, $allowed, true)) { @@ -1462,9 +1519,15 @@ class ServerController public function sidebarServers(): void { + $userId = (int) Session::get('user_id'); + $accessibleIds = $this->serverModel->getAccessibleServerIds($userId); + $servers = $this->serverModel->findAll(); $result = []; foreach ($servers as $s) { + if (!in_array((int) $s['id'], $accessibleIds, true)) { + continue; + } $result[] = [ 'id' => $s['id'], 'name' => $s['name'], @@ -1477,8 +1540,7 @@ class ServerController public function services(int $id): void { - $server = $this->serverModel->findById($id); - if (!$server) { $this->view->error(404); } + $server = $this->requireServerAccess($id, 'viewer'); try { $ssh = new SSHService(); @@ -1527,6 +1589,12 @@ class ServerController $this->view->json(['success' => false, 'message' => 'Server not found'], 404); } + $userId = (int) Session::get('user_id'); + $role = $this->serverModel->getUserRole($id, $userId); + if ($role !== 'owner' && $role !== 'manager') { + $this->view->json(['success' => false, 'message' => 'Forbidden'], 403); + } + $action = $_POST['action'] ?? ''; $service = $_POST['service'] ?? ''; $allowed = ['start', 'stop', 'restart', 'reload', 'enable', 'disable']; @@ -1558,8 +1626,7 @@ class ServerController public function systemUsers(int $id): void { - $server = $this->serverModel->findById($id); - if (!$server) { $this->view->error(404); } + $server = $this->requireServerAccess($id, 'viewer'); try { $ssh = new SSHService(); @@ -1614,6 +1681,12 @@ class ServerController $this->view->json(['success' => false, 'message' => 'Server not found'], 404); } + $userId = (int) Session::get('user_id'); + $role = $this->serverModel->getUserRole($id, $userId); + if ($role !== 'owner' && $role !== 'manager') { + $this->view->json(['success' => false, 'message' => 'Forbidden'], 403); + } + $action = $_POST['action'] ?? ''; $allowed = ['add', 'delete', 'lock', 'unlock', 'addgroup']; if (!in_array($action, $allowed, true)) { @@ -1723,4 +1796,141 @@ class ServerController 'data' => $metrics, ]); } + + public function team(): void + { + $userId = (int) Session::get('user_id'); + $servers = $this->serverModel->getOwnedServers($userId); + + $result = []; + foreach ($servers as $server) { + $memberCount = count($this->serverModel->getTeam((int) $server['id'])); + $result[] = [ + 'id' => $server['id'], + 'name' => $server['name'], + 'ip_address' => $server['ip_address'], + 'member_count' => $memberCount, + 'status' => $server['current_status'] ?? 'unknown', + ]; + } + + $this->view->display('servers.team', [ + 'title' => 'Team Management - ServerManager', + 'servers' => $result, + ]); + } + + public function teamServer(int $id): void + { + $userId = (int) Session::get('user_id'); + $server = $this->serverModel->findById($id); + + if (!$server || (int) $server['created_by'] !== $userId) { + $this->view->error(404); + } + + $members = $this->serverModel->getTeam($id); + + $userModel = new User(); + $users = $userModel->getAll(1, 999); + $availableUsers = []; + $memberIds = array_map(fn($m) => $m['user_id'], $members); + foreach ($users['data'] as $u) { + if (!in_array((int) $u['id'], $memberIds, true)) { + $availableUsers[] = $u; + } + } + + $this->view->display('servers.team_server', [ + 'title' => 'Team - ' . $server['name'] . ' - ServerManager', + 'server' => $server, + 'members' => $members, + 'availableUsers' => $availableUsers, + ]); + } + + public function addTeamUser(int $id): void + { + $userId = (int) Session::get('user_id'); + $server = $this->serverModel->findById($id); + + if (!$server || (int) $server['created_by'] !== $userId) { + $this->view->json(['success' => false, 'message' => 'Server not found'], 404); + } + + $targetUserId = (int) ($_POST['user_id'] ?? 0); + $role = $_POST['role'] ?? 'viewer'; + + if ($targetUserId <= 0) { + $this->view->json(['success' => false, 'message' => 'Invalid user']); + } + + if (!in_array($role, ['viewer', 'manager'], true)) { + $this->view->json(['success' => false, 'message' => 'Invalid role']); + } + + if ($this->serverModel->addUser($id, $targetUserId, $role)) { + $this->auditService->log('team_user_added', 'server', $id, [ + 'target_user_id' => $targetUserId, + 'role' => $role, + ]); + $this->view->json(['success' => true, 'message' => 'User added to server.']); + } + + $this->view->json(['success' => false, 'message' => 'User is already assigned to this server.']); + } + + public function removeTeamUser(int $id): void + { + $userId = (int) Session::get('user_id'); + $server = $this->serverModel->findById($id); + + if (!$server || (int) $server['created_by'] !== $userId) { + $this->view->json(['success' => false, 'message' => 'Server not found'], 404); + } + + $targetUserId = (int) ($_POST['user_id'] ?? 0); + + if ($targetUserId <= 0) { + $this->view->json(['success' => false, 'message' => 'Invalid user']); + } + + $this->serverModel->removeUser($id, $targetUserId); + + $this->auditService->log('team_user_removed', 'server', $id, [ + 'target_user_id' => $targetUserId, + ]); + + $this->view->json(['success' => true, 'message' => 'User removed from server.']); + } + + public function updateTeamUserRole(int $id): void + { + $userId = (int) Session::get('user_id'); + $server = $this->serverModel->findById($id); + + if (!$server || (int) $server['created_by'] !== $userId) { + $this->view->json(['success' => false, 'message' => 'Server not found'], 404); + } + + $targetUserId = (int) ($_POST['user_id'] ?? 0); + $role = $_POST['role'] ?? 'viewer'; + + if ($targetUserId <= 0) { + $this->view->json(['success' => false, 'message' => 'Invalid user']); + } + + if (!in_array($role, ['viewer', 'manager'], true)) { + $this->view->json(['success' => false, 'message' => 'Invalid role']); + } + + $this->serverModel->updateUserRole($id, $targetUserId, $role); + + $this->auditService->log('team_user_role_updated', 'server', $id, [ + 'target_user_id' => $targetUserId, + 'role' => $role, + ]); + + $this->view->json(['success' => true, 'message' => 'User role updated.']); + } } diff --git a/src/Controllers/TerminalController.php b/src/Controllers/TerminalController.php index 8c7884c..668bfbf 100755 --- a/src/Controllers/TerminalController.php +++ b/src/Controllers/TerminalController.php @@ -32,6 +32,11 @@ class TerminalController $this->view->error(404); } + $userId = (int) Session::get('user_id'); + if (!$this->serverModel->canView($id, $userId)) { + $this->view->error(404); + } + $commandHistory = new CommandHistory(); $history = $commandHistory->getByServer($id, 50); @@ -50,6 +55,11 @@ class TerminalController $this->view->json(['success' => false, 'message' => 'Server not found'], 404); } + $userId = (int) Session::get('user_id'); + if (!$this->serverModel->canView($id, $userId)) { + $this->view->json(['success' => false, 'message' => 'Forbidden'], 403); + } + $command = $_POST['command'] ?? ''; if (empty($command)) { diff --git a/src/Models/Server.php b/src/Models/Server.php index e292b2d..6ebca72 100755 --- a/src/Models/Server.php +++ b/src/Models/Server.php @@ -7,6 +7,7 @@ namespace ServerManager\Models; use ServerManager\Core\Database; use ServerManager\Core\App; use ServerManager\Core\Encryption; +use ServerManager\Core\Session; class Server { @@ -74,6 +75,13 @@ class Server $params[] = $filters['status']; } + if (!empty($filters['accessible_ids'])) { + $ids = array_map('intval', $filters['accessible_ids']); + $placeholders = implode(',', array_fill(0, count($ids), '?')); + $where[] = "s.id IN ({$placeholders})"; + $params = array_merge($params, $ids); + } + $whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; $offset = ($page - 1) * $perPage; @@ -121,6 +129,11 @@ class Server $data['current_status'] = 'unknown'; } + $userId = Session::get('user_id'); + if ($userId && empty($data['created_by'])) { + $data['created_by'] = $userId; + } + return $this->db->insert('servers', $data); } @@ -186,4 +199,155 @@ class Server ); return (int) ($result['total'] ?? 0); } + + public function getUserRole(int $serverId, int $userId): ?string + { + $server = $this->db->fetch('SELECT created_by FROM servers WHERE id = ?', [$serverId]); + if (!$server) { + return null; + } + + if ((int) $server['created_by'] === $userId) { + return 'owner'; + } + + $assignment = $this->db->fetch( + 'SELECT role FROM server_user WHERE server_id = ? AND user_id = ?', + [$serverId, $userId] + ); + + return $assignment ? $assignment['role'] : null; + } + + public function getAccessibleServerIds(int $userId): array + { + $owned = $this->db->fetchAll( + 'SELECT id FROM servers WHERE created_by = ?', + [$userId] + ); + + $assigned = $this->db->fetchAll( + 'SELECT server_id FROM server_user WHERE user_id = ?', + [$userId] + ); + + $ids = []; + foreach ($owned as $row) { + $ids[] = (int) $row['id']; + } + foreach ($assigned as $row) { + $ids[] = (int) $row['server_id']; + } + + return array_unique($ids); + } + + public function isOwner(int $serverId, int $userId): bool + { + $server = $this->db->fetch('SELECT created_by FROM servers WHERE id = ?', [$serverId]); + return $server && (int) $server['created_by'] === $userId; + } + + public function canManage(int $serverId, int $userId): bool + { + $role = $this->getUserRole($serverId, $userId); + return $role === 'owner' || $role === 'manager'; + } + + public function canView(int $serverId, int $userId): bool + { + return $this->getUserRole($serverId, $userId) !== null; + } + + public function getTeam(int $serverId): array + { + $members = $this->db->fetchAll( + 'SELECT su.*, u.username, u.email + FROM server_user su + JOIN users u ON su.user_id = u.id + WHERE su.server_id = ? + ORDER BY su.created_at ASC', + [$serverId] + ); + + $owner = $this->db->fetch( + 'SELECT u.id, u.username, u.email + FROM servers s + JOIN users u ON s.created_by = u.id + WHERE s.id = ?', + [$serverId] + ); + + $result = []; + if ($owner) { + $result[] = [ + 'user_id' => (int) $owner['id'], + 'username' => $owner['username'], + 'email' => $owner['email'], + 'role' => 'owner', + ]; + } + foreach ($members as $m) { + $result[] = [ + 'user_id' => (int) $m['user_id'], + 'username' => $m['username'], + 'email' => $m['email'], + 'role' => $m['role'], + ]; + } + return $result; + } + + public function addUser(int $serverId, int $userId, string $role): bool + { + if (!in_array($role, ['viewer', 'manager'], true)) { + return false; + } + + $existing = $this->db->fetch( + 'SELECT id FROM server_user WHERE server_id = ? AND user_id = ?', + [$serverId, $userId] + ); + if ($existing) { + return false; + } + + $this->db->insert('server_user', [ + 'server_id' => $serverId, + 'user_id' => $userId, + 'role' => $role, + ]); + return true; + } + + public function removeUser(int $serverId, int $userId): bool + { + return $this->db->delete( + 'server_user', + 'server_id = ? AND user_id = ?', + [$serverId, $userId] + ) > 0; + } + + public function updateUserRole(int $serverId, int $userId, string $role): bool + { + if (!in_array($role, ['viewer', 'manager'], true)) { + return false; + } + + return $this->db->update( + 'server_user', + ['role' => $role], + 'server_id = ? AND user_id = ?', + [$serverId, $userId] + ) > 0; + } + + public function getOwnedServers(int $userId): array + { + return $this->db->fetchAll( + 'SELECT * FROM servers WHERE created_by = ? ORDER BY name ASC', + [$userId] + ); + } } diff --git a/views/admin/users.php b/views/admin/users.php index 9e6cd27..d2cf2a3 100755 --- a/views/admin/users.php +++ b/views/admin/users.php @@ -100,8 +100,8 @@
@@ -130,7 +130,7 @@ function showCreateUserModal() { document.getElementById('modalPassword').value = ''; document.getElementById('modalPassword').required = true; document.getElementById('passwordHint').style.display = 'block'; - document.getElementById('modalRole').value = 'operator'; + document.getElementById('modalRole').value = 'admin'; document.getElementById('modalActive').checked = true; document.getElementById('saveUserBtn').textContent = 'Create'; document.getElementById('userModal').style.display = 'flex'; diff --git a/views/dashboard/index.php b/views/dashboard/index.php index 660f904..d1fed38 100755 --- a/views/dashboard/index.php +++ b/views/dashboard/index.php @@ -65,7 +65,9 @@ $recentActivity = $recentActivity ?? [];

No servers configured yet.

+ Add Server +
diff --git a/views/layouts/main.php b/views/layouts/main.php index 48824e1..502b48e 100755 --- a/views/layouts/main.php +++ b/views/layouts/main.php @@ -39,6 +39,12 @@
+ diff --git a/views/servers/index.php b/views/servers/index.php index 85343f5..a6eb12f 100755 --- a/views/servers/index.php +++ b/views/servers/index.php @@ -9,9 +9,11 @@ $groups = $groups ?? [];

Manage your Linux servers

+ Add Server +
@@ -45,7 +47,9 @@ $groups = $groups ?? [];

No servers found.

+ Add Your First Server +
@@ -106,6 +110,7 @@ $groups = $groups ?? []; + @@ -113,6 +118,7 @@ $groups = $groups ?? []; onclick="deleteServer(, '')"> +
diff --git a/views/servers/show.php b/views/servers/show.php index 4d8fd96..1f4d995 100755 --- a/views/servers/show.php +++ b/views/servers/show.php @@ -2,6 +2,8 @@ $server = $server ?? []; $metrics = $metrics ?? null; $historicalMetrics = $historicalMetrics ?? []; +$server_role = $server_role ?? null; +$canManage = $server_role === 'owner' || $server_role === 'manager'; ?>
+ + + + + + + + Terminal + Edit + +
+
@@ -154,6 +168,7 @@ $historicalMetrics = $historicalMetrics ?? []; System Logs + @@ -180,6 +195,7 @@ $historicalMetrics = $historicalMetrics ?? []; Restart Service + diff --git a/views/servers/team.php b/views/servers/team.php new file mode 100644 index 0000000..4cd1894 --- /dev/null +++ b/views/servers/team.php @@ -0,0 +1,60 @@ + + + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + +
ServerIP AddressStatusTeam MembersActions
+ + + + + + + + + members + + + Manage Team + +
+
+ +
+
diff --git a/views/servers/team_server.php b/views/servers/team_server.php new file mode 100644 index 0000000..228cc03 --- /dev/null +++ b/views/servers/team_server.php @@ -0,0 +1,199 @@ + + + + +
+
+
+

Team Members

+
+
+ +
+ +

No team members yet.

+
+ +
+ + + + + + + + + + + + + + + + + +
UserRoleActions
+ +
+
+ + Owner + + + + + + + +
+
+ +
+
+ +
+
+

Add User

+
+
+ +
+ +

All users already have access to this server.

+
+ +
+ + +
+ + +
+ +
+ + +
+ + +
+ +
+
+
+ + + +