diff --git a/config/config.php b/config/config.php index 5813d05..6561c88 100755 --- a/config/config.php +++ b/config/config.php @@ -9,6 +9,7 @@ return [ 'debug' => filter_var($_ENV['APP_DEBUG'] ?? false, FILTER_VALIDATE_BOOLEAN), 'url' => $_ENV['APP_URL'] ?? 'http://localhost', 'timezone' => $_ENV['APP_TIMEZONE'] ?? 'America/Santo_Domingo', + 'asset_version' => $_ENV['ASSET_VERSION'] ?? '0.0.0', ], 'database' => [ diff --git a/public/assets/css/style.css b/public/assets/css/style.css index 6c04cc0..789c036 100755 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -121,7 +121,7 @@ code, pre { } /* App Container */ -.app-container { +.app-layout { display: flex; min-height: 100vh; } @@ -159,7 +159,7 @@ code, pre { min-height: var(--topbar-height); } -.sidebar-logo { +.sidebar-brand { display: flex; align-items: center; gap: 0.75rem; @@ -169,32 +169,13 @@ code, pre { white-space: nowrap; } -.sidebar-logo i { +.sidebar-brand i { font-size: 1.5rem; color: var(--accent); } -.sidebar-toggle { - background: none; - border: none; - color: var(--text-secondary); - cursor: pointer; - font-size: 1.1rem; - padding: 0.5rem; - border-radius: var(--radius-sm); - transition: color var(--transition); - flex-shrink: 0; -} - -.sidebar-toggle:hover { - color: var(--text-primary); -} - -.sidebar-toggle i, -.mobile-toggle i, -.nav-link i, -.nav-sub-item i, -.user-menu a i { +.nav-item i, +.nav-sub-item i { pointer-events: none; } @@ -204,61 +185,6 @@ code, pre { padding: 0.75rem 0; } -.nav-list { - list-style: none; -} - -.nav-item { - margin: 2px 0; -} - -.nav-link { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.65rem 1.25rem; - color: var(--text-secondary); - border-radius: 0; - transition: all var(--transition); - font-size: 0.92rem; - white-space: nowrap; - border-left: 3px solid transparent; -} - -.nav-link i { - width: 20px; - text-align: center; - flex-shrink: 0; -} - -.nav-link:hover { - color: var(--text-primary); - background: var(--bg-hover); -} - -.nav-link.active { - color: var(--accent); - background: var(--accent-light); - border-left-color: var(--accent); - font-weight: 500; -} - -.nav-divider { - height: 1px; - background: var(--border-color); - margin: 0.5rem 1rem; -} - -.nav-section { - padding: 0.5rem 1.25rem; - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); - font-weight: 600; - white-space: nowrap; -} - .sidebar-footer { border-top: 1px solid var(--border-color); padding: 0.75rem 1rem; @@ -321,21 +247,72 @@ code, pre { color: var(--text-primary); } +.nav-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.65rem 1.25rem; + color: var(--text-secondary); + border-radius: 0; + transition: all var(--transition); + font-size: 0.92rem; + white-space: nowrap; + border-left: 3px solid transparent; +} + +.nav-item i { + width: 20px; + text-align: center; + flex-shrink: 0; +} + +.nav-item:hover { + color: var(--text-primary); + background: var(--bg-hover); +} + +.nav-item.active { + color: var(--accent); + background: var(--accent-light); + border-left-color: var(--accent); + font-weight: 500; +} + +.nav-divider { + height: 1px; + background: var(--border-color); + margin: 0.5rem 1rem; +} + +.nav-section { + padding: 0.5rem 1.25rem; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); + font-weight: 600; + white-space: nowrap; +} + + + /* Collapsed state */ -.sidebar.collapsed .logo-text, -.sidebar.collapsed .nav-link span, +.sidebar.collapsed .nav-item span, +.sidebar.collapsed .nav-chevron, +.sidebar.collapsed .notification-badge, +.sidebar.collapsed .nav-divider, .sidebar.collapsed .nav-section, .sidebar.collapsed .user-details, .sidebar.collapsed .user-menu, -.sidebar.collapsed .nav-divider { +.sidebar.collapsed .sidebar-footer { display: none; } -.sidebar.collapsed .sidebar-logo { +.sidebar.collapsed .sidebar-brand { justify-content: center; } -.sidebar.collapsed .nav-link { +.sidebar.collapsed .nav-item { justify-content: center; padding: 0.65rem; } @@ -348,7 +325,7 @@ code, pre { Main Content ========================================================================== */ -.main-content { +.main-area { flex: 1; margin-left: var(--sidebar-width); transition: margin-left var(--transition); @@ -357,7 +334,7 @@ code, pre { flex-direction: column; } -.sidebar.collapsed ~ .main-content { +.sidebar.collapsed ~ .main-area { margin-left: var(--sidebar-collapsed); } @@ -365,7 +342,7 @@ code, pre { Top Bar ========================================================================== */ -.top-bar { +.topbar { height: var(--topbar-height); background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); @@ -378,48 +355,25 @@ code, pre { z-index: 50; } -.top-bar-left { +.topbar-left { display: flex; align-items: center; gap: 1rem; } -.mobile-toggle { - display: none; - background: none; - border: none; - color: var(--text-secondary); - font-size: 1.3rem; - cursor: pointer; - padding: 0.5rem 0.75rem; - border-radius: var(--radius-sm); - transition: color var(--transition); -} - -.mobile-toggle:hover { - color: var(--text-primary); - background: var(--bg-hover); -} - -.mobile-overlay { - display: none; -} - -.breadcrumb { - font-size: 0.92rem; - color: var(--text-secondary); - font-weight: 500; -} - -.top-bar-right { +.topbar-right { display: flex; align-items: center; gap: 0.5rem; } -.top-bar-actions { +.topbar-user { display: flex; - gap: 0.25rem; + align-items: center; + gap: 0.5rem; + font-size: 0.88rem; + color: var(--text-secondary); + white-space: nowrap; } .btn-icon { @@ -442,11 +396,15 @@ code, pre { Content Area ========================================================================== */ -.content-area { +.page-content { flex: 1; padding: 1.5rem; } +.container { + max-width: 100%; +} + /* ========================================================================== Page Header ========================================================================== */ @@ -2340,20 +2298,22 @@ textarea.form-input { width: 280px; } - .sidebar.mobile-open.collapsed .logo-text, - .sidebar.mobile-open.collapsed .nav-link span, + .sidebar.mobile-open.collapsed .nav-item span, + .sidebar.mobile-open.collapsed .nav-chevron, + .sidebar.mobile-open.collapsed .notification-badge, + .sidebar.mobile-open.collapsed .nav-divider, .sidebar.mobile-open.collapsed .nav-section, .sidebar.mobile-open.collapsed .user-details, .sidebar.mobile-open.collapsed .user-menu, - .sidebar.mobile-open.collapsed .nav-divider { + .sidebar.mobile-open.collapsed .sidebar-footer { display: flex; } - .sidebar.mobile-open.collapsed .sidebar-logo { + .sidebar.mobile-open.collapsed .sidebar-brand { justify-content: flex-start; } - .sidebar.mobile-open.collapsed .nav-link { + .sidebar.mobile-open.collapsed .nav-item { justify-content: flex-start; padding: 0.65rem 1.25rem; } @@ -2362,31 +2322,15 @@ textarea.form-input { justify-content: flex-start; } - .main-content { + .main-area { margin-left: 0 !important; } - .mobile-toggle { - display: block; - } - - .mobile-overlay { - display: none; - position: fixed; - inset: 0; - background: rgba(0,0,0,0.5); - z-index: 99; - } - - .mobile-overlay.active { - display: block; - } - - .content-area { + .page-content { padding: 1rem; } - .top-bar { + .topbar { padding: 0 1rem; } @@ -2414,7 +2358,7 @@ textarea.form-input { display: none; } - .nav-submenu { + .nav-sublist { background: rgba(0,0,0,0.1); } } @@ -2481,11 +2425,11 @@ textarea.form-input { ========================================================================== */ @media print { - .sidebar, .top-bar { + .sidebar, .topbar { display: none; } - .main-content { + .main-area { margin-left: 0; } } @@ -2780,7 +2724,7 @@ textarea.form-input { } /* Sidebar accordion */ -.nav-item-accordion .nav-link { +.nav-item-accordion .nav-item-toggle { display: flex; align-items: center; gap: 0.5rem; @@ -2797,20 +2741,13 @@ textarea.form-input { transform: rotate(90deg); } -.nav-submenu { +.nav-sublist { max-height: 0; overflow: hidden; transition: max-height 0.25s ease; background: rgba(0,0,0,0.15); } -.nav-submenu-loading { - padding: 0.5rem 1rem; - font-size: 0.78rem; - color: var(--text-muted); - text-align: center; -} - .nav-sub-item { display: flex; align-items: center; @@ -3330,7 +3267,7 @@ input[type="range"]::-moz-range-track { display: block; } -.notif-dropdown-header { +.notif-header { display: flex; align-items: center; justify-content: space-between; @@ -3338,11 +3275,9 @@ input[type="range"]::-moz-range-track { border-bottom: 1px solid var(--border-color); } -.notif-dropdown-body { +.notif-list { max-height: 360px; overflow-y: auto; } -.notif-dropdown-footer { - border-top: 1px solid var(--border-color); -} + diff --git a/public/assets/js/app.js b/public/assets/js/app.js index f124df8..fe7baad 100644 --- a/public/assets/js/app.js +++ b/public/assets/js/app.js @@ -12,14 +12,6 @@ const sidebar = document.getElementById('sidebar'); const sidebarToggle = document.getElementById('sidebarToggle'); - const mobileToggle = document.getElementById('mobileToggle'); - const mobileOverlay = document.getElementById('mobileOverlay'); - - function closeMobileSidebar() { - if (!sidebar) return; - sidebar.classList.remove('mobile-open'); - if (mobileOverlay) mobileOverlay.classList.remove('active'); - } if (sidebarToggle && sidebar) { sidebarToggle.addEventListener('click', function () { @@ -34,35 +26,6 @@ } catch (e) {} } - if (mobileToggle && sidebar) { - mobileToggle.addEventListener('click', function () { - const opening = !sidebar.classList.contains('mobile-open'); - if (opening) { - sidebar.classList.remove('collapsed'); - sidebar.classList.add('mobile-open'); - if (mobileOverlay) mobileOverlay.classList.add('active'); - } else { - closeMobileSidebar(); - } - }); - } - - if (sidebar) { - document.addEventListener('click', function (e) { - if (window.innerWidth <= 768 && - sidebar.classList.contains('mobile-open') && - !sidebar.contains(e.target) && - e.target !== mobileToggle && - e.target !== mobileOverlay) { - closeMobileSidebar(); - } - }); - } - - if (mobileOverlay) { - mobileOverlay.addEventListener('click', closeMobileSidebar); - } - /* ========================================================================== Theme Toggle ========================================================================== */ diff --git a/routes/web.php b/routes/web.php index 9d3da81..a4b32c9 100755 --- a/routes/web.php +++ b/routes/web.php @@ -31,6 +31,7 @@ $router->get('/login', [AuthController::class, 'loginForm']); $router->post('/login', [AuthController::class, 'login'], [CSRFMiddleware::class, RateLimitMiddleware::class]); $router->get('/register', [AuthController::class, 'registerForm']); $router->post('/register', [AuthController::class, 'register'], [CSRFMiddleware::class]); +$router->get('/logout', [AuthController::class, 'logout']); $router->post('/logout', [AuthController::class, 'logout'], [CSRFMiddleware::class]); $router->get('/profile', [AuthController::class, 'profile'], [AuthMiddleware::class]); @@ -72,6 +73,11 @@ $router->get('/servers/:id/metrics', [ServerController::class, 'metrics'], [Auth $router->post('/servers/:id/thresholds', [ServerController::class, 'saveThresholds'], [AuthMiddleware::class, CSRFMiddleware::class]); $router->get('/sidebar/servers', [ServerController::class, 'sidebarServers'], [AuthMiddleware::class]); +$router->get('/account', function () { + $view = \ServerManager\Core\App::getInstance()->getView(); + $view->redirect('/profile'); +}); + $router->get('/teams', [TeamController::class, 'index'], [AuthMiddleware::class, RoleMiddleware::class]); $router->get('/teams/search-users', [TeamController::class, 'searchUsers'], [AuthMiddleware::class, RoleMiddleware::class]); $router->get('/teams/create', [TeamController::class, 'create'], [AuthMiddleware::class, RoleMiddleware::class]); @@ -90,6 +96,16 @@ $router->post('/terminal/:id/execute', [TerminalController::class, 'execute'], [ $router->get('/terminal/:id/history', [TerminalController::class, 'history'], [AuthMiddleware::class]); $router->post('/terminal/:id/clear-history', [TerminalController::class, 'clearHistory'], [AuthMiddleware::class, CSRFMiddleware::class]); +$router->get('/admin', function () { + $view = \ServerManager\Core\App::getInstance()->getView(); + $view->redirect('/admin/audit'); +}, [AuthMiddleware::class, RoleMiddleware::class]); + +$router->get('/admin/activity', function () { + $view = \ServerManager\Core\App::getInstance()->getView(); + $view->redirect('/admin/audit'); +}, [AuthMiddleware::class, RoleMiddleware::class]); + $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]); diff --git a/src/Controllers/ApiController.php b/src/Controllers/ApiController.php index 9ac6c43..69e64a4 100755 --- a/src/Controllers/ApiController.php +++ b/src/Controllers/ApiController.php @@ -400,6 +400,7 @@ class ApiController 'error' => 'Command execution failed.', ], 500); } + } public function users(): void { diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php index 10fa48e..d68258e 100755 --- a/src/Controllers/AuthController.php +++ b/src/Controllers/AuthController.php @@ -67,6 +67,7 @@ class AuthController Session::set('username', $user['username']); Session::set('user_role', $user['role']); Session::set('user_email', $user['email']); + Session::set('api_token', $user['api_token']); Session::set('login_time', time()); $lifetime = App::getConfig()['session']['lifetime'] ?? 1440; diff --git a/src/Helpers/functions.php b/src/Helpers/functions.php index 74b6b54..8b376c8 100755 --- a/src/Helpers/functions.php +++ b/src/Helpers/functions.php @@ -47,6 +47,14 @@ if (!function_exists('asset')) { } } +if (!function_exists('asset_version')) { + function asset_version(): string + { + $config = \ServerManager\Core\App::getConfig(); + return $config['app']['asset_version'] ?? '0.0.0'; + } +} + if (!function_exists('url')) { function url(string $path = ''): string { @@ -90,11 +98,23 @@ if (!function_exists('auth')) { return null; } + $apiToken = \ServerManager\Core\Session::get('api_token'); + + if (empty($apiToken)) { + $userModel = new \ServerManager\Models\User(); + $user = $userModel->findById((int) \ServerManager\Core\Session::get('user_id')); + if ($user && !empty($user['api_token'])) { + $apiToken = $user['api_token']; + \ServerManager\Core\Session::set('api_token', $apiToken); + } + } + return [ 'id' => \ServerManager\Core\Session::get('user_id'), 'username' => \ServerManager\Core\Session::get('username'), 'role' => \ServerManager\Core\Session::get('user_role'), 'email' => \ServerManager\Core\Session::get('user_email'), + 'api_token' => $apiToken, ]; } } diff --git a/views/admin/audit.php b/views/admin/audit.php index 6f3016c..097952c 100755 --- a/views/admin/audit.php +++ b/views/admin/audit.php @@ -187,4 +187,4 @@ function selected(string $value, string $compare): string { - + diff --git a/views/admin/notifications.php b/views/admin/notifications.php index 86185f4..313575e 100644 --- a/views/admin/notifications.php +++ b/views/admin/notifications.php @@ -139,4 +139,4 @@ $data = $notifications['data'] ?? []; - + diff --git a/views/admin/security.php b/views/admin/security.php index 76576f3..11b2c6a 100755 --- a/views/admin/security.php +++ b/views/admin/security.php @@ -102,4 +102,4 @@ - + diff --git a/views/admin/users.php b/views/admin/users.php index 16d2319..fb3c683 100755 --- a/views/admin/users.php +++ b/views/admin/users.php @@ -121,4 +121,4 @@ - + diff --git a/views/dashboard/index.php b/views/dashboard/index.php index 03bc55b..544e121 100755 --- a/views/dashboard/index.php +++ b/views/dashboard/index.php @@ -165,5 +165,5 @@ $aggregatedMetrics = $aggregatedMetrics ?? []; - + diff --git a/views/layouts/auth.php b/views/layouts/auth.php index 565b29f..d14013b 100755 --- a/views/layouts/auth.php +++ b/views/layouts/auth.php @@ -5,7 +5,7 @@ <?= htmlspecialchars($title ?? 'ServerManager') ?> - + @@ -13,6 +13,6 @@
- + diff --git a/views/layouts/main.php b/views/layouts/main.php index 1629a42..f2ae26a 100755 --- a/views/layouts/main.php +++ b/views/layouts/main.php @@ -1,5 +1,4 @@ @@ -16,7 +19,7 @@ $currentRoute = $_SERVER['REQUEST_URI'] ?? '/'; <?= htmlspecialchars($title) ?> — ServerManager - + @@ -41,6 +44,12 @@ $currentRoute = $_SERVER['REQUEST_URI'] ?? '/'; Dashboard + + + + Notifications + + - + + + + + - + Teams - - - + + + Users + + + - Activity Log + Audit Logs - - - - - - Administration + + + Security - + + + App Version + + + + + Notifications + + + + + Policies + + + - - - - My Account + + + Teams + - - - Log out + + + + + + Download APK + + @@ -153,12 +201,12 @@ $currentRoute = $_SERVER['REQUEST_URI'] ?? '/';
- + - + diff --git a/views/notifications/index.php b/views/notifications/index.php index 1a9bfb5..b71368d 100644 --- a/views/notifications/index.php +++ b/views/notifications/index.php @@ -67,4 +67,4 @@ $data = $notifications ?? []; - + diff --git a/views/pages/landing.php b/views/pages/landing.php index da68394..177a18d 100644 --- a/views/pages/landing.php +++ b/views/pages/landing.php @@ -1,4 +1,4 @@ - +
diff --git a/views/pages/privacy.php b/views/pages/privacy.php index 321eb4a..0c61462 100644 --- a/views/pages/privacy.php +++ b/views/pages/privacy.php @@ -1,4 +1,4 @@ - +