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
This commit is contained in:
2026-06-07 06:49:12 -04:00
parent 9e11f767e7
commit 5f5de248c6
16 changed files with 813 additions and 71 deletions

View File

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

View File

@@ -31,8 +31,8 @@ $router->get('/profile', [AuthController::class, 'profile'], [AuthMiddleware::cl
$router->post('/profile', [AuthController::class, 'updateProfile'], [AuthMiddleware::class, CSRFMiddleware::class]); $router->post('/profile', [AuthController::class, 'updateProfile'], [AuthMiddleware::class, CSRFMiddleware::class]);
$router->get('/servers', [ServerController::class, 'index'], [AuthMiddleware::class]); $router->get('/servers', [ServerController::class, 'index'], [AuthMiddleware::class]);
$router->get('/servers/create', [ServerController::class, 'create'], [AuthMiddleware::class]); $router->get('/servers/create', [ServerController::class, 'create'], [AuthMiddleware::class, RoleMiddleware::class]);
$router->post('/servers/create', [ServerController::class, 'store'], [AuthMiddleware::class, CSRFMiddleware::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', [ServerController::class, 'show'], [AuthMiddleware::class]);
$router->get('/servers/:id/edit', [ServerController::class, 'edit'], [AuthMiddleware::class]); $router->get('/servers/:id/edit', [ServerController::class, 'edit'], [AuthMiddleware::class]);
$router->post('/servers/:id/edit', [ServerController::class, 'update'], [AuthMiddleware::class, CSRFMiddleware::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('/servers/:id/metrics', [ServerController::class, 'metrics'], [AuthMiddleware::class]);
$router->get('/sidebar/servers', [ServerController::class, 'sidebarServers'], [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->get('/terminal/:id', [TerminalController::class, 'index'], [AuthMiddleware::class]);
$router->post('/terminal/:id/execute', [TerminalController::class, 'execute'], [AuthMiddleware::class, CSRFMiddleware::class]); $router->post('/terminal/:id/execute', [TerminalController::class, 'execute'], [AuthMiddleware::class, CSRFMiddleware::class]);
$router->get('/terminal/:id/history', [TerminalController::class, 'history'], [AuthMiddleware::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->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->get('/users', [AdminController::class, 'users']);
$router->post('/users/create', [AdminController::class, 'createUser'], [CSRFMiddleware::class]); $router->post('/users/create', [AdminController::class, 'createUser'], [CSRFMiddleware::class]);
$router->post('/users/:id/update', [AdminController::class, 'updateUser'], [CSRFMiddleware::class]); $router->post('/users/:id/update', [AdminController::class, 'updateUser'], [CSRFMiddleware::class]);

View File

@@ -62,7 +62,7 @@ class AdminController
'username' => $_POST['username'], 'username' => $_POST['username'],
'email' => $_POST['email'], 'email' => $_POST['email'],
'password' => $_POST['password'], 'password' => $_POST['password'],
'role' => $_POST['role'], 'role' => $_POST['role'] ?? 'admin',
'is_active' => 1, 'is_active' => 1,
]); ]);
@@ -159,6 +159,11 @@ class AdminController
public function securitySettings(): void 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', [ $this->view->display('admin.security', [
'title' => 'Security Settings - ServerManager', 'title' => 'Security Settings - ServerManager',
]); ]);

View File

@@ -76,6 +76,11 @@ class ApiController
$filters = []; $filters = [];
if ($search) $filters['search'] = $search; if ($search) $filters['search'] = $search;
$accessibleIds = $serverModel->getAccessibleServerIds((int) $user['id']);
if (!empty($accessibleIds)) {
$filters['accessible_ids'] = $accessibleIds;
}
$servers = $serverModel->getAll($page, $perPage, $filters); $servers = $serverModel->getAll($page, $perPage, $filters);
$this->view->json([ $this->view->json([
@@ -101,6 +106,10 @@ class ApiController
$this->view->json(['error' => 'Server not found'], 404); $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(); $monitoringService = new MonitoringService();
$metrics = $monitoringService->getLatestMetrics($id); $metrics = $monitoringService->getLatestMetrics($id);
@@ -154,6 +163,10 @@ class ApiController
$this->view->json(['error' => 'Server not found'], 404); $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); $serverModel->update($id, $input);
$this->view->json([ $this->view->json([
@@ -173,6 +186,10 @@ class ApiController
$this->view->json(['error' => 'Server not found'], 404); $this->view->json(['error' => 'Server not found'], 404);
} }
if (!$serverModel->canManage($id, (int) $user['id'])) {
$this->view->json(['error' => 'Forbidden'], 403);
}
$serverModel->delete($id); $serverModel->delete($id);
$this->view->json([ $this->view->json([
@@ -185,6 +202,11 @@ class ApiController
{ {
$user = $this->authenticateRequest(); $user = $this->authenticateRequest();
$serverModel = new Server();
if (!$serverModel->canView($id, (int) $user['id'])) {
$this->view->json(['error' => 'Server not found'], 404);
}
$monitoringService = new MonitoringService(); $monitoringService = new MonitoringService();
$hours = (int) ($_GET['hours'] ?? 24); $hours = (int) ($_GET['hours'] ?? 24);
@@ -202,6 +224,11 @@ class ApiController
{ {
$user = $this->authenticateRequest(); $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; $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$command = $input['command'] ?? ''; $command = $input['command'] ?? '';

View File

@@ -126,13 +126,13 @@ class AuthController
'username' => $username, 'username' => $username,
'email' => $email, 'email' => $email,
'password' => $_POST['password'], 'password' => $_POST['password'],
'role' => 'operator', 'role' => 'admin',
'is_active' => 1, 'is_active' => 1,
]); ]);
$this->auditService->log('user_registered', 'user', $id, [ $this->auditService->log('user_registered', 'user', $id, [
'username' => $username, 'username' => $username,
'role' => 'operator', 'role' => 'admin',
]); ]);
Session::setFlash('success', 'Account created. You can now log in.'); Session::setFlash('success', 'Account created. You can now log in.');

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ServerManager\Controllers; namespace ServerManager\Controllers;
use ServerManager\Core\App; use ServerManager\Core\App;
use ServerManager\Core\Session;
use ServerManager\Models\Server; use ServerManager\Models\Server;
use ServerManager\Services\MonitoringService; use ServerManager\Services\MonitoringService;
use ServerManager\Services\AuditService; use ServerManager\Services\AuditService;
@@ -28,7 +29,13 @@ class DashboardController
$recentActivity = $this->auditService->getRecentActivity(10); $recentActivity = $this->auditService->getRecentActivity(10);
$serverModel = new Server(); $serverModel = new Server();
$userId = (int) Session::get('user_id');
$accessibleIds = $serverModel->getAccessibleServerIds($userId);
$servers = $serverModel->findAll(true); $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', [ $this->view->display('dashboard.index', [
'title' => 'Dashboard - ServerManager', 'title' => 'Dashboard - ServerManager',

View File

@@ -8,6 +8,7 @@ use ServerManager\Core\App;
use ServerManager\Core\Session; use ServerManager\Core\Session;
use ServerManager\Core\Validator; use ServerManager\Core\Validator;
use ServerManager\Models\Server; use ServerManager\Models\Server;
use ServerManager\Models\User;
use ServerManager\Services\SSHService; use ServerManager\Services\SSHService;
use ServerManager\Services\MonitoringService; use ServerManager\Services\MonitoringService;
use ServerManager\Services\AuditService; use ServerManager\Services\AuditService;
@@ -26,18 +27,47 @@ class ServerController
$this->auditService = new AuditService(); $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 public function index(): void
{ {
$page = (int) ($_GET['page'] ?? 1); $page = (int) ($_GET['page'] ?? 1);
$search = $_GET['search'] ?? ''; $search = $_GET['search'] ?? '';
$status = $_GET['status'] ?? ''; $status = $_GET['status'] ?? '';
$group = $_GET['group'] ?? ''; $group = $_GET['group'] ?? '';
$userId = (int) Session::get('user_id');
$filters = []; $filters = [];
if ($search) $filters['search'] = $search; if ($search) $filters['search'] = $search;
if ($status) $filters['status'] = $status; if ($status) $filters['status'] = $status;
if ($group) $filters['group_name'] = $group; 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); $servers = $this->serverModel->getAll($page, 15, $filters);
$groups = $this->serverModel->getGroups(); $groups = $this->serverModel->getGroups();
@@ -107,11 +137,9 @@ class ServerController
public function show(int $id): void public function show(int $id): void
{ {
$server = $this->serverModel->findById($id); $server = $this->requireServerAccess($id, 'viewer');
if (!$server) { $role = $this->serverModel->getUserRole($id, (int) Session::get('user_id'));
$this->view->error(404);
}
$monitoringService = new MonitoringService(); $monitoringService = new MonitoringService();
$latestMetrics = $monitoringService->getLatestMetrics($id); $latestMetrics = $monitoringService->getLatestMetrics($id);
@@ -122,16 +150,13 @@ class ServerController
'server' => $server, 'server' => $server,
'metrics' => $latestMetrics, 'metrics' => $latestMetrics,
'historicalMetrics' => $historicalMetrics, 'historicalMetrics' => $historicalMetrics,
'server_role' => $role,
]); ]);
} }
public function edit(int $id): void public function edit(int $id): void
{ {
$server = $this->serverModel->findById($id); $server = $this->requireServerAccess($id, 'manager');
if (!$server) {
$this->view->error(404);
}
$groups = $this->serverModel->getGroups(); $groups = $this->serverModel->getGroups();
@@ -144,11 +169,7 @@ class ServerController
public function update(int $id): void public function update(int $id): void
{ {
$server = $this->serverModel->findById($id); $server = $this->requireServerAccess($id, 'manager');
if (!$server) {
$this->view->error(404);
}
$validator = new Validator(); $validator = new Validator();
$rules = [ $rules = [
@@ -194,11 +215,7 @@ class ServerController
public function delete(int $id): void public function delete(int $id): void
{ {
$server = $this->serverModel->findById($id); $server = $this->requireServerAccess($id, 'manager');
if (!$server) {
$this->view->error(404);
}
$this->serverModel->delete($id); $this->serverModel->delete($id);
@@ -218,6 +235,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404); $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); $newState = $this->serverModel->toggleActive($id);
$this->auditService->log( $this->auditService->log(
@@ -236,11 +259,7 @@ class ServerController
public function testConnection(int $id): void public function testConnection(int $id): void
{ {
$server = $this->serverModel->findById($id); $server = $this->requireServerAccess($id, 'viewer');
if (!$server) {
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$ssh = new SSHService(); $ssh = new SSHService();
$result = $ssh->testConnection($server); $result = $ssh->testConnection($server);
@@ -255,11 +274,7 @@ class ServerController
public function processes(int $id): void public function processes(int $id): void
{ {
$server = $this->serverModel->findById($id); $server = $this->requireServerAccess($id, 'viewer');
if (!$server) {
$this->view->error(404);
}
try { try {
$ssh = new SSHService(); $ssh = new SSHService();
@@ -306,11 +321,7 @@ class ServerController
public function systemInfo(int $id): void public function systemInfo(int $id): void
{ {
$server = $this->serverModel->findById($id); $server = $this->requireServerAccess($id, 'viewer');
if (!$server) {
$this->view->error(404);
}
try { try {
$ssh = new SSHService(); $ssh = new SSHService();
@@ -335,11 +346,7 @@ class ServerController
public function logs(int $id): void public function logs(int $id): void
{ {
$server = $this->serverModel->findById($id); $server = $this->requireServerAccess($id, 'viewer');
if (!$server) {
$this->view->error(404);
}
$logType = $_GET['type'] ?? 'syslog'; $logType = $_GET['type'] ?? 'syslog';
$lines = (int) ($_GET['lines'] ?? 100); $lines = (int) ($_GET['lines'] ?? 100);
@@ -393,6 +400,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404); $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 { try {
$ssh = new SSHService(); $ssh = new SSHService();
if (!$ssh->connect($server)) { if (!$ssh->connect($server)) {
@@ -425,6 +438,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404); $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 { try {
$ssh = new SSHService(); $ssh = new SSHService();
if (!$ssh->connect($server)) { if (!$ssh->connect($server)) {
@@ -457,6 +476,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404); $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'] ?? ''; $service = $_POST['service'] ?? '';
if (empty($service)) { if (empty($service)) {
$this->view->json(['success' => false, 'message' => 'Service name is required']); $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); $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); $pid = (int) ($_POST['pid'] ?? 0);
$signal = (int) ($_POST['signal'] ?? 15); $signal = (int) ($_POST['signal'] ?? 15);
@@ -542,11 +573,7 @@ class ServerController
public function nginx(int $id): void public function nginx(int $id): void
{ {
$server = $this->serverModel->findById($id); $server = $this->requireServerAccess($id, 'viewer');
if (!$server) {
$this->view->error(404);
}
try { try {
$ssh = new SSHService(); $ssh = new SSHService();
@@ -601,11 +628,7 @@ class ServerController
public function nginxGetFile(int $id): void public function nginxGetFile(int $id): void
{ {
$server = $this->serverModel->findById($id); $server = $this->requireServerAccess($id, 'viewer');
if (!$server) {
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$file = $_GET['file'] ?? ''; $file = $_GET['file'] ?? '';
if (empty($file) || str_contains($file, '..')) { if (empty($file) || str_contains($file, '..')) {
@@ -658,6 +681,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404); $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'] ?? ''; $file = $_POST['file'] ?? '';
$content = $_POST['content'] ?? ''; $content = $_POST['content'] ?? '';
@@ -712,6 +741,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404); $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'] ?? ''; $site = $_POST['site'] ?? '';
if (empty($site) || str_contains($site, '..') || str_contains($site, '/')) { if (empty($site) || str_contains($site, '..') || str_contains($site, '/')) {
$this->view->json(['success' => false, 'message' => 'Invalid site name']); $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); $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'] ?? ''); $siteName = trim($_POST['site_name'] ?? '');
$siteRoot = trim($_POST['site_root'] ?? ''); $siteRoot = trim($_POST['site_root'] ?? '');
$serverName = trim($_POST['server_name'] ?? ''); $serverName = trim($_POST['server_name'] ?? '');
@@ -836,6 +877,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404); $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'] ?? ''; $site = $_POST['site'] ?? '';
$enable = (bool) ($_POST['enable'] ?? false); $enable = (bool) ($_POST['enable'] ?? false);
@@ -879,6 +926,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404); $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'] ?? ''; $action = $_POST['action'] ?? '';
$allowed = ['restart', 'reload', 'test', 'start', 'stop']; $allowed = ['restart', 'reload', 'test', 'start', 'stop'];
if (!in_array($action, $allowed, true)) { if (!in_array($action, $allowed, true)) {
@@ -921,11 +974,7 @@ class ServerController
public function ssl(int $id): void public function ssl(int $id): void
{ {
$server = $this->serverModel->findById($id); $server = $this->requireServerAccess($id, 'viewer');
if (!$server) {
$this->view->error(404);
}
try { try {
$ssh = new SSHService(); $ssh = new SSHService();
@@ -994,6 +1043,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404); $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'] ?? ''; $action = $_POST['action'] ?? '';
$allowed = ['install', 'request', 'renew', 'delete']; $allowed = ['install', 'request', 'renew', 'delete'];
if (!in_array($action, $allowed, true)) { if (!in_array($action, $allowed, true)) {
@@ -1086,11 +1141,7 @@ class ServerController
public function database(int $id): void public function database(int $id): void
{ {
$server = $this->serverModel->findById($id); $server = $this->requireServerAccess($id, 'viewer');
if (!$server) {
$this->view->error(404);
}
try { try {
$ssh = new SSHService(); $ssh = new SSHService();
@@ -1198,6 +1249,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404); $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'] ?? ''; $action = $_POST['action'] ?? '';
$allowed = ['query', 'tables', 'structure', 'browse', 'users', 'create_user', 'grant', 'revoke', 'drop_user', 'change_pass']; $allowed = ['query', 'tables', 'structure', 'browse', 'users', 'create_user', 'grant', 'revoke', 'drop_user', 'change_pass'];
if (!in_array($action, $allowed, true)) { if (!in_array($action, $allowed, true)) {
@@ -1462,9 +1519,15 @@ class ServerController
public function sidebarServers(): void public function sidebarServers(): void
{ {
$userId = (int) Session::get('user_id');
$accessibleIds = $this->serverModel->getAccessibleServerIds($userId);
$servers = $this->serverModel->findAll(); $servers = $this->serverModel->findAll();
$result = []; $result = [];
foreach ($servers as $s) { foreach ($servers as $s) {
if (!in_array((int) $s['id'], $accessibleIds, true)) {
continue;
}
$result[] = [ $result[] = [
'id' => $s['id'], 'id' => $s['id'],
'name' => $s['name'], 'name' => $s['name'],
@@ -1477,8 +1540,7 @@ class ServerController
public function services(int $id): void public function services(int $id): void
{ {
$server = $this->serverModel->findById($id); $server = $this->requireServerAccess($id, 'viewer');
if (!$server) { $this->view->error(404); }
try { try {
$ssh = new SSHService(); $ssh = new SSHService();
@@ -1527,6 +1589,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404); $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'] ?? ''; $action = $_POST['action'] ?? '';
$service = $_POST['service'] ?? ''; $service = $_POST['service'] ?? '';
$allowed = ['start', 'stop', 'restart', 'reload', 'enable', 'disable']; $allowed = ['start', 'stop', 'restart', 'reload', 'enable', 'disable'];
@@ -1558,8 +1626,7 @@ class ServerController
public function systemUsers(int $id): void public function systemUsers(int $id): void
{ {
$server = $this->serverModel->findById($id); $server = $this->requireServerAccess($id, 'viewer');
if (!$server) { $this->view->error(404); }
try { try {
$ssh = new SSHService(); $ssh = new SSHService();
@@ -1614,6 +1681,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404); $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'] ?? ''; $action = $_POST['action'] ?? '';
$allowed = ['add', 'delete', 'lock', 'unlock', 'addgroup']; $allowed = ['add', 'delete', 'lock', 'unlock', 'addgroup'];
if (!in_array($action, $allowed, true)) { if (!in_array($action, $allowed, true)) {
@@ -1723,4 +1796,141 @@ class ServerController
'data' => $metrics, '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.']);
}
} }

View File

@@ -32,6 +32,11 @@ class TerminalController
$this->view->error(404); $this->view->error(404);
} }
$userId = (int) Session::get('user_id');
if (!$this->serverModel->canView($id, $userId)) {
$this->view->error(404);
}
$commandHistory = new CommandHistory(); $commandHistory = new CommandHistory();
$history = $commandHistory->getByServer($id, 50); $history = $commandHistory->getByServer($id, 50);
@@ -50,6 +55,11 @@ class TerminalController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404); $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'] ?? ''; $command = $_POST['command'] ?? '';
if (empty($command)) { if (empty($command)) {

View File

@@ -7,6 +7,7 @@ namespace ServerManager\Models;
use ServerManager\Core\Database; use ServerManager\Core\Database;
use ServerManager\Core\App; use ServerManager\Core\App;
use ServerManager\Core\Encryption; use ServerManager\Core\Encryption;
use ServerManager\Core\Session;
class Server class Server
{ {
@@ -74,6 +75,13 @@ class Server
$params[] = $filters['status']; $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) : ''; $whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
$offset = ($page - 1) * $perPage; $offset = ($page - 1) * $perPage;
@@ -121,6 +129,11 @@ class Server
$data['current_status'] = 'unknown'; $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); return $this->db->insert('servers', $data);
} }
@@ -186,4 +199,155 @@ class Server
); );
return (int) ($result['total'] ?? 0); 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]
);
}
} }

View File

@@ -100,8 +100,8 @@
<div class="form-group"> <div class="form-group">
<label for="modalRole">Role</label> <label for="modalRole">Role</label>
<select id="modalRole" name="role" class="form-select" required> <select id="modalRole" name="role" class="form-select" required>
<option value="admin" selected>Administrator</option>
<option value="operator">Operator</option> <option value="operator">Operator</option>
<option value="admin">Administrator</option>
<option value="super_admin">Super Administrator</option> <option value="super_admin">Super Administrator</option>
</select> </select>
</div> </div>
@@ -130,7 +130,7 @@ function showCreateUserModal() {
document.getElementById('modalPassword').value = ''; document.getElementById('modalPassword').value = '';
document.getElementById('modalPassword').required = true; document.getElementById('modalPassword').required = true;
document.getElementById('passwordHint').style.display = 'block'; document.getElementById('passwordHint').style.display = 'block';
document.getElementById('modalRole').value = 'operator'; document.getElementById('modalRole').value = 'admin';
document.getElementById('modalActive').checked = true; document.getElementById('modalActive').checked = true;
document.getElementById('saveUserBtn').textContent = 'Create'; document.getElementById('saveUserBtn').textContent = 'Create';
document.getElementById('userModal').style.display = 'flex'; document.getElementById('userModal').style.display = 'flex';

View File

@@ -65,7 +65,9 @@ $recentActivity = $recentActivity ?? [];
<div class="empty-state"> <div class="empty-state">
<i class="fas fa-server"></i> <i class="fas fa-server"></i>
<p>No servers configured yet.</p> <p>No servers configured yet.</p>
<?php if (is_admin()): ?>
<a href="/servers/create" class="btn btn-primary">Add Server</a> <a href="/servers/create" class="btn btn-primary">Add Server</a>
<?php endif; ?>
</div> </div>
<?php else: ?> <?php else: ?>
<div class="table-responsive"> <div class="table-responsive">

View File

@@ -39,6 +39,12 @@
<div class="nav-submenu-loading"><i class="fas fa-spinner fa-spin"></i></div> <div class="nav-submenu-loading"><i class="fas fa-spinner fa-spin"></i></div>
</div> </div>
</li> </li>
<li class="nav-item">
<a href="/team" class="nav-link <?= str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/team') ? 'active' : '' ?>">
<i class="fas fa-users-cog"></i>
<span>Team</span>
</a>
</li>
<li class="nav-divider"></li> <li class="nav-divider"></li>
<li class="nav-section">Administration</li> <li class="nav-section">Administration</li>
<?php if (in_array($_SESSION['user_role'] ?? '', ['super_admin', 'admin'])): ?> <?php if (in_array($_SESSION['user_role'] ?? '', ['super_admin', 'admin'])): ?>

View File

@@ -9,9 +9,11 @@ $groups = $groups ?? [];
<p class="page-subtitle">Manage your Linux servers</p> <p class="page-subtitle">Manage your Linux servers</p>
</div> </div>
<div class="page-header-right"> <div class="page-header-right">
<?php if (is_admin()): ?>
<a href="/servers/create" class="btn btn-primary"> <a href="/servers/create" class="btn btn-primary">
<i class="fas fa-plus"></i> Add Server <i class="fas fa-plus"></i> Add Server
</a> </a>
<?php endif; ?>
</div> </div>
</div> </div>
@@ -45,7 +47,9 @@ $groups = $groups ?? [];
<div class="empty-state"> <div class="empty-state">
<i class="fas fa-server"></i> <i class="fas fa-server"></i>
<p>No servers found.</p> <p>No servers found.</p>
<?php if (is_admin()): ?>
<a href="/servers/create" class="btn btn-primary">Add Your First Server</a> <a href="/servers/create" class="btn btn-primary">Add Your First Server</a>
<?php endif; ?>
</div> </div>
<?php else: ?> <?php else: ?>
<div class="table-responsive"> <div class="table-responsive">
@@ -106,6 +110,7 @@ $groups = $groups ?? [];
<a href="/terminal/<?= $server['id'] ?>" class="btn btn-xs" title="Terminal"> <a href="/terminal/<?= $server['id'] ?>" class="btn btn-xs" title="Terminal">
<i class="fas fa-terminal"></i> <i class="fas fa-terminal"></i>
</a> </a>
<?php if (is_admin()): ?>
<a href="/servers/<?= $server['id'] ?>/edit" class="btn btn-xs" title="Edit"> <a href="/servers/<?= $server['id'] ?>/edit" class="btn btn-xs" title="Edit">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
@@ -113,6 +118,7 @@ $groups = $groups ?? [];
onclick="deleteServer(<?= $server['id'] ?>, '<?= htmlspecialchars(addslashes($server['name'])) ?>')"> onclick="deleteServer(<?= $server['id'] ?>, '<?= htmlspecialchars(addslashes($server['name'])) ?>')">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
<?php endif; ?>
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -2,6 +2,8 @@
$server = $server ?? []; $server = $server ?? [];
$metrics = $metrics ?? null; $metrics = $metrics ?? null;
$historicalMetrics = $historicalMetrics ?? []; $historicalMetrics = $historicalMetrics ?? [];
$server_role = $server_role ?? null;
$canManage = $server_role === 'owner' || $server_role === 'manager';
?> ?>
<div class="page-header"> <div class="page-header">
@@ -20,12 +22,23 @@ $historicalMetrics = $historicalMetrics ?? [];
</div> </div>
<div class="page-header-right"> <div class="page-header-right">
<div class="btn-group"> <div class="btn-group">
<?php if ($server_role === 'owner'): ?>
<a href="/team/<?= $server['id'] ?>" class="btn btn-secondary" title="Manage Team">
<i class="fas fa-users-cog"></i>
</a>
<?php endif; ?>
<span class="badge badge-<?= $server_role === 'owner' || $server_role === 'manager' ? 'warning' : 'info' ?>" style="align-self:center">
<?= htmlspecialchars(ucfirst($server_role ?? 'viewer')) ?>
</span>
<a href="/terminal/<?= $server['id'] ?>" class="btn btn-dark"> <a href="/terminal/<?= $server['id'] ?>" class="btn btn-dark">
<i class="fas fa-terminal"></i> Terminal <i class="fas fa-terminal"></i> Terminal
</a> </a>
<?php if ($canManage): ?>
<a href="/servers/<?= $server['id'] ?>/edit" class="btn btn-secondary"> <a href="/servers/<?= $server['id'] ?>/edit" class="btn btn-secondary">
<i class="fas fa-edit"></i> Edit <i class="fas fa-edit"></i> Edit
</a> </a>
<?php endif; ?>
<?php if ($canManage): ?>
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-secondary dropdown-toggle"> <button class="btn btn-secondary dropdown-toggle">
<i class="fas fa-cog"></i> Actions <i class="fas fa-cog"></i> Actions
@@ -43,6 +56,7 @@ $historicalMetrics = $historicalMetrics ?? [];
</button> </button>
</div> </div>
</div> </div>
<?php endif; ?>
</div> </div>
</div> </div>
</div> </div>
@@ -154,6 +168,7 @@ $historicalMetrics = $historicalMetrics ?? [];
<i class="fas fa-file-alt"></i> <i class="fas fa-file-alt"></i>
<span>System Logs</span> <span>System Logs</span>
</a> </a>
<?php if ($canManage): ?>
<a href="/servers/<?= $server['id'] ?>/nginx" class="action-card"> <a href="/servers/<?= $server['id'] ?>/nginx" class="action-card">
<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24" style="opacity:0.8"> <svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24" style="opacity:0.8">
<path d="M12 2L2 7.2v9.6L12 22l10-5.2V7.2L12 2zm0 1.5l8.5 4.4v8.2L12 20.5l-8.5-4.4V7.9L12 3.5zM7.4 8.5v7l1.5.9V11l3.1 4.3 1.5.9V8.5l-1.5.9v4.4L8.9 9.4l-1.5-.9zm9.2 0l-1.5.9-3.1 4.4V17l1.5-.9V11l3.1-4.3z"/> <path d="M12 2L2 7.2v9.6L12 22l10-5.2V7.2L12 2zm0 1.5l8.5 4.4v8.2L12 20.5l-8.5-4.4V7.9L12 3.5zM7.4 8.5v7l1.5.9V11l3.1 4.3 1.5.9V8.5l-1.5.9v4.4L8.9 9.4l-1.5-.9zm9.2 0l-1.5.9-3.1 4.4V17l1.5-.9V11l3.1-4.3z"/>
@@ -180,6 +195,7 @@ $historicalMetrics = $historicalMetrics ?? [];
<i class="fas fa-sync-alt"></i> <i class="fas fa-sync-alt"></i>
<span>Restart Service</span> <span>Restart Service</span>
</button> </button>
<?php endif; ?>
</div> </div>
</div> </div>
</div> </div>

60
views/servers/team.php Normal file
View File

@@ -0,0 +1,60 @@
<?php $servers = $servers ?? []; ?>
<div class="page-header">
<div class="page-header-left">
<h1 class="page-title">Team Management</h1>
<p class="page-subtitle">Manage who has access to your servers</p>
</div>
</div>
<div class="card">
<div class="card-body">
<?php if (empty($servers)): ?>
<div class="empty-state">
<i class="fas fa-users-cog"></i>
<p>You don't own any servers yet.</p>
<p class="text-muted">Servers you create will appear here so you can manage their team access.</p>
<a href="/servers/create" class="btn btn-primary">Add Server</a>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Server</th>
<th>IP Address</th>
<th>Status</th>
<th>Team Members</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($servers as $server): ?>
<tr>
<td>
<a href="/servers/<?= $server['id'] ?>">
<?= htmlspecialchars($server['name']) ?>
</a>
</td>
<td><code><?= htmlspecialchars($server['ip_address']) ?></code></td>
<td>
<span class="status-badge status-<?= $server['status'] ?? 'unknown' ?>">
<?= ucfirst($server['status'] ?? 'unknown') ?>
</span>
</td>
<td>
<span class="badge badge-info"><?= $server['member_count'] ?> members</span>
</td>
<td>
<a href="/team/<?= $server['id'] ?>" class="btn btn-sm">
<i class="fas fa-users"></i> Manage Team
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>

View File

@@ -0,0 +1,199 @@
<?php
$server = $server ?? [];
$members = $members ?? [];
$availableUsers = $availableUsers ?? [];
?>
<div class="page-header">
<div class="page-header-left">
<div class="back-link">
<a href="/team"><i class="fas fa-arrow-left"></i> Back to Team</a>
</div>
<h1 class="page-title"><?= htmlspecialchars($server['name'] ?? '') ?></h1>
<p class="page-subtitle">Manage team access for this server</p>
</div>
</div>
<div class="dashboard-grid">
<div class="dashboard-card">
<div class="card-header">
<h2><i class="fas fa-users"></i> Team Members</h2>
</div>
<div class="card-body">
<?php if (empty($members)): ?>
<div class="empty-state">
<i class="fas fa-users"></i>
<p>No team members yet.</p>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>User</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="teamMembersBody">
<?php foreach ($members as $member): ?>
<tr data-user-id="<?= $member['user_id'] ?>">
<td>
<strong><?= htmlspecialchars($member['username']) ?></strong>
<br><small class="text-muted"><?= htmlspecialchars($member['email']) ?></small>
</td>
<td>
<?php if ($member['role'] === 'owner'): ?>
<span class="badge badge-danger">Owner</span>
<?php else: ?>
<select class="form-select form-select-sm role-select"
onchange="updateRole(<?= $member['user_id'] ?>, this.value)"
style="width:auto">
<option value="viewer" <?= $member['role'] === 'viewer' ? 'selected' : '' ?>>Viewer</option>
<option value="manager" <?= $member['role'] === 'manager' ? 'selected' : '' ?>>Manager</option>
</select>
<?php endif; ?>
</td>
<td>
<?php if ($member['role'] !== 'owner'): ?>
<button class="btn btn-xs btn-danger"
onclick="removeUser(<?= $member['user_id'] ?>, '<?= htmlspecialchars(addslashes($member['username'])) ?>')">
<i class="fas fa-trash"></i>
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<div class="dashboard-card">
<div class="card-header">
<h2><i class="fas fa-user-plus"></i> Add User</h2>
</div>
<div class="card-body">
<?php if (empty($availableUsers)): ?>
<div class="empty-state">
<i class="fas fa-user-plus"></i>
<p>All users already have access to this server.</p>
</div>
<?php else: ?>
<form id="addUserForm" class="form" onsubmit="addUser(event)">
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
<div class="form-group">
<label for="userId">User</label>
<select id="userId" name="user_id" class="form-select" required>
<option value="">Select a user...</option>
<?php foreach ($availableUsers as $user): ?>
<option value="<?= $user['id'] ?>">
<?= htmlspecialchars($user['username']) ?> (<?= htmlspecialchars($user['email']) ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="role">Role</label>
<select id="role" name="role" class="form-select" required>
<option value="viewer">Viewer — can view server details and use terminal</option>
<option value="manager">Manager — full control except team management</option>
</select>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-user-plus"></i> Add User
</button>
</form>
<?php endif; ?>
</div>
</div>
</div>
<form id="csrfForm" style="display:none">
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
</form>
<script>
function getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.content || '';
}
function addUser(e) {
e.preventDefault();
const form = document.getElementById('addUserForm');
const data = new FormData(form);
fetch('/team/<?= $server['id'] ?>/add-user', {
method: 'POST',
headers: {
'X-CSRF-Token': getCsrfToken(),
},
body: data,
})
.then(r => r.json())
.then(res => {
if (res.success) {
showToast('success', res.message);
location.reload();
} else {
showToast('error', res.message);
}
})
.catch(err => showToast('error', 'Request failed'));
}
function removeUser(userId, username) {
if (!confirm('Remove "' + username + '" from this server?')) return;
const form = new FormData();
form.append('user_id', userId);
form.append('_csrf_token', getCsrfToken());
fetch('/team/<?= $server['id'] ?>/remove-user', {
method: 'POST',
headers: {
'X-CSRF-Token': getCsrfToken(),
},
body: form,
})
.then(r => r.json())
.then(res => {
if (res.success) {
showToast('success', res.message);
location.reload();
} else {
showToast('error', res.message);
}
})
.catch(err => showToast('error', 'Request failed'));
}
function updateRole(userId, role) {
const form = new FormData();
form.append('user_id', userId);
form.append('role', role);
form.append('_csrf_token', getCsrfToken());
fetch('/team/<?= $server['id'] ?>/update-role', {
method: 'POST',
headers: {
'X-CSRF-Token': getCsrfToken(),
},
body: form,
})
.then(r => r.json())
.then(res => {
if (res.success) {
showToast('success', 'Role updated to ' + role);
} else {
showToast('error', res.message);
}
})
.catch(err => showToast('error', 'Request failed'));
}
</script>