Merge pull request 'fix: CSS sync, sidebar restore, asset versioning, route/auth fixes, admin dashboard' (#92) from fix/policy-container-width into master
Reviewed-on: #92 Reviewed-by: rafaga21 <rafaelminaya20@hotmail.com>
This commit was merged in pull request #92.
This commit is contained in:
@@ -301,10 +301,7 @@ code, pre {
|
|||||||
.sidebar.collapsed .nav-chevron,
|
.sidebar.collapsed .nav-chevron,
|
||||||
.sidebar.collapsed .notification-badge,
|
.sidebar.collapsed .notification-badge,
|
||||||
.sidebar.collapsed .nav-divider,
|
.sidebar.collapsed .nav-divider,
|
||||||
.sidebar.collapsed .nav-section,
|
.sidebar.collapsed .nav-section {
|
||||||
.sidebar.collapsed .user-details,
|
|
||||||
.sidebar.collapsed .user-menu,
|
|
||||||
.sidebar.collapsed .sidebar-footer {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,10 +314,6 @@ code, pre {
|
|||||||
padding: 0.65rem;
|
padding: 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.collapsed .user-info {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
Main Content
|
Main Content
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
@@ -374,6 +367,39 @@ code, pre {
|
|||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-family: inherit;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition: background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-user:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-user-dropdown {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-right {
|
||||||
|
right: 0;
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-header {
|
||||||
|
padding: 0.55rem 1rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-header small {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
@@ -1056,7 +1082,8 @@ textarea.form-input {
|
|||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown.active .dropdown-menu {
|
.dropdown.active .dropdown-menu,
|
||||||
|
.dropdown-menu.open {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2302,10 +2329,7 @@ textarea.form-input {
|
|||||||
.sidebar.mobile-open.collapsed .nav-chevron,
|
.sidebar.mobile-open.collapsed .nav-chevron,
|
||||||
.sidebar.mobile-open.collapsed .notification-badge,
|
.sidebar.mobile-open.collapsed .notification-badge,
|
||||||
.sidebar.mobile-open.collapsed .nav-divider,
|
.sidebar.mobile-open.collapsed .nav-divider,
|
||||||
.sidebar.mobile-open.collapsed .nav-section,
|
.sidebar.mobile-open.collapsed .nav-section {
|
||||||
.sidebar.mobile-open.collapsed .user-details,
|
|
||||||
.sidebar.mobile-open.collapsed .user-menu,
|
|
||||||
.sidebar.mobile-open.collapsed .sidebar-footer {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2318,10 +2342,6 @@ textarea.form-input {
|
|||||||
padding: 0.65rem 1.25rem;
|
padding: 0.65rem 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.mobile-open.collapsed .user-info {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-area {
|
.main-area {
|
||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
}
|
}
|
||||||
@@ -3280,4 +3300,21 @@ input[type="range"]::-moz-range-track {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notif-view-all {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
transition: background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notif-view-all:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ function openNotifications() {
|
|||||||
const dd = document.getElementById('notifDropdown');
|
const dd = document.getElementById('notifDropdown');
|
||||||
dd.classList.add('open');
|
dd.classList.add('open');
|
||||||
fetchNotifications();
|
fetchNotifications();
|
||||||
|
const userMenu = document.getElementById('userMenu');
|
||||||
|
if (userMenu) userMenu.classList.remove('open');
|
||||||
document.addEventListener('click', notifOutsideClick);
|
document.addEventListener('click', notifOutsideClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +33,11 @@ function notifOutsideClick(e) {
|
|||||||
if (!wrapper.contains(e.target)) {
|
if (!wrapper.contains(e.target)) {
|
||||||
closeNotifications();
|
closeNotifications();
|
||||||
}
|
}
|
||||||
|
const userWrapper = document.querySelector('.topbar-user-dropdown');
|
||||||
|
if (userWrapper && !userWrapper.contains(e.target)) {
|
||||||
|
const userMenu = document.getElementById('userMenu');
|
||||||
|
if (userMenu) userMenu.classList.remove('open');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchNotifications() {
|
function fetchNotifications() {
|
||||||
@@ -48,14 +55,14 @@ function fetchNotifications() {
|
|||||||
list.innerHTML = '<div style="padding:40px 20px;text-align:center;color:var(--text-muted)">Could not load notifications.</div>';
|
list.innerHTML = '<div style="padding:40px 20px;text-align:center;color:var(--text-muted)">Could not load notifications.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renderNotifications(d.data || [], d.unread_count || 0);
|
renderNotifications(d.data || [], d.unread_count || 0, d.pagination?.total || 0);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
list.innerHTML = '<div style="padding:40px 20px;text-align:center;color:var(--danger)">Failed to load notifications.</div>';
|
list.innerHTML = '<div style="padding:40px 20px;text-align:center;color:var(--danger)">Failed to load notifications.</div>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNotifications(items, unreadCount) {
|
function renderNotifications(items, unreadCount, total) {
|
||||||
const list = document.getElementById('notifList');
|
const list = document.getElementById('notifList');
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
@@ -85,6 +92,13 @@ function renderNotifications(items, unreadCount) {
|
|||||||
+ (n.is_read ? '' : '<div class="notif-dot"></div>')
|
+ (n.is_read ? '' : '<div class="notif-dot"></div>')
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (total > items.length) {
|
||||||
|
html += '<a href="/notifications" class="notif-view-all">'
|
||||||
|
+ 'View all notifications (' + total + ' total)'
|
||||||
|
+ '</a>';
|
||||||
|
}
|
||||||
|
|
||||||
list.innerHTML = html;
|
list.innerHTML = html;
|
||||||
|
|
||||||
document.getElementById('markAllReadBtn').style.display = unreadCount > 0 ? 'inline-flex' : 'none';
|
document.getElementById('markAllReadBtn').style.display = unreadCount > 0 ? 'inline-flex' : 'none';
|
||||||
@@ -200,3 +214,32 @@ function toggleServerList(e) {
|
|||||||
sub.style.maxHeight = sub.scrollHeight + 'px';
|
sub.style.maxHeight = sub.scrollHeight + 'px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleUserMenu(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const menu = document.getElementById('userMenu');
|
||||||
|
if (!menu) return;
|
||||||
|
const isOpen = menu.classList.contains('open');
|
||||||
|
document.querySelectorAll('.dropdown-menu.open').forEach(function(m) {
|
||||||
|
if (m !== menu) m.classList.remove('open');
|
||||||
|
});
|
||||||
|
if (!isOpen) {
|
||||||
|
menu.classList.add('open');
|
||||||
|
closeNotifications();
|
||||||
|
setTimeout(function() {
|
||||||
|
document.addEventListener('click', closeUserMenu);
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
menu.classList.remove('open');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeUserMenu(e) {
|
||||||
|
const menu = document.getElementById('userMenu');
|
||||||
|
if (!menu) return;
|
||||||
|
const wrapper = menu.closest('.topbar-user-dropdown');
|
||||||
|
if (wrapper && !wrapper.contains(e.target)) {
|
||||||
|
menu.classList.remove('open');
|
||||||
|
document.removeEventListener('click', closeUserMenu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -96,10 +96,7 @@ $router->post('/terminal/:id/execute', [TerminalController::class, 'execute'], [
|
|||||||
$router->get('/terminal/:id/history', [TerminalController::class, 'history'], [AuthMiddleware::class]);
|
$router->get('/terminal/:id/history', [TerminalController::class, 'history'], [AuthMiddleware::class]);
|
||||||
$router->post('/terminal/:id/clear-history', [TerminalController::class, 'clearHistory'], [AuthMiddleware::class, CSRFMiddleware::class]);
|
$router->post('/terminal/:id/clear-history', [TerminalController::class, 'clearHistory'], [AuthMiddleware::class, CSRFMiddleware::class]);
|
||||||
|
|
||||||
$router->get('/admin', function () {
|
$router->get('/admin', [AdminController::class, 'index'], [AuthMiddleware::class, RoleMiddleware::class]);
|
||||||
$view = \ServerManager\Core\App::getInstance()->getView();
|
|
||||||
$view->redirect('/admin/audit');
|
|
||||||
}, [AuthMiddleware::class, RoleMiddleware::class]);
|
|
||||||
|
|
||||||
$router->get('/admin/activity', function () {
|
$router->get('/admin/activity', function () {
|
||||||
$view = \ServerManager\Core\App::getInstance()->getView();
|
$view = \ServerManager\Core\App::getInstance()->getView();
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ class AdminController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function index(): void
|
||||||
|
{
|
||||||
|
$this->view->display('admin.index', [
|
||||||
|
'title' => 'Administration - ServerManager',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function users(): void
|
public function users(): void
|
||||||
{
|
{
|
||||||
$this->requireSuperAdmin();
|
$this->requireSuperAdmin();
|
||||||
|
|||||||
35
views/admin/index.php
Normal file
35
views/admin/index.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">Administration</h1>
|
||||||
|
<p class="page-subtitle">Manage system settings and configurations</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-grid">
|
||||||
|
<a href="/teams" class="action-card">
|
||||||
|
<i class="fas fa-users-cog"></i>
|
||||||
|
<span>Teams</span>
|
||||||
|
</a>
|
||||||
|
<a href="/admin/users" class="action-card">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
<span>Users</span>
|
||||||
|
</a>
|
||||||
|
<a href="/admin/audit" class="action-card">
|
||||||
|
<i class="fas fa-history"></i>
|
||||||
|
<span>Audit Logs</span>
|
||||||
|
</a>
|
||||||
|
<a href="/admin/security" class="action-card">
|
||||||
|
<i class="fas fa-shield-alt"></i>
|
||||||
|
<span>Security</span>
|
||||||
|
</a>
|
||||||
|
<a href="/admin/app-version" class="action-card">
|
||||||
|
<i class="fas fa-mobile-alt"></i>
|
||||||
|
<span>App Version</span>
|
||||||
|
</a>
|
||||||
|
<a href="/admin/notifications" class="action-card">
|
||||||
|
<i class="fas fa-bell"></i>
|
||||||
|
<span>Notifications</span>
|
||||||
|
</a>
|
||||||
|
<a href="/admin/policies" class="action-card">
|
||||||
|
<i class="fas fa-file-contract"></i>
|
||||||
|
<span>Policies</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
@@ -44,12 +44,6 @@ if (!isset($user) || $user === null) {
|
|||||||
<span>Dashboard</span>
|
<span>Dashboard</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Notifications -->
|
|
||||||
<a href="/notifications" class="nav-item <?= str_starts_with($currentRoute, '/notifications') ? 'active' : '' ?>">
|
|
||||||
<i class="fas fa-bell"></i>
|
|
||||||
<span>Notifications</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- Servers -->
|
<!-- Servers -->
|
||||||
<div class="nav-item-accordion <?= str_starts_with($currentRoute, '/servers') || str_starts_with($currentRoute, '/terminal') ? 'expanded' : '' ?>">
|
<div class="nav-item-accordion <?= str_starts_with($currentRoute, '/servers') || str_starts_with($currentRoute, '/terminal') ? 'expanded' : '' ?>">
|
||||||
<a href="#" class="nav-item nav-item-toggle <?= str_starts_with($currentRoute, '/servers') ? 'active' : '' ?>" onclick="toggleServerList(event)">
|
<a href="#" class="nav-item nav-item-toggle <?= str_starts_with($currentRoute, '/servers') ? 'active' : '' ?>" onclick="toggleServerList(event)">
|
||||||
@@ -67,39 +61,9 @@ if (!isset($user) || $user === null) {
|
|||||||
<hr class="nav-divider">
|
<hr class="nav-divider">
|
||||||
<div class="nav-section">Administration</div>
|
<div class="nav-section">Administration</div>
|
||||||
|
|
||||||
<a href="/teams" class="nav-item <?= str_starts_with($currentRoute, '/teams') ? 'active' : '' ?>">
|
<a href="/admin" class="nav-item <?= str_starts_with($currentRoute, '/admin') ? 'active' : '' ?>">
|
||||||
<i class="fas fa-users-cog"></i>
|
<i class="fas fa-cog"></i>
|
||||||
<span>Teams</span>
|
<span>Administration</span>
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="/admin/users" class="nav-item <?= str_starts_with($currentRoute, '/admin/users') ? 'active' : '' ?>">
|
|
||||||
<i class="fas fa-users"></i>
|
|
||||||
<span>Users</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="/admin/audit" class="nav-item <?= str_starts_with($currentRoute, '/admin/audit') ? 'active' : '' ?>">
|
|
||||||
<i class="fas fa-history"></i>
|
|
||||||
<span>Audit Logs</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="/admin/security" class="nav-item <?= str_starts_with($currentRoute, '/admin/security') ? 'active' : '' ?>">
|
|
||||||
<i class="fas fa-shield-alt"></i>
|
|
||||||
<span>Security</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="/admin/app-version" class="nav-item <?= str_starts_with($currentRoute, '/admin/app-version') ? 'active' : '' ?>">
|
|
||||||
<i class="fas fa-mobile-alt"></i>
|
|
||||||
<span>App Version</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="/admin/notifications" class="nav-item <?= str_starts_with($currentRoute, '/admin/notifications') ? 'active' : '' ?>">
|
|
||||||
<i class="fas fa-bell"></i>
|
|
||||||
<span>Notifications</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="/admin/policies" class="nav-item <?= str_starts_with($currentRoute, '/admin/policies') ? 'active' : '' ?>">
|
|
||||||
<i class="fas fa-file-contract"></i>
|
|
||||||
<span>Policies</span>
|
|
||||||
</a>
|
</a>
|
||||||
<?php elseif (is_admin()): ?>
|
<?php elseif (is_admin()): ?>
|
||||||
<hr class="nav-divider">
|
<hr class="nav-divider">
|
||||||
@@ -120,22 +84,7 @@ if (!isset($user) || $user === null) {
|
|||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="sidebar-footer">
|
|
||||||
<div class="user-info">
|
|
||||||
<div class="user-avatar"><?= strtoupper(substr($user['username'] ?? '?', 0, 2)) ?></div>
|
|
||||||
<div class="user-details">
|
|
||||||
<span class="user-name"><?= htmlspecialchars($user['username'] ?? 'Guest') ?></span>
|
|
||||||
<span class="user-role"><?= htmlspecialchars(ucfirst(str_replace('_', ' ', $user['role'] ?? 'guest'))) ?></span>
|
|
||||||
</div>
|
|
||||||
<div class="user-menu">
|
|
||||||
<a href="/profile" title="Profile"><i class="fas fa-user-cog"></i></a>
|
|
||||||
<form method="POST" action="/logout" class="logout-form" style="display:inline">
|
|
||||||
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
|
||||||
<button type="submit" title="Logout" style="background:none;border:none;color:inherit;cursor:pointer;padding:0;font:inherit;line-height:1"><i class="fas fa-sign-out-alt"></i></button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
@@ -171,10 +120,28 @@ if (!isset($user) || $user === null) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="topbar-user">
|
<div class="dropdown topbar-user-dropdown">
|
||||||
|
<button class="topbar-user" onclick="toggleUserMenu(event)">
|
||||||
<i class="fas fa-user-circle"></i>
|
<i class="fas fa-user-circle"></i>
|
||||||
<?= htmlspecialchars($user['username'] ?? 'Guest') ?>
|
<?= htmlspecialchars($user['username'] ?? 'Guest') ?>
|
||||||
</span>
|
<i class="fas fa-chevron-down" style="font-size:0.7rem;margin-left:4px"></i>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right" id="userMenu">
|
||||||
|
<div class="dropdown-header">
|
||||||
|
<strong><?= htmlspecialchars($user['username'] ?? 'Guest') ?></strong>
|
||||||
|
<small><?= htmlspecialchars(ucfirst(str_replace('_', ' ', $user['role'] ?? 'guest'))) ?></small>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a href="/profile" class="dropdown-item"><i class="fas fa-user-cog"></i> My Account</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<form method="POST" action="/logout" style="padding:0;margin:0">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||||
|
<button type="submit" class="dropdown-item" style="width:100%;text-align:left;border:none;background:none;cursor:pointer;font:inherit">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> Log out
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user