Merge pull request 'Server ownership, team access, role-based UI' (#7) from feature/roles-y-accesos into master
Reviewed-on: #7
This commit was merged in pull request #7.
This commit is contained in:
24
database/migrations/002_server_access.sql
Normal file
24
database/migrations/002_server_access.sql
Normal 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;
|
||||
47
database/migrations/003_teams.sql
Normal file
47
database/migrations/003_teams.sql
Normal file
@@ -0,0 +1,47 @@
|
||||
-- ============================================================================
|
||||
-- ServerManager Teams
|
||||
-- Team-based access control: teams group users and servers together
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `teams` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(100) NOT NULL,
|
||||
`description` TEXT DEFAULT NULL,
|
||||
`created_by` INT UNSIGNED NOT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_name` (`name`),
|
||||
KEY `idx_created_by` (`created_by`),
|
||||
CONSTRAINT `fk_teams_created_by` FOREIGN KEY (`created_by`)
|
||||
REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `team_user` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`team_id` INT UNSIGNED NOT NULL,
|
||||
`user_id` INT UNSIGNED NOT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_team_user` (`team_id`, `user_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
CONSTRAINT `fk_team_user_team` FOREIGN KEY (`team_id`)
|
||||
REFERENCES `teams` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `fk_team_user_user` FOREIGN KEY (`user_id`)
|
||||
REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `team_server` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`team_id` INT UNSIGNED NOT NULL,
|
||||
`server_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_team_server` (`team_id`, `server_id`),
|
||||
KEY `idx_server_id` (`server_id`),
|
||||
CONSTRAINT `fk_team_server_team` FOREIGN KEY (`team_id`)
|
||||
REFERENCES `teams` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `fk_team_server_server` FOREIGN KEY (`server_id`)
|
||||
REFERENCES `servers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -9,6 +9,7 @@ use ServerManager\Controllers\ServerController;
|
||||
use ServerManager\Controllers\TerminalController;
|
||||
use ServerManager\Controllers\AdminController;
|
||||
use ServerManager\Controllers\ApiController;
|
||||
use ServerManager\Controllers\TeamController;
|
||||
use ServerManager\Middleware\AuthMiddleware;
|
||||
use ServerManager\Middleware\CSRFMiddleware;
|
||||
use ServerManager\Middleware\RateLimitMiddleware;
|
||||
@@ -31,8 +32,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 +66,24 @@ $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('/teams', [TeamController::class, 'index'], [AuthMiddleware::class, RoleMiddleware::class]);
|
||||
$router->get('/teams/create', [TeamController::class, 'create'], [AuthMiddleware::class, RoleMiddleware::class]);
|
||||
$router->post('/teams/create', [TeamController::class, 'store'], [AuthMiddleware::class, CSRFMiddleware::class, RoleMiddleware::class]);
|
||||
$router->get('/teams/:id', [TeamController::class, 'show'], [AuthMiddleware::class, RoleMiddleware::class]);
|
||||
$router->post('/teams/:id/update', [TeamController::class, 'update'], [AuthMiddleware::class, CSRFMiddleware::class]);
|
||||
$router->post('/teams/:id/delete', [TeamController::class, 'delete'], [AuthMiddleware::class, CSRFMiddleware::class]);
|
||||
$router->post('/teams/:id/add-user', [TeamController::class, 'addUser'], [AuthMiddleware::class, CSRFMiddleware::class]);
|
||||
$router->post('/teams/:id/remove-user', [TeamController::class, 'removeUser'], [AuthMiddleware::class, CSRFMiddleware::class]);
|
||||
$router->post('/teams/:id/add-server', [TeamController::class, 'addServer'], [AuthMiddleware::class, CSRFMiddleware::class]);
|
||||
$router->post('/teams/:id/remove-server', [TeamController::class, 'removeServer'], [AuthMiddleware::class, CSRFMiddleware::class]);
|
||||
$router->post('/teams/:id/update-server-role', [TeamController::class, 'updateServerRole'], [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]);
|
||||
|
||||
@@ -23,8 +23,18 @@ class AdminController
|
||||
$this->auditService = new AuditService();
|
||||
}
|
||||
|
||||
private function requireSuperAdmin(): void
|
||||
{
|
||||
if (Session::get('user_role') !== 'super_admin') {
|
||||
Session::setFlash('error', 'You do not have permission to access this page.');
|
||||
$this->view->redirect('/dashboard');
|
||||
}
|
||||
}
|
||||
|
||||
public function users(): void
|
||||
{
|
||||
$this->requireSuperAdmin();
|
||||
|
||||
$page = (int) ($_GET['page'] ?? 1);
|
||||
$users = $this->userModel->getAll($page, 20);
|
||||
|
||||
@@ -36,6 +46,10 @@ class AdminController
|
||||
|
||||
public function createUser(): void
|
||||
{
|
||||
if (Session::get('user_role') !== 'super_admin') {
|
||||
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
|
||||
}
|
||||
|
||||
$validator = new Validator();
|
||||
$rules = [
|
||||
'username' => 'required|min:3|max:50|alphaNum',
|
||||
@@ -62,7 +76,7 @@ class AdminController
|
||||
'username' => $_POST['username'],
|
||||
'email' => $_POST['email'],
|
||||
'password' => $_POST['password'],
|
||||
'role' => $_POST['role'],
|
||||
'role' => $_POST['role'] ?? 'admin',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
@@ -77,6 +91,10 @@ class AdminController
|
||||
|
||||
public function updateUser(int $id): void
|
||||
{
|
||||
if (Session::get('user_role') !== 'super_admin') {
|
||||
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
|
||||
}
|
||||
|
||||
$user = $this->userModel->findById($id);
|
||||
|
||||
if (!$user) {
|
||||
@@ -117,6 +135,10 @@ class AdminController
|
||||
|
||||
public function deleteUser(int $id): void
|
||||
{
|
||||
if (Session::get('user_role') !== 'super_admin') {
|
||||
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
|
||||
}
|
||||
|
||||
$user = $this->userModel->findById($id);
|
||||
|
||||
if (!$user) {
|
||||
@@ -139,6 +161,11 @@ class AdminController
|
||||
|
||||
public function auditLogs(): void
|
||||
{
|
||||
if (Session::get('user_role') !== 'super_admin') {
|
||||
Session::setFlash('error', 'You do not have permission to access this page.');
|
||||
$this->view->redirect('/dashboard');
|
||||
}
|
||||
|
||||
$page = (int) ($_GET['page'] ?? 1);
|
||||
$action = $_GET['action'] ?? '';
|
||||
$userId = $_GET['user_id'] ?? '';
|
||||
@@ -159,6 +186,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',
|
||||
]);
|
||||
|
||||
@@ -76,6 +76,9 @@ class ApiController
|
||||
$filters = [];
|
||||
if ($search) $filters['search'] = $search;
|
||||
|
||||
$accessibleIds = $serverModel->getAccessibleServerIds((int) $user['id']);
|
||||
$filters['accessible_ids'] = !empty($accessibleIds) ? $accessibleIds : [-1];
|
||||
|
||||
$servers = $serverModel->getAll($page, $perPage, $filters);
|
||||
|
||||
$this->view->json([
|
||||
@@ -101,6 +104,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 +161,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 +184,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 +200,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 +222,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'] ?? '';
|
||||
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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;
|
||||
@@ -24,11 +25,22 @@ class DashboardController
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
$stats = $this->monitoringService->getDashboardStats();
|
||||
$recentActivity = $this->auditService->getRecentActivity(10);
|
||||
|
||||
$role = Session::get('user_role');
|
||||
$serverModel = new Server();
|
||||
$userId = (int) Session::get('user_id');
|
||||
|
||||
if ($role === 'super_admin') {
|
||||
$stats = $this->monitoringService->getDashboardStats();
|
||||
$servers = $serverModel->findAll(true);
|
||||
} else {
|
||||
$accessibleIds = $serverModel->getAccessibleServerIds($userId);
|
||||
$stats = $this->monitoringService->getDashboardStats($accessibleIds);
|
||||
$servers = !empty($accessibleIds) ? array_values(array_filter($serverModel->findAll(true), function ($s) use ($accessibleIds) {
|
||||
return in_array((int) $s['id'], $accessibleIds, true);
|
||||
})) : [];
|
||||
}
|
||||
|
||||
$recentActivity = $this->auditService->getRecentActivity(10);
|
||||
|
||||
$this->view->display('dashboard.index', [
|
||||
'title' => 'Dashboard - ServerManager',
|
||||
@@ -40,7 +52,15 @@ class DashboardController
|
||||
|
||||
public function stats(): void
|
||||
{
|
||||
$role = Session::get('user_role');
|
||||
|
||||
if ($role === 'super_admin') {
|
||||
$stats = $this->monitoringService->getDashboardStats();
|
||||
} else {
|
||||
$serverModel = new Server();
|
||||
$accessibleIds = $serverModel->getAccessibleServerIds((int) Session::get('user_id'));
|
||||
$stats = $this->monitoringService->getDashboardStats($accessibleIds);
|
||||
}
|
||||
|
||||
$this->view->json([
|
||||
'success' => true,
|
||||
|
||||
@@ -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,45 @@ 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);
|
||||
$filters['accessible_ids'] = !empty($accessibleIds) ? $accessibleIds : [-1];
|
||||
|
||||
$servers = $this->serverModel->getAll($page, 15, $filters);
|
||||
$groups = $this->serverModel->getGroups();
|
||||
|
||||
@@ -107,11 +135,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 +148,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 +167,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 +213,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 +233,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 +257,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 +272,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 +319,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 +344,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 +398,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 +436,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 +474,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 +524,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 +571,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 +626,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 +679,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 +739,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 +783,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 +875,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 +924,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 +972,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 +1041,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 +1139,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 +1247,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 +1517,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 +1538,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 +1587,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 +1624,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 +1679,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 +1794,5 @@ class ServerController
|
||||
'data' => $metrics,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
292
src/Controllers/TeamController.php
Normal file
292
src/Controllers/TeamController.php
Normal file
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ServerManager\Controllers;
|
||||
|
||||
use ServerManager\Core\App;
|
||||
use ServerManager\Core\Session;
|
||||
use ServerManager\Core\Validator;
|
||||
use ServerManager\Models\Team;
|
||||
use ServerManager\Models\Server;
|
||||
use ServerManager\Models\User;
|
||||
use ServerManager\Services\AuditService;
|
||||
|
||||
class TeamController
|
||||
{
|
||||
private \ServerManager\Core\View $view;
|
||||
private Team $teamModel;
|
||||
private Server $serverModel;
|
||||
private AuditService $auditService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->view = App::getInstance()->getView();
|
||||
$this->teamModel = new Team();
|
||||
$this->serverModel = new Server();
|
||||
$this->auditService = new AuditService();
|
||||
}
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
$userId = (int) Session::get('user_id');
|
||||
$teams = $this->teamModel->findByCreator($userId);
|
||||
|
||||
$this->view->display('teams.index', [
|
||||
'title' => 'Teams - ServerManager',
|
||||
'teams' => $teams,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
$this->view->display('teams.create', [
|
||||
'title' => 'Create Team - ServerManager',
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(): void
|
||||
{
|
||||
$validator = new Validator();
|
||||
$rules = [
|
||||
'name' => 'required|min:2|max:100',
|
||||
'description' => 'max:500',
|
||||
];
|
||||
|
||||
if (!$validator->validate($_POST, $rules)) {
|
||||
Session::setFlash('error', $validator->getFirstError());
|
||||
$this->view->redirect('/teams/create');
|
||||
}
|
||||
|
||||
$id = $this->teamModel->create([
|
||||
'name' => $_POST['name'],
|
||||
'description' => $_POST['description'] ?? '',
|
||||
]);
|
||||
|
||||
$this->auditService->log('team_created', 'team', $id, [
|
||||
'name' => $_POST['name'],
|
||||
]);
|
||||
|
||||
Session::setFlash('success', 'Team created successfully.');
|
||||
$this->view->redirect('/teams');
|
||||
}
|
||||
|
||||
public function show(int $id): void
|
||||
{
|
||||
$userId = (int) Session::get('user_id');
|
||||
$team = $this->teamModel->findById($id);
|
||||
|
||||
if (!$team || (int) $team['created_by'] !== $userId) {
|
||||
$this->view->error(404);
|
||||
}
|
||||
|
||||
$members = $this->teamModel->getMembers($id);
|
||||
$servers = $this->teamModel->getServers($id);
|
||||
|
||||
$userModel = new User();
|
||||
$allUsers = $userModel->getAll(1, 999);
|
||||
$memberIds = array_map(fn($m) => $m['user_id'], $members);
|
||||
$availableUsers = array_filter($allUsers['data'], fn($u) => !in_array((int) $u['id'], $memberIds, true));
|
||||
|
||||
$userId = (int) Session::get('user_id');
|
||||
$ownedServers = $this->serverModel->getOwnedServers($userId);
|
||||
$serverIds = array_map(fn($s) => (int) $s['id'], $servers);
|
||||
$availableServers = array_filter($ownedServers, fn($s) => !in_array((int) $s['id'], $serverIds, true));
|
||||
|
||||
$this->view->display('teams.show', [
|
||||
'title' => $team['name'] . ' - Teams - ServerManager',
|
||||
'team' => $team,
|
||||
'members' => $members,
|
||||
'servers' => $servers,
|
||||
'availableUsers' => $availableUsers,
|
||||
'availableServers' => $availableServers,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(int $id): void
|
||||
{
|
||||
$userId = (int) Session::get('user_id');
|
||||
$team = $this->teamModel->findById($id);
|
||||
|
||||
if (!$team || (int) $team['created_by'] !== $userId) {
|
||||
$this->view->json(['success' => false, 'message' => 'Team not found'], 404);
|
||||
}
|
||||
|
||||
$validator = new Validator();
|
||||
$rules = [
|
||||
'name' => 'required|min:2|max:100',
|
||||
'description' => 'max:500',
|
||||
];
|
||||
|
||||
if (!$validator->validate($_POST, $rules)) {
|
||||
$this->view->json(['success' => false, 'message' => $validator->getFirstError()]);
|
||||
}
|
||||
|
||||
$this->teamModel->update($id, [
|
||||
'name' => $_POST['name'],
|
||||
'description' => $_POST['description'] ?? '',
|
||||
]);
|
||||
|
||||
$this->auditService->log('team_updated', 'team', $id, [
|
||||
'name' => $_POST['name'],
|
||||
]);
|
||||
|
||||
$this->view->json(['success' => true, 'message' => 'Team updated.']);
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$userId = (int) Session::get('user_id');
|
||||
$team = $this->teamModel->findById($id);
|
||||
|
||||
if (!$team || (int) $team['created_by'] !== $userId) {
|
||||
$this->view->json(['success' => false, 'message' => 'Team not found'], 404);
|
||||
}
|
||||
|
||||
$this->teamModel->delete($id);
|
||||
|
||||
$this->auditService->log('team_deleted', 'team', $id, [
|
||||
'name' => $team['name'],
|
||||
]);
|
||||
|
||||
Session::setFlash('success', 'Team deleted.');
|
||||
$this->view->redirect('/teams');
|
||||
}
|
||||
|
||||
public function addUser(int $id): void
|
||||
{
|
||||
$userId = (int) Session::get('user_id');
|
||||
$team = $this->teamModel->findById($id);
|
||||
|
||||
if (!$team || (int) $team['created_by'] !== $userId) {
|
||||
$this->view->json(['success' => false, 'message' => 'Team not found'], 404);
|
||||
}
|
||||
|
||||
$userId = (int) ($_POST['user_id'] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
$this->view->json(['success' => false, 'message' => 'Invalid user']);
|
||||
}
|
||||
|
||||
if ($this->teamModel->addMember($id, $userId)) {
|
||||
$this->auditService->log('team_user_added', 'team', $id, [
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
$this->view->json(['success' => true, 'message' => 'User added to team.']);
|
||||
}
|
||||
|
||||
$this->view->json(['success' => false, 'message' => 'User is already in this team.']);
|
||||
}
|
||||
|
||||
public function removeUser(int $id): void
|
||||
{
|
||||
$userId = (int) Session::get('user_id');
|
||||
$team = $this->teamModel->findById($id);
|
||||
|
||||
if (!$team || (int) $team['created_by'] !== $userId) {
|
||||
$this->view->json(['success' => false, 'message' => 'Team not found'], 404);
|
||||
}
|
||||
|
||||
$userId = (int) ($_POST['user_id'] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
$this->view->json(['success' => false, 'message' => 'Invalid user']);
|
||||
}
|
||||
|
||||
$this->teamModel->removeMember($id, $userId);
|
||||
|
||||
$this->auditService->log('team_user_removed', 'team', $id, [
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
|
||||
$this->view->json(['success' => true, 'message' => 'User removed from team.']);
|
||||
}
|
||||
|
||||
public function addServer(int $id): void
|
||||
{
|
||||
$userId = (int) Session::get('user_id');
|
||||
$team = $this->teamModel->findById($id);
|
||||
|
||||
if (!$team || (int) $team['created_by'] !== $userId) {
|
||||
$this->view->json(['success' => false, 'message' => 'Team not found'], 404);
|
||||
}
|
||||
|
||||
$serverId = (int) ($_POST['server_id'] ?? 0);
|
||||
$role = $_POST['role'] ?? 'viewer';
|
||||
|
||||
if ($serverId <= 0) {
|
||||
$this->view->json(['success' => false, 'message' => 'Invalid server']);
|
||||
}
|
||||
|
||||
if (!in_array($role, ['viewer', 'manager'], true)) {
|
||||
$this->view->json(['success' => false, 'message' => 'Invalid role']);
|
||||
}
|
||||
|
||||
$userId = (int) Session::get('user_id');
|
||||
$server = $this->serverModel->findById($serverId);
|
||||
if (!$server || (int) $server['created_by'] !== $userId) {
|
||||
$this->view->json(['success' => false, 'message' => 'Server not found or not owned by you'], 404);
|
||||
}
|
||||
|
||||
if ($this->teamModel->addServer($id, $serverId, $role)) {
|
||||
$this->auditService->log('team_server_added', 'team', $id, [
|
||||
'server_id' => $serverId,
|
||||
'role' => $role,
|
||||
]);
|
||||
$this->view->json(['success' => true, 'message' => 'Server assigned to team.']);
|
||||
}
|
||||
|
||||
$this->view->json(['success' => false, 'message' => 'Server is already assigned to this team.']);
|
||||
}
|
||||
|
||||
public function removeServer(int $id): void
|
||||
{
|
||||
$userId = (int) Session::get('user_id');
|
||||
$team = $this->teamModel->findById($id);
|
||||
|
||||
if (!$team || (int) $team['created_by'] !== $userId) {
|
||||
$this->view->json(['success' => false, 'message' => 'Team not found'], 404);
|
||||
}
|
||||
|
||||
$serverId = (int) ($_POST['server_id'] ?? 0);
|
||||
if ($serverId <= 0) {
|
||||
$this->view->json(['success' => false, 'message' => 'Invalid server']);
|
||||
}
|
||||
|
||||
$this->teamModel->removeServer($id, $serverId);
|
||||
|
||||
$this->auditService->log('team_server_removed', 'team', $id, [
|
||||
'server_id' => $serverId,
|
||||
]);
|
||||
|
||||
$this->view->json(['success' => true, 'message' => 'Server removed from team.']);
|
||||
}
|
||||
|
||||
public function updateServerRole(int $id): void
|
||||
{
|
||||
$userId = (int) Session::get('user_id');
|
||||
$team = $this->teamModel->findById($id);
|
||||
|
||||
if (!$team || (int) $team['created_by'] !== $userId) {
|
||||
$this->view->json(['success' => false, 'message' => 'Team not found'], 404);
|
||||
}
|
||||
|
||||
$serverId = (int) ($_POST['server_id'] ?? 0);
|
||||
$role = $_POST['role'] ?? 'viewer';
|
||||
|
||||
if ($serverId <= 0) {
|
||||
$this->view->json(['success' => false, 'message' => 'Invalid server']);
|
||||
}
|
||||
|
||||
if (!in_array($role, ['viewer', 'manager'], true)) {
|
||||
$this->view->json(['success' => false, 'message' => 'Invalid role']);
|
||||
}
|
||||
|
||||
$this->teamModel->updateServerRole($id, $serverId, $role);
|
||||
|
||||
$this->auditService->log('team_server_role_updated', 'team', $id, [
|
||||
'server_id' => $serverId,
|
||||
'role' => $role,
|
||||
]);
|
||||
|
||||
$this->view->json(['success' => true, 'message' => 'Server role updated.']);
|
||||
}
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,184 @@ 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';
|
||||
}
|
||||
|
||||
$direct = $this->db->fetch(
|
||||
'SELECT role FROM server_user WHERE server_id = ? AND user_id = ?',
|
||||
[$serverId, $userId]
|
||||
);
|
||||
|
||||
$teamRole = $this->db->fetch(
|
||||
'SELECT ts.role FROM team_user tu
|
||||
JOIN team_server ts ON tu.team_id = ts.team_id
|
||||
WHERE tu.user_id = ? AND ts.server_id = ?
|
||||
ORDER BY FIELD(ts.role, \'manager\', \'viewer\')
|
||||
LIMIT 1',
|
||||
[$userId, $serverId]
|
||||
);
|
||||
|
||||
$directRole = $direct ? $direct['role'] : null;
|
||||
$teamRoleValue = $teamRole ? $teamRole['role'] : null;
|
||||
|
||||
if ($directRole === 'manager' || $teamRoleValue === 'manager') {
|
||||
return 'manager';
|
||||
}
|
||||
if ($directRole === 'viewer' || $teamRoleValue === 'viewer') {
|
||||
return 'viewer';
|
||||
}
|
||||
|
||||
return 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]
|
||||
);
|
||||
|
||||
$teamServerIds = $this->db->fetchAll(
|
||||
'SELECT DISTINCT ts.server_id FROM team_user tu
|
||||
JOIN team_server ts ON tu.team_id = ts.team_id
|
||||
WHERE tu.user_id = ?',
|
||||
[$userId]
|
||||
);
|
||||
|
||||
$ids = [];
|
||||
foreach ($owned as $row) {
|
||||
$ids[] = (int) $row['id'];
|
||||
}
|
||||
foreach ($assigned as $row) {
|
||||
$ids[] = (int) $row['server_id'];
|
||||
}
|
||||
foreach ($teamServerIds 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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
164
src/Models/Team.php
Normal file
164
src/Models/Team.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ServerManager\Models;
|
||||
|
||||
use ServerManager\Core\Database;
|
||||
use ServerManager\Core\Session;
|
||||
|
||||
class Team
|
||||
{
|
||||
private Database $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::getInstance();
|
||||
}
|
||||
|
||||
public function findAll(?int $userId = null): array
|
||||
{
|
||||
$where = $userId ? ' WHERE t.created_by = ?' : '';
|
||||
$params = $userId ? [$userId] : [];
|
||||
|
||||
return $this->db->fetchAll(
|
||||
"SELECT t.*,
|
||||
(SELECT COUNT(*) FROM team_user WHERE team_id = t.id) AS member_count,
|
||||
(SELECT COUNT(*) FROM team_server WHERE team_id = t.id) AS server_count
|
||||
FROM teams t{$where}
|
||||
ORDER BY t.name ASC",
|
||||
$params
|
||||
);
|
||||
}
|
||||
|
||||
public function findByCreator(int $userId): array
|
||||
{
|
||||
return $this->findAll($userId);
|
||||
}
|
||||
|
||||
public function findById(int $id): ?array
|
||||
{
|
||||
$team = $this->db->fetch(
|
||||
'SELECT t.*,
|
||||
(SELECT COUNT(*) FROM team_user WHERE team_id = t.id) AS member_count,
|
||||
(SELECT COUNT(*) FROM team_server WHERE team_id = t.id) AS server_count
|
||||
FROM teams t WHERE t.id = ?',
|
||||
[$id]
|
||||
);
|
||||
return $team ?: null;
|
||||
}
|
||||
|
||||
public function create(array $data): int
|
||||
{
|
||||
$data['created_by'] = (int) Session::get('user_id');
|
||||
$data['created_at'] = date('Y-m-d H:i:s');
|
||||
$data['updated_at'] = date('Y-m-d H:i:s');
|
||||
return $this->db->insert('teams', $data);
|
||||
}
|
||||
|
||||
public function update(int $id, array $data): bool
|
||||
{
|
||||
$data['updated_at'] = date('Y-m-d H:i:s');
|
||||
return $this->db->update('teams', $data, 'id = ?', [$id]) > 0;
|
||||
}
|
||||
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
return $this->db->delete('teams', 'id = ?', [$id]) > 0;
|
||||
}
|
||||
|
||||
public function getMembers(int $teamId): array
|
||||
{
|
||||
return $this->db->fetchAll(
|
||||
'SELECT tu.*, u.username, u.email
|
||||
FROM team_user tu
|
||||
JOIN users u ON tu.user_id = u.id
|
||||
WHERE tu.team_id = ?
|
||||
ORDER BY u.username ASC',
|
||||
[$teamId]
|
||||
);
|
||||
}
|
||||
|
||||
public function addMember(int $teamId, int $userId): bool
|
||||
{
|
||||
$existing = $this->db->fetch(
|
||||
'SELECT id FROM team_user WHERE team_id = ? AND user_id = ?',
|
||||
[$teamId, $userId]
|
||||
);
|
||||
if ($existing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->db->insert('team_user', [
|
||||
'team_id' => $teamId,
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function removeMember(int $teamId, int $userId): bool
|
||||
{
|
||||
return $this->db->delete(
|
||||
'team_user',
|
||||
'team_id = ? AND user_id = ?',
|
||||
[$teamId, $userId]
|
||||
) > 0;
|
||||
}
|
||||
|
||||
public function getServers(int $teamId): array
|
||||
{
|
||||
return $this->db->fetchAll(
|
||||
'SELECT ts.*, s.name AS server_name, s.ip_address
|
||||
FROM team_server ts
|
||||
JOIN servers s ON ts.server_id = s.id
|
||||
WHERE ts.team_id = ?
|
||||
ORDER BY s.name ASC',
|
||||
[$teamId]
|
||||
);
|
||||
}
|
||||
|
||||
public function addServer(int $teamId, int $serverId, string $role): bool
|
||||
{
|
||||
if (!in_array($role, ['viewer', 'manager'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$existing = $this->db->fetch(
|
||||
'SELECT id FROM team_server WHERE team_id = ? AND server_id = ?',
|
||||
[$teamId, $serverId]
|
||||
);
|
||||
if ($existing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->db->insert('team_server', [
|
||||
'team_id' => $teamId,
|
||||
'server_id' => $serverId,
|
||||
'role' => $role,
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function removeServer(int $teamId, int $serverId): bool
|
||||
{
|
||||
return $this->db->delete(
|
||||
'team_server',
|
||||
'team_id = ? AND server_id = ?',
|
||||
[$teamId, $serverId]
|
||||
) > 0;
|
||||
}
|
||||
|
||||
public function updateServerRole(int $teamId, int $serverId, string $role): bool
|
||||
{
|
||||
if (!in_array($role, ['viewer', 'manager'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->db->update(
|
||||
'team_server',
|
||||
['role' => $role],
|
||||
'team_id = ? AND server_id = ?',
|
||||
[$teamId, $serverId]
|
||||
) > 0;
|
||||
}
|
||||
}
|
||||
@@ -173,20 +173,41 @@ class MonitoringService
|
||||
);
|
||||
}
|
||||
|
||||
public function getDashboardStats(): array
|
||||
public function getDashboardStats(?array $serverIds = null): array
|
||||
{
|
||||
$totalServers = $this->db->fetch("SELECT COUNT(*) as total FROM servers");
|
||||
$onlineServers = $this->db->fetch("SELECT COUNT(*) as total FROM servers WHERE current_status = 'online' AND is_active = 1");
|
||||
$offlineServers = $this->db->fetch("SELECT COUNT(*) as total FROM servers WHERE current_status = 'offline' AND is_active = 1");
|
||||
$where = '';
|
||||
$params = [];
|
||||
|
||||
if ($serverIds !== null) {
|
||||
if (empty($serverIds)) {
|
||||
$serverIds = [-1];
|
||||
}
|
||||
$placeholders = implode(',', array_fill(0, count($serverIds), '?'));
|
||||
$where = " AND s.id IN ({$placeholders})";
|
||||
$params = $serverIds;
|
||||
}
|
||||
|
||||
$totalServers = $this->db->fetch("SELECT COUNT(*) as total FROM servers s WHERE 1=1{$where}", $params);
|
||||
$onlineServers = $this->db->fetch(
|
||||
"SELECT COUNT(*) as total FROM servers s WHERE s.current_status = 'online' AND s.is_active = 1{$where}",
|
||||
$params
|
||||
);
|
||||
$offlineServers = $this->db->fetch(
|
||||
"SELECT COUNT(*) as total FROM servers s WHERE s.current_status = 'offline' AND s.is_active = 1{$where}",
|
||||
$params
|
||||
);
|
||||
|
||||
$avgCpu = $this->db->fetch(
|
||||
"SELECT AVG(cpu_usage) as avg FROM servers WHERE is_active = 1 AND current_status = 'online'"
|
||||
"SELECT AVG(s.cpu_usage) as avg FROM servers s WHERE s.is_active = 1 AND s.current_status = 'online'{$where}",
|
||||
$params
|
||||
);
|
||||
$avgRam = $this->db->fetch(
|
||||
"SELECT AVG(ram_usage) as avg FROM servers WHERE is_active = 1 AND current_status = 'online'"
|
||||
"SELECT AVG(s.ram_usage) as avg FROM servers s WHERE s.is_active = 1 AND s.current_status = 'online'{$where}",
|
||||
$params
|
||||
);
|
||||
$avgDisk = $this->db->fetch(
|
||||
"SELECT AVG(disk_usage) as avg FROM servers WHERE is_active = 1 AND current_status = 'online'"
|
||||
"SELECT AVG(s.disk_usage) as avg FROM servers s WHERE s.is_active = 1 AND s.current_status = 'online'{$where}",
|
||||
$params
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -100,8 +100,8 @@
|
||||
<div class="form-group">
|
||||
<label for="modalRole">Role</label>
|
||||
<select id="modalRole" name="role" class="form-select" required>
|
||||
<option value="admin" selected>Administrator</option>
|
||||
<option value="operator">Operator</option>
|
||||
<option value="admin">Administrator</option>
|
||||
<option value="super_admin">Super Administrator</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -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';
|
||||
|
||||
@@ -65,7 +65,9 @@ $recentActivity = $recentActivity ?? [];
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-server"></i>
|
||||
<p>No servers configured yet.</p>
|
||||
<?php if (is_admin()): ?>
|
||||
<a href="/servers/create" class="btn btn-primary">Add Server</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
|
||||
@@ -39,9 +39,15 @@
|
||||
<div class="nav-submenu-loading"><i class="fas fa-spinner fa-spin"></i></div>
|
||||
</div>
|
||||
</li>
|
||||
<?php if (is_super_admin()): ?>
|
||||
<li class="nav-divider"></li>
|
||||
<li class="nav-section">Administration</li>
|
||||
<?php if (in_array($_SESSION['user_role'] ?? '', ['super_admin', 'admin'])): ?>
|
||||
<li class="nav-item">
|
||||
<a href="/teams" class="nav-link <?= str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/teams') ? 'active' : '' ?>">
|
||||
<i class="fas fa-users-cog"></i>
|
||||
<span>Teams</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/users" class="nav-link <?= str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/admin/users') ? 'active' : '' ?>">
|
||||
<i class="fas fa-users"></i>
|
||||
@@ -54,14 +60,21 @@
|
||||
<span>Audit Logs</span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if ($_SESSION['user_role'] ?? '' === 'super_admin'): ?>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/security" class="nav-link <?= str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/admin/security') ? 'active' : '' ?>">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
<span>Security</span>
|
||||
</a>
|
||||
</li>
|
||||
<?php elseif (is_admin()): ?>
|
||||
<li class="nav-divider"></li>
|
||||
<li class="nav-section">Administration</li>
|
||||
<li class="nav-item">
|
||||
<a href="/teams" class="nav-link <?= str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/teams') ? 'active' : '' ?>">
|
||||
<i class="fas fa-users-cog"></i>
|
||||
<span>Teams</span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -9,9 +9,11 @@ $groups = $groups ?? [];
|
||||
<p class="page-subtitle">Manage your Linux servers</p>
|
||||
</div>
|
||||
<div class="page-header-right">
|
||||
<?php if (is_admin()): ?>
|
||||
<a href="/servers/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Add Server
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +47,9 @@ $groups = $groups ?? [];
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-server"></i>
|
||||
<p>No servers found.</p>
|
||||
<?php if (is_admin()): ?>
|
||||
<a href="/servers/create" class="btn btn-primary">Add Your First Server</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
@@ -106,6 +110,7 @@ $groups = $groups ?? [];
|
||||
<a href="/terminal/<?= $server['id'] ?>" class="btn btn-xs" title="Terminal">
|
||||
<i class="fas fa-terminal"></i>
|
||||
</a>
|
||||
<?php if (is_admin()): ?>
|
||||
<a href="/servers/<?= $server['id'] ?>/edit" class="btn btn-xs" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
@@ -113,6 +118,7 @@ $groups = $groups ?? [];
|
||||
onclick="deleteServer(<?= $server['id'] ?>, '<?= htmlspecialchars(addslashes($server['name'])) ?>')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
$server = $server ?? [];
|
||||
$metrics = $metrics ?? null;
|
||||
$historicalMetrics = $historicalMetrics ?? [];
|
||||
$server_role = $server_role ?? null;
|
||||
$canManage = $server_role === 'owner' || $server_role === 'manager';
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
@@ -20,12 +22,23 @@ $historicalMetrics = $historicalMetrics ?? [];
|
||||
</div>
|
||||
<div class="page-header-right">
|
||||
<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">
|
||||
<i class="fas fa-terminal"></i> Terminal
|
||||
</a>
|
||||
<?php if ($canManage): ?>
|
||||
<a href="/servers/<?= $server['id'] ?>/edit" class="btn btn-secondary">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($canManage): ?>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle">
|
||||
<i class="fas fa-cog"></i> Actions
|
||||
@@ -43,6 +56,7 @@ $historicalMetrics = $historicalMetrics ?? [];
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,6 +168,7 @@ $historicalMetrics = $historicalMetrics ?? [];
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span>System Logs</span>
|
||||
</a>
|
||||
<?php if ($canManage): ?>
|
||||
<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">
|
||||
<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>
|
||||
<span>Restart Service</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
36
views/teams/create.php
Normal file
36
views/teams/create.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<div class="page-header">
|
||||
<div class="page-header-left">
|
||||
<div class="back-link">
|
||||
<a href="/teams"><i class="fas fa-arrow-left"></i> Back to Teams</a>
|
||||
</div>
|
||||
<h1 class="page-title">Create Team</h1>
|
||||
<p class="page-subtitle">Create a work team to manage server access</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/teams/create" class="form" style="max-width:600px">
|
||||
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Team Name *</label>
|
||||
<input type="text" id="name" name="name" class="form-input"
|
||||
placeholder="e.g., DevOps, Developers, Support" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" class="form-input" rows="3"
|
||||
placeholder="Optional description of the team"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Create Team
|
||||
</button>
|
||||
<a href="/teams" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
78
views/teams/index.php
Normal file
78
views/teams/index.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php $teams = $teams ?? []; ?>
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Teams</h1>
|
||||
<p class="page-subtitle">Create and manage work teams with server access</p>
|
||||
</div>
|
||||
<div class="page-header-right">
|
||||
<a href="/teams/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Create Team
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<?php if (empty($teams)): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-users-cog"></i>
|
||||
<p>No teams created yet.</p>
|
||||
<a href="/teams/create" class="btn btn-primary">Create Your First Team</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Members</th>
|
||||
<th>Servers</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($teams as $team): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/teams/<?= $team['id'] ?>" class="fw-semibold">
|
||||
<?= htmlspecialchars($team['name']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-muted"><?= htmlspecialchars(mb_substr($team['description'] ?? '', 0, 80)) ?></td>
|
||||
<td><span class="badge badge-info"><?= (int) ($team['member_count'] ?? 0) ?></span></td>
|
||||
<td><span class="badge badge-primary"><?= (int) ($team['server_count'] ?? 0) ?></span></td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a href="/teams/<?= $team['id'] ?>" class="btn btn-xs" title="Manage">
|
||||
<i class="fas fa-cog"></i>
|
||||
</a>
|
||||
<button class="btn btn-xs btn-danger" title="Delete"
|
||||
onclick="deleteTeam(<?= $team['id'] ?>, '<?= htmlspecialchars(addslashes($team['name'])) ?>')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="deleteForm" method="POST" style="display:none">
|
||||
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function deleteTeam(id, name) {
|
||||
if (confirm('Delete team "' + name + '"?\nThis will remove access for all members.')) {
|
||||
const form = document.getElementById('deleteForm');
|
||||
form.action = '/teams/' + id + '/delete';
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
289
views/teams/show.php
Normal file
289
views/teams/show.php
Normal file
@@ -0,0 +1,289 @@
|
||||
<?php
|
||||
$team = $team ?? [];
|
||||
$members = $members ?? [];
|
||||
$servers = $servers ?? [];
|
||||
$availableUsers = $availableUsers ?? [];
|
||||
$availableServers = $availableServers ?? [];
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-header-left">
|
||||
<div class="back-link">
|
||||
<a href="/teams"><i class="fas fa-arrow-left"></i> Back to Teams</a>
|
||||
</div>
|
||||
<h1 class="page-title"><?= htmlspecialchars($team['name'] ?? '') ?></h1>
|
||||
<p class="page-subtitle"><?= htmlspecialchars($team['description'] ?? '') ?></p>
|
||||
</div>
|
||||
<div class="page-header-right">
|
||||
<button class="btn btn-secondary" onclick="editTeam()">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="deleteTeam()">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<div class="dashboard-card">
|
||||
<div class="card-header">
|
||||
<h2><i class="fas fa-users"></i> Members</h2>
|
||||
<span class="badge badge-info"><?= count($members) ?></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($members as $member): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?= htmlspecialchars($member['username']) ?></strong>
|
||||
<br><small class="text-muted"><?= htmlspecialchars($member['email']) ?></small>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-xs btn-danger"
|
||||
onclick="removeMember(<?= $member['user_id'] ?>, '<?= htmlspecialchars(addslashes($member['username'])) ?>')">
|
||||
<i class="fas fa-user-minus"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($availableUsers)): ?>
|
||||
<form class="form-inline" style="margin-top:1rem;display:flex;gap:0.5rem;align-items:end" onsubmit="addMember(event)">
|
||||
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||
<div class="form-group" style="flex:1;margin-bottom:0">
|
||||
<select id="addUserId" class="form-select" required>
|
||||
<option value="">Add user...</option>
|
||||
<?php foreach ($availableUsers as $user): ?>
|
||||
<option value="<?= $user['id'] ?>">
|
||||
<?= htmlspecialchars($user['username']) ?> (<?= htmlspecialchars($user['email']) ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-plus"></i> Add
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<div class="card-header">
|
||||
<h2><i class="fas fa-server"></i> Servers</h2>
|
||||
<span class="badge badge-primary"><?= count($servers) ?></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Server</th>
|
||||
<th>Role</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($servers as $s): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/servers/<?= $s['server_id'] ?>">
|
||||
<?= htmlspecialchars($s['server_name']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm role-select"
|
||||
onchange="updateServerRole(<?= $s['server_id'] ?>, this.value)"
|
||||
style="width:auto">
|
||||
<option value="viewer" <?= $s['role'] === 'viewer' ? 'selected' : '' ?>>Viewer</option>
|
||||
<option value="manager" <?= $s['role'] === 'manager' ? 'selected' : '' ?>>Manager</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-xs btn-danger"
|
||||
onclick="removeServer(<?= $s['server_id'] ?>, '<?= htmlspecialchars(addslashes($s['server_name'])) ?>')">
|
||||
<i class="fas fa-unlink"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($availableServers)): ?>
|
||||
<form class="form-inline" style="margin-top:1rem;display:flex;gap:0.5rem;align-items:end" onsubmit="addServer(event)">
|
||||
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||
<div class="form-group" style="flex:1;margin-bottom:0">
|
||||
<select id="addServerId" class="form-select" required>
|
||||
<option value="">Assign server...</option>
|
||||
<?php foreach ($availableServers as $s): ?>
|
||||
<option value="<?= $s['id'] ?>">
|
||||
<?= htmlspecialchars($s['name']) ?> (<?= htmlspecialchars($s['ip_address']) ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<select id="addServerRole" class="form-select" required>
|
||||
<option value="viewer">Viewer</option>
|
||||
<option value="manager">Manager</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-plus"></i> Assign
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="postForm" method="POST" 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 editTeam() {
|
||||
const name = prompt('Team name:', '<?= htmlspecialchars(addslashes($team['name'] ?? '')) ?>');
|
||||
if (!name) return;
|
||||
const desc = prompt('Description:', '<?= htmlspecialchars(addslashes($team['description'] ?? '')) ?>');
|
||||
|
||||
const form = new FormData();
|
||||
form.append('name', name);
|
||||
form.append('description', desc || '');
|
||||
form.append('_csrf_token', getCsrfToken());
|
||||
|
||||
fetch('/teams/<?= $team['id'] ?>/update', {
|
||||
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); }
|
||||
});
|
||||
}
|
||||
|
||||
function deleteTeam() {
|
||||
if (!confirm('Delete this team? Members will lose access to assigned servers.')) return;
|
||||
document.getElementById('postForm').action = '/teams/<?= $team['id'] ?>/delete';
|
||||
document.getElementById('postForm').submit();
|
||||
}
|
||||
|
||||
function addMember(e) {
|
||||
e.preventDefault();
|
||||
const userId = document.getElementById('addUserId').value;
|
||||
if (!userId) return;
|
||||
|
||||
const form = new FormData();
|
||||
form.append('user_id', userId);
|
||||
form.append('_csrf_token', getCsrfToken());
|
||||
|
||||
fetch('/teams/<?= $team['id'] ?>/add-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); }
|
||||
});
|
||||
}
|
||||
|
||||
function removeMember(userId, username) {
|
||||
if (!confirm('Remove "' + username + '" from this team?')) return;
|
||||
|
||||
const form = new FormData();
|
||||
form.append('user_id', userId);
|
||||
form.append('_csrf_token', getCsrfToken());
|
||||
|
||||
fetch('/teams/<?= $team['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); }
|
||||
});
|
||||
}
|
||||
|
||||
function addServer(e) {
|
||||
e.preventDefault();
|
||||
const serverId = document.getElementById('addServerId').value;
|
||||
const role = document.getElementById('addServerRole').value;
|
||||
if (!serverId) return;
|
||||
|
||||
const form = new FormData();
|
||||
form.append('server_id', serverId);
|
||||
form.append('role', role);
|
||||
form.append('_csrf_token', getCsrfToken());
|
||||
|
||||
fetch('/teams/<?= $team['id'] ?>/add-server', {
|
||||
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); }
|
||||
});
|
||||
}
|
||||
|
||||
function removeServer(serverId, name) {
|
||||
if (!confirm('Remove "' + name + '" from this team?')) return;
|
||||
|
||||
const form = new FormData();
|
||||
form.append('server_id', serverId);
|
||||
form.append('_csrf_token', getCsrfToken());
|
||||
|
||||
fetch('/teams/<?= $team['id'] ?>/remove-server', {
|
||||
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); }
|
||||
});
|
||||
}
|
||||
|
||||
function updateServerRole(serverId, role) {
|
||||
const form = new FormData();
|
||||
form.append('server_id', serverId);
|
||||
form.append('role', role);
|
||||
form.append('_csrf_token', getCsrfToken());
|
||||
|
||||
fetch('/teams/<?= $team['id'] ?>/update-server-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); }
|
||||
});
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user