Add views directory and UI improvements
- Add all view templates (servers, auth, dashboard, admin, layouts, errors, terminal) - Fix SSH double decryption bug in SSHService - Fix router parameter type casting for strict_types - Add missing error views (401, 403, 404) - Add nginx manager with file editing - Add SSL certificates management - Add database manager with phpMyAdmin-like interface - Add sidebar server accordion
This commit is contained in:
90
views/admin/audit.php
Executable file
90
views/admin/audit.php
Executable file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
$logs = $logs ?? [];
|
||||
$action = $action ?? '';
|
||||
$userId = $userId ?? '';
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Audit Logs</h1>
|
||||
<p class="page-subtitle">Comprehensive activity tracking and security monitoring</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-filters">
|
||||
<select id="actionFilter" class="form-select" onchange="applyFilters()">
|
||||
<option value="">All Actions</option>
|
||||
<option value="login_success" <?= $action === 'login_success' ? 'selected' : '' ?>>Login Success</option>
|
||||
<option value="login_failed" <?= $action === 'login_failed' ? 'selected' : '' ?>>Login Failed</option>
|
||||
<option value="server_created" <?= $action === 'server_created' ? 'selected' : '' ?>>Server Created</option>
|
||||
<option value="server_deleted" <?= $action === 'server_deleted' ? 'selected' : '' ?>>Server Deleted</option>
|
||||
<option value="credential_access" <?= $action === 'credential_access' ? 'selected' : '' ?>>Credential Access</option>
|
||||
<option value="command_executed" <?= $action === 'command_executed' ? 'selected' : '' ?>>Command Executed</option>
|
||||
<option value="user_created" <?= $action === 'user_created' ? 'selected' : '' ?>>User Created</option>
|
||||
<option value="user_deleted" <?= $action === 'user_deleted' ? 'selected' : '' ?>>User Deleted</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Entity</th>
|
||||
<th>Details</th>
|
||||
<th>IP Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($logs['data'] as $log): ?>
|
||||
<tr class="audit-row <?= str_contains($log['action'] ?? '', 'failed') ? 'audit-row-danger' : '' ?>">
|
||||
<td><?= htmlspecialchars($log['created_at'] ?? '') ?></td>
|
||||
<td><?= htmlspecialchars($log['actor_name'] ?? $log['username'] ?? 'System') ?></td>
|
||||
<td>
|
||||
<span class="badge badge-<?= match(true) {
|
||||
str_contains($log['action'] ?? '', 'failed') => 'danger',
|
||||
str_contains($log['action'] ?? '', 'success') => 'success',
|
||||
str_contains($log['action'] ?? '', 'deleted') => 'danger',
|
||||
str_contains($log['action'] ?? '', 'created') => 'success',
|
||||
default => 'info',
|
||||
} ?>">
|
||||
<?= htmlspecialchars(str_replace('_', ' ', $log['action'] ?? '')) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= htmlspecialchars(($log['entity_type'] ?? '') . ' #' . ($log['entity_id'] ?? '')) ?></td>
|
||||
<td>
|
||||
<?php if ($log['details'] ?? false): ?>
|
||||
<pre class="audit-details"><?= htmlspecialchars($log['details']) ?></pre>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><code><?= htmlspecialchars($log['ip_address'] ?? '') ?></code></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if ($logs['total_pages'] > 1): ?>
|
||||
<div class="pagination">
|
||||
<?php for ($i = 1; $i <= $logs['total_pages']; $i++): ?>
|
||||
<a href="?page=<?= $i ?>&action=<?= urlencode($action) ?>&user_id=<?= urlencode($userId) ?>"
|
||||
class="page-link <?= $i === ($logs['page'] ?? 1) ? 'active' : '' ?>">
|
||||
<?= $i ?>
|
||||
</a>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function applyFilters() {
|
||||
const action = document.getElementById('actionFilter').value;
|
||||
window.location.href = '/admin/audit?action=' + encodeURIComponent(action);
|
||||
}
|
||||
</script>
|
||||
123
views/admin/security.php
Executable file
123
views/admin/security.php
Executable file
@@ -0,0 +1,123 @@
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Security Settings</h1>
|
||||
<p class="page-subtitle">Configure security parameters and view status</p>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<div class="dashboard-card">
|
||||
<div class="card-header">
|
||||
<h2><i class="fas fa-lock"></i> Encryption Status</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="security-status">
|
||||
<div class="security-item">
|
||||
<span>Master Encryption Key</span>
|
||||
<?php
|
||||
$config = \ServerManager\Core\App::getConfig();
|
||||
$keyConfigured = !empty($config['encryption']['master_key']) ||
|
||||
(isset($config['encryption']['master_key_file']) && file_exists($config['encryption']['master_key_file']));
|
||||
?>
|
||||
<span class="badge badge-<?= $keyConfigured ? 'success' : 'danger' ?>">
|
||||
<?= $keyConfigured ? 'Configured' : 'Not Configured' ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="security-item">
|
||||
<span>Password Hashing</span>
|
||||
<span class="badge badge-success">Argon2id</span>
|
||||
</div>
|
||||
<div class="security-item">
|
||||
<span>AES-256-CBC Encryption</span>
|
||||
<span class="badge badge-success">Active</span>
|
||||
</div>
|
||||
<div class="security-item">
|
||||
<span>Session Configuration</span>
|
||||
<span class="badge badge-<?= $config['session']['secure'] ? 'success' : 'warning' ?>">
|
||||
<?= $config['session']['secure'] ? 'Secure' : 'HTTP' ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<div class="card-header">
|
||||
<h2><i class="fas fa-shield-alt"></i> Security Features</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="security-status">
|
||||
<div class="security-item">
|
||||
<span>CSRF Protection</span>
|
||||
<span class="badge badge-success">Enabled</span>
|
||||
</div>
|
||||
<div class="security-item">
|
||||
<span>XSS Prevention</span>
|
||||
<span class="badge badge-success">Enabled</span>
|
||||
</div>
|
||||
<div class="security-item">
|
||||
<span>Rate Limiting</span>
|
||||
<span class="badge badge-success">Enabled (<?= $config['rate_limit']['max_attempts'] ?> attempts/<?= $config['rate_limit']['decay_minutes'] ?>min)</span>
|
||||
</div>
|
||||
<div class="security-item">
|
||||
<span>Input Validation</span>
|
||||
<span class="badge badge-success">Enabled</span>
|
||||
</div>
|
||||
<div class="security-item">
|
||||
<span>Audit Logging</span>
|
||||
<span class="badge badge-success">Enabled</span>
|
||||
</div>
|
||||
<div class="security-item">
|
||||
<span>Session Security</span>
|
||||
<span class="badge badge-success">HTTP Only + SameSite</span>
|
||||
</div>
|
||||
<div class="security-item">
|
||||
<span>API Authentication</span>
|
||||
<span class="badge badge-success">Bearer Token</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<div class="card-header">
|
||||
<h2><i class="fas fa-key"></i> Master Key Management</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>Important:</strong> The master encryption key is stored outside the database.
|
||||
If lost, ALL encrypted SSH credentials will be unrecoverable.
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Key File Location</label>
|
||||
<code><?= htmlspecialchars($config['encryption']['master_key_file'] ?? '/etc/servermanager/master.key') ?></code>
|
||||
</div>
|
||||
|
||||
<div class="form-actions" style="margin-top: 1rem;">
|
||||
<button class="btn btn-warning" onclick="generateKey()">
|
||||
<i class="fas fa-key"></i> Generate New Master Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function generateKey() {
|
||||
if (!confirm('WARNING: Generating a new master key will make all existing encrypted credentials UNREADABLE. ' +
|
||||
'You will need to re-enter all SSH credentials. Continue?')) return;
|
||||
|
||||
fetch('/admin/security/generate-key', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||
},
|
||||
body: '_csrf_token=' + encodeURIComponent(document.querySelector('meta[name="csrf-token"]')?.content || ''),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
showToast(data.success ? 'success' : 'error', data.message);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
201
views/admin/users.php
Executable file
201
views/admin/users.php
Executable file
@@ -0,0 +1,201 @@
|
||||
<?php $users = $users ?? []; ?>
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">User Management</h1>
|
||||
<p class="page-subtitle">Manage administrator accounts</p>
|
||||
</div>
|
||||
<div class="page-header-right">
|
||||
<button class="btn btn-primary" onclick="showCreateUserModal()">
|
||||
<i class="fas fa-plus"></i> Add User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Status</th>
|
||||
<th>Last Login</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users['data'] as $user): ?>
|
||||
<tr>
|
||||
<td><?= $user['id'] ?></td>
|
||||
<td><?= htmlspecialchars($user['username']) ?></td>
|
||||
<td><?= htmlspecialchars($user['email']) ?></td>
|
||||
<td>
|
||||
<span class="badge badge-<?= $user['role'] === 'super_admin' ? 'danger' : ($user['role'] === 'admin' ? 'warning' : 'info') ?>">
|
||||
<?= htmlspecialchars(ucfirst(str_replace('_', ' ', $user['role']))) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge <?= $user['is_active'] ? 'status-online' : 'status-offline' ?>">
|
||||
<?= $user['is_active'] ? 'Active' : 'Inactive' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= $user['last_login_at'] ?? 'Never' ?></td>
|
||||
<td><?= date('Y-m-d', strtotime($user['created_at'] ?? '')) ?></td>
|
||||
<td>
|
||||
<button class="btn btn-xs" onclick="editUser(<?= $user['id'] ?>, '<?= htmlspecialchars(addslashes($user['username'])) ?>', '<?= htmlspecialchars(addslashes($user['email'])) ?>', '<?= $user['role'] ?>', <?= $user['is_active'] ?>)">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-danger" onclick="deleteUser(<?= $user['id'] ?>, '<?= htmlspecialchars(addslashes($user['username'])) ?>')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if ($users['total_pages'] > 1): ?>
|
||||
<div class="pagination">
|
||||
<?php for ($i = 1; $i <= $users['total_pages']; $i++): ?>
|
||||
<a href="?page=<?= $i ?>" class="page-link <?= $i === ($users['page'] ?? 1) ? 'active' : '' ?>">
|
||||
<?= $i ?>
|
||||
</a>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="userModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalTitle">Create User</h3>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="userForm">
|
||||
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||
<input type="hidden" id="userId" value="">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="modalUsername">Username</label>
|
||||
<input type="text" id="modalUsername" name="username" class="form-input" required minlength="3">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="modalEmail">Email</label>
|
||||
<input type="email" id="modalEmail" name="email" class="form-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="modalPassword">Password</label>
|
||||
<input type="password" id="modalPassword" name="password" class="form-input" minlength="8">
|
||||
<small class="form-hint" id="passwordHint">Minimum 8 characters</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="modalRole">Role</label>
|
||||
<select id="modalRole" name="role" class="form-select" required>
|
||||
<option value="operator">Operator</option>
|
||||
<option value="admin">Administrator</option>
|
||||
<option value="super_admin">Super Administrator</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="modalActive" name="is_active" value="1" checked>
|
||||
<span>Active</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" id="saveUserBtn" onclick="saveUser()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showCreateUserModal() {
|
||||
document.getElementById('modalTitle').textContent = 'Create User';
|
||||
document.getElementById('userId').value = '';
|
||||
document.getElementById('modalUsername').value = '';
|
||||
document.getElementById('modalEmail').value = '';
|
||||
document.getElementById('modalPassword').value = '';
|
||||
document.getElementById('modalPassword').required = true;
|
||||
document.getElementById('passwordHint').style.display = 'block';
|
||||
document.getElementById('modalRole').value = 'operator';
|
||||
document.getElementById('modalActive').checked = true;
|
||||
document.getElementById('saveUserBtn').textContent = 'Create';
|
||||
document.getElementById('userModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function editUser(id, username, email, role, isActive) {
|
||||
document.getElementById('modalTitle').textContent = 'Edit User';
|
||||
document.getElementById('userId').value = id;
|
||||
document.getElementById('modalUsername').value = username;
|
||||
document.getElementById('modalEmail').value = email;
|
||||
document.getElementById('modalPassword').value = '';
|
||||
document.getElementById('modalPassword').required = false;
|
||||
document.getElementById('passwordHint').style.display = 'none';
|
||||
document.getElementById('modalRole').value = role;
|
||||
document.getElementById('modalActive').checked = !!isActive;
|
||||
document.getElementById('saveUserBtn').textContent = 'Update';
|
||||
document.getElementById('userModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('userModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function saveUser() {
|
||||
const id = document.getElementById('userId').value;
|
||||
const formData = new FormData(document.getElementById('userForm'));
|
||||
|
||||
const isEdit = !!id;
|
||||
const url = isEdit ? '/admin/users/' + id + '/update' : '/admin/users/create';
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||
},
|
||||
body: new URLSearchParams(formData),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast('success', data.message);
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
showToast('error', data.message);
|
||||
}
|
||||
closeModal();
|
||||
})
|
||||
.catch(err => showToast('error', 'Failed to save user'));
|
||||
}
|
||||
|
||||
function deleteUser(id, username) {
|
||||
if (!confirm('Are you sure you want to delete user "' + username + '"?')) return;
|
||||
|
||||
fetch('/admin/users/' + id + '/delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||
},
|
||||
body: '_csrf_token=' + encodeURIComponent(document.querySelector('meta[name="csrf-token"]')?.content || ''),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) location.reload();
|
||||
else showToast('error', data.message);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user