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

@@ -8,6 +8,7 @@ use ServerManager\Core\App;
use ServerManager\Core\Session;
use ServerManager\Core\Validator;
use ServerManager\Models\Server;
use ServerManager\Models\User;
use ServerManager\Services\SSHService;
use ServerManager\Services\MonitoringService;
use ServerManager\Services\AuditService;
@@ -26,18 +27,47 @@ class ServerController
$this->auditService = new AuditService();
}
private function requireServerAccess(int $serverId, string $minRole = 'viewer'): ?array
{
$userId = (int) Session::get('user_id');
$server = $this->serverModel->findById($serverId);
if (!$server) {
$this->view->error(404);
}
$role = $this->serverModel->getUserRole($serverId, $userId);
if ($role === null) {
$this->view->error(404);
}
if ($minRole === 'manager' && $role !== 'owner' && $role !== 'manager') {
Session::setFlash('error', 'You do not have permission to perform this action.');
$this->view->redirect("/servers/{$serverId}");
}
return $server;
}
public function index(): void
{
$page = (int) ($_GET['page'] ?? 1);
$search = $_GET['search'] ?? '';
$status = $_GET['status'] ?? '';
$group = $_GET['group'] ?? '';
$userId = (int) Session::get('user_id');
$filters = [];
if ($search) $filters['search'] = $search;
if ($status) $filters['status'] = $status;
if ($group) $filters['group_name'] = $group;
$accessibleIds = $this->serverModel->getAccessibleServerIds($userId);
if (!empty($accessibleIds)) {
$filters['accessible_ids'] = $accessibleIds;
}
$servers = $this->serverModel->getAll($page, 15, $filters);
$groups = $this->serverModel->getGroups();
@@ -107,11 +137,9 @@ class ServerController
public function show(int $id): void
{
$server = $this->serverModel->findById($id);
$server = $this->requireServerAccess($id, 'viewer');
if (!$server) {
$this->view->error(404);
}
$role = $this->serverModel->getUserRole($id, (int) Session::get('user_id'));
$monitoringService = new MonitoringService();
$latestMetrics = $monitoringService->getLatestMetrics($id);
@@ -122,16 +150,13 @@ class ServerController
'server' => $server,
'metrics' => $latestMetrics,
'historicalMetrics' => $historicalMetrics,
'server_role' => $role,
]);
}
public function edit(int $id): void
{
$server = $this->serverModel->findById($id);
if (!$server) {
$this->view->error(404);
}
$server = $this->requireServerAccess($id, 'manager');
$groups = $this->serverModel->getGroups();
@@ -144,11 +169,7 @@ class ServerController
public function update(int $id): void
{
$server = $this->serverModel->findById($id);
if (!$server) {
$this->view->error(404);
}
$server = $this->requireServerAccess($id, 'manager');
$validator = new Validator();
$rules = [
@@ -194,11 +215,7 @@ class ServerController
public function delete(int $id): void
{
$server = $this->serverModel->findById($id);
if (!$server) {
$this->view->error(404);
}
$server = $this->requireServerAccess($id, 'manager');
$this->serverModel->delete($id);
@@ -218,6 +235,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$userId = (int) Session::get('user_id');
$role = $this->serverModel->getUserRole($id, $userId);
if ($role !== 'owner' && $role !== 'manager') {
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
}
$newState = $this->serverModel->toggleActive($id);
$this->auditService->log(
@@ -236,11 +259,7 @@ class ServerController
public function testConnection(int $id): void
{
$server = $this->serverModel->findById($id);
if (!$server) {
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$server = $this->requireServerAccess($id, 'viewer');
$ssh = new SSHService();
$result = $ssh->testConnection($server);
@@ -255,11 +274,7 @@ class ServerController
public function processes(int $id): void
{
$server = $this->serverModel->findById($id);
if (!$server) {
$this->view->error(404);
}
$server = $this->requireServerAccess($id, 'viewer');
try {
$ssh = new SSHService();
@@ -306,11 +321,7 @@ class ServerController
public function systemInfo(int $id): void
{
$server = $this->serverModel->findById($id);
if (!$server) {
$this->view->error(404);
}
$server = $this->requireServerAccess($id, 'viewer');
try {
$ssh = new SSHService();
@@ -335,11 +346,7 @@ class ServerController
public function logs(int $id): void
{
$server = $this->serverModel->findById($id);
if (!$server) {
$this->view->error(404);
}
$server = $this->requireServerAccess($id, 'viewer');
$logType = $_GET['type'] ?? 'syslog';
$lines = (int) ($_GET['lines'] ?? 100);
@@ -393,6 +400,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$userId = (int) Session::get('user_id');
$role = $this->serverModel->getUserRole($id, $userId);
if ($role !== 'owner' && $role !== 'manager') {
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
}
try {
$ssh = new SSHService();
if (!$ssh->connect($server)) {
@@ -425,6 +438,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$userId = (int) Session::get('user_id');
$role = $this->serverModel->getUserRole($id, $userId);
if ($role !== 'owner' && $role !== 'manager') {
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
}
try {
$ssh = new SSHService();
if (!$ssh->connect($server)) {
@@ -457,6 +476,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$userId = (int) Session::get('user_id');
$role = $this->serverModel->getUserRole($id, $userId);
if ($role !== 'owner' && $role !== 'manager') {
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
}
$service = $_POST['service'] ?? '';
if (empty($service)) {
$this->view->json(['success' => false, 'message' => 'Service name is required']);
@@ -501,6 +526,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$userId = (int) Session::get('user_id');
$role = $this->serverModel->getUserRole($id, $userId);
if ($role !== 'owner' && $role !== 'manager') {
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
}
$pid = (int) ($_POST['pid'] ?? 0);
$signal = (int) ($_POST['signal'] ?? 15);
@@ -542,11 +573,7 @@ class ServerController
public function nginx(int $id): void
{
$server = $this->serverModel->findById($id);
if (!$server) {
$this->view->error(404);
}
$server = $this->requireServerAccess($id, 'viewer');
try {
$ssh = new SSHService();
@@ -601,11 +628,7 @@ class ServerController
public function nginxGetFile(int $id): void
{
$server = $this->serverModel->findById($id);
if (!$server) {
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$server = $this->requireServerAccess($id, 'viewer');
$file = $_GET['file'] ?? '';
if (empty($file) || str_contains($file, '..')) {
@@ -658,6 +681,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$userId = (int) Session::get('user_id');
$role = $this->serverModel->getUserRole($id, $userId);
if ($role !== 'owner' && $role !== 'manager') {
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
}
$file = $_POST['file'] ?? '';
$content = $_POST['content'] ?? '';
@@ -712,6 +741,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$userId = (int) Session::get('user_id');
$role = $this->serverModel->getUserRole($id, $userId);
if ($role !== 'owner' && $role !== 'manager') {
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
}
$site = $_POST['site'] ?? '';
if (empty($site) || str_contains($site, '..') || str_contains($site, '/')) {
$this->view->json(['success' => false, 'message' => 'Invalid site name']);
@@ -750,6 +785,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$userId = (int) Session::get('user_id');
$role = $this->serverModel->getUserRole($id, $userId);
if ($role !== 'owner' && $role !== 'manager') {
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
}
$siteName = trim($_POST['site_name'] ?? '');
$siteRoot = trim($_POST['site_root'] ?? '');
$serverName = trim($_POST['server_name'] ?? '');
@@ -836,6 +877,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$userId = (int) Session::get('user_id');
$role = $this->serverModel->getUserRole($id, $userId);
if ($role !== 'owner' && $role !== 'manager') {
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
}
$site = $_POST['site'] ?? '';
$enable = (bool) ($_POST['enable'] ?? false);
@@ -879,6 +926,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$userId = (int) Session::get('user_id');
$role = $this->serverModel->getUserRole($id, $userId);
if ($role !== 'owner' && $role !== 'manager') {
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
}
$action = $_POST['action'] ?? '';
$allowed = ['restart', 'reload', 'test', 'start', 'stop'];
if (!in_array($action, $allowed, true)) {
@@ -921,11 +974,7 @@ class ServerController
public function ssl(int $id): void
{
$server = $this->serverModel->findById($id);
if (!$server) {
$this->view->error(404);
}
$server = $this->requireServerAccess($id, 'viewer');
try {
$ssh = new SSHService();
@@ -994,6 +1043,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$userId = (int) Session::get('user_id');
$role = $this->serverModel->getUserRole($id, $userId);
if ($role !== 'owner' && $role !== 'manager') {
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
}
$action = $_POST['action'] ?? '';
$allowed = ['install', 'request', 'renew', 'delete'];
if (!in_array($action, $allowed, true)) {
@@ -1086,11 +1141,7 @@ class ServerController
public function database(int $id): void
{
$server = $this->serverModel->findById($id);
if (!$server) {
$this->view->error(404);
}
$server = $this->requireServerAccess($id, 'viewer');
try {
$ssh = new SSHService();
@@ -1198,6 +1249,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$userId = (int) Session::get('user_id');
$role = $this->serverModel->getUserRole($id, $userId);
if ($role !== 'owner' && $role !== 'manager') {
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
}
$action = $_POST['action'] ?? '';
$allowed = ['query', 'tables', 'structure', 'browse', 'users', 'create_user', 'grant', 'revoke', 'drop_user', 'change_pass'];
if (!in_array($action, $allowed, true)) {
@@ -1462,9 +1519,15 @@ class ServerController
public function sidebarServers(): void
{
$userId = (int) Session::get('user_id');
$accessibleIds = $this->serverModel->getAccessibleServerIds($userId);
$servers = $this->serverModel->findAll();
$result = [];
foreach ($servers as $s) {
if (!in_array((int) $s['id'], $accessibleIds, true)) {
continue;
}
$result[] = [
'id' => $s['id'],
'name' => $s['name'],
@@ -1477,8 +1540,7 @@ class ServerController
public function services(int $id): void
{
$server = $this->serverModel->findById($id);
if (!$server) { $this->view->error(404); }
$server = $this->requireServerAccess($id, 'viewer');
try {
$ssh = new SSHService();
@@ -1527,6 +1589,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$userId = (int) Session::get('user_id');
$role = $this->serverModel->getUserRole($id, $userId);
if ($role !== 'owner' && $role !== 'manager') {
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
}
$action = $_POST['action'] ?? '';
$service = $_POST['service'] ?? '';
$allowed = ['start', 'stop', 'restart', 'reload', 'enable', 'disable'];
@@ -1558,8 +1626,7 @@ class ServerController
public function systemUsers(int $id): void
{
$server = $this->serverModel->findById($id);
if (!$server) { $this->view->error(404); }
$server = $this->requireServerAccess($id, 'viewer');
try {
$ssh = new SSHService();
@@ -1614,6 +1681,12 @@ class ServerController
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$userId = (int) Session::get('user_id');
$role = $this->serverModel->getUserRole($id, $userId);
if ($role !== 'owner' && $role !== 'manager') {
$this->view->json(['success' => false, 'message' => 'Forbidden'], 403);
}
$action = $_POST['action'] ?? '';
$allowed = ['add', 'delete', 'lock', 'unlock', 'addgroup'];
if (!in_array($action, $allowed, true)) {
@@ -1723,4 +1796,141 @@ class ServerController
'data' => $metrics,
]);
}
public function team(): void
{
$userId = (int) Session::get('user_id');
$servers = $this->serverModel->getOwnedServers($userId);
$result = [];
foreach ($servers as $server) {
$memberCount = count($this->serverModel->getTeam((int) $server['id']));
$result[] = [
'id' => $server['id'],
'name' => $server['name'],
'ip_address' => $server['ip_address'],
'member_count' => $memberCount,
'status' => $server['current_status'] ?? 'unknown',
];
}
$this->view->display('servers.team', [
'title' => 'Team Management - ServerManager',
'servers' => $result,
]);
}
public function teamServer(int $id): void
{
$userId = (int) Session::get('user_id');
$server = $this->serverModel->findById($id);
if (!$server || (int) $server['created_by'] !== $userId) {
$this->view->error(404);
}
$members = $this->serverModel->getTeam($id);
$userModel = new User();
$users = $userModel->getAll(1, 999);
$availableUsers = [];
$memberIds = array_map(fn($m) => $m['user_id'], $members);
foreach ($users['data'] as $u) {
if (!in_array((int) $u['id'], $memberIds, true)) {
$availableUsers[] = $u;
}
}
$this->view->display('servers.team_server', [
'title' => 'Team - ' . $server['name'] . ' - ServerManager',
'server' => $server,
'members' => $members,
'availableUsers' => $availableUsers,
]);
}
public function addTeamUser(int $id): void
{
$userId = (int) Session::get('user_id');
$server = $this->serverModel->findById($id);
if (!$server || (int) $server['created_by'] !== $userId) {
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$targetUserId = (int) ($_POST['user_id'] ?? 0);
$role = $_POST['role'] ?? 'viewer';
if ($targetUserId <= 0) {
$this->view->json(['success' => false, 'message' => 'Invalid user']);
}
if (!in_array($role, ['viewer', 'manager'], true)) {
$this->view->json(['success' => false, 'message' => 'Invalid role']);
}
if ($this->serverModel->addUser($id, $targetUserId, $role)) {
$this->auditService->log('team_user_added', 'server', $id, [
'target_user_id' => $targetUserId,
'role' => $role,
]);
$this->view->json(['success' => true, 'message' => 'User added to server.']);
}
$this->view->json(['success' => false, 'message' => 'User is already assigned to this server.']);
}
public function removeTeamUser(int $id): void
{
$userId = (int) Session::get('user_id');
$server = $this->serverModel->findById($id);
if (!$server || (int) $server['created_by'] !== $userId) {
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$targetUserId = (int) ($_POST['user_id'] ?? 0);
if ($targetUserId <= 0) {
$this->view->json(['success' => false, 'message' => 'Invalid user']);
}
$this->serverModel->removeUser($id, $targetUserId);
$this->auditService->log('team_user_removed', 'server', $id, [
'target_user_id' => $targetUserId,
]);
$this->view->json(['success' => true, 'message' => 'User removed from server.']);
}
public function updateTeamUserRole(int $id): void
{
$userId = (int) Session::get('user_id');
$server = $this->serverModel->findById($id);
if (!$server || (int) $server['created_by'] !== $userId) {
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
}
$targetUserId = (int) ($_POST['user_id'] ?? 0);
$role = $_POST['role'] ?? 'viewer';
if ($targetUserId <= 0) {
$this->view->json(['success' => false, 'message' => 'Invalid user']);
}
if (!in_array($role, ['viewer', 'manager'], true)) {
$this->view->json(['success' => false, 'message' => 'Invalid role']);
}
$this->serverModel->updateUserRole($id, $targetUserId, $role);
$this->auditService->log('team_user_role_updated', 'server', $id, [
'target_user_id' => $targetUserId,
'role' => $role,
]);
$this->view->json(['success' => true, 'message' => 'User role updated.']);
}
}