Add system update feature, git-ignore local configs, and fix homepage request logging

- Exclude config.json and admin.json in .gitignore so live settings/passwords are never overwritten by git
- Auto-generate config.json and admin.json with defaults if missing
- Add config.json.example and admin.json.example reference templates
- Add System Update page (/admin/update) in Admin Console to pull updates via Git with 1 click
- Log effective page name in index.php instead of literal 'auto'
- Add extra HAProxy/PFSense proxy headers to RequestLogger::getClientIp()
This commit is contained in:
2026-07-28 17:26:34 +02:00
parent 959f109f9e
commit 5884877f18
11 changed files with 205 additions and 13 deletions
+4
View File
@@ -21,6 +21,10 @@ Thumbs.db
*.tmp
*.temp
# Local configuration & credentials
config.json
admin/config/admin.json
# No content
content/
!content/.gitkeep
@@ -11,8 +11,5 @@
"session_timeout": 1800,
"max_login_attempts": 5,
"lockout_duration": 900
},
"config": {
"default_page": "auto"
}
}
+27 -2
View File
@@ -20,10 +20,35 @@ class AdminAuth
private function loadAdminConfig(): array
{
$path = $this->config['admin_config'];
$examplePath = dirname($path) . '/admin.json.example';
if (!file_exists($path)) {
return ['users' => [], 'security' => []];
if (file_exists($examplePath)) {
@copy($examplePath, $path);
} else {
$defaultAdminConfig = [
'users' => [
[
'username' => 'admin',
'password_hash' => password_hash('admin', PASSWORD_BCRYPT),
'role' => 'admin',
'created' => date('Y-m-d'),
]
],
'security' => [
'session_timeout' => 1800,
'max_login_attempts' => 5,
'lockout_duration' => 900,
]
];
$dir = dirname($path);
if (!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
@file_put_contents($path, json_encode($defaultAdminConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
}
$data = json_decode(file_get_contents($path), true);
$data = file_exists($path) ? json_decode(file_get_contents($path), true) : null;
return is_array($data) ? $data : ['users' => [], 'security' => []];
}
+8
View File
@@ -86,6 +86,11 @@ $layoutSidebarColor = $layoutThemeConfig['header_color'] ?? '#0a369d';
<i class="bi bi-journal-text"></i> Logs
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= ($route ?? '') === 'update' ? 'active' : '' ?>" href="/admin/update">
<i class="bi bi-cloud-arrow-down"></i> Update
</a>
</li>
<li class="nav-item mt-3">
<a class="nav-link" href="/" target="_blank">
<i class="bi bi-box-arrow-up-right"></i> Website bekijken
@@ -168,6 +173,9 @@ $layoutSidebarColor = $layoutThemeConfig['header_color'] ?? '#0a369d';
case 'logs':
require __DIR__ . '/pages/logs.php';
break;
case 'update':
require __DIR__ . '/pages/update.php';
break;
}
?>
</main>
+56
View File
@@ -0,0 +1,56 @@
<h2 class="mb-4"><i class="bi bi-cloud-arrow-down"></i> Systeem Update</h2>
<?php if (!empty($updateOutput)): ?>
<div class="card shadow-sm mb-4">
<div class="card-header bg-dark text-white">
<i class="bi bi-terminal"></i> Update Resultaten
</div>
<div class="card-body bg-dark text-light p-3">
<pre class="m-0 text-light" style="font-family: monospace; font-size: 0.9rem;"><?= htmlspecialchars($updateOutput) ?></pre>
</div>
</div>
<?php endif; ?>
<div class="card shadow-sm mb-4">
<div class="card-header">
<i class="bi bi-info-circle"></i> Systeeminformatie
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<strong>Huidige CMS Versie:</strong>
<span class="badge bg-primary ms-2"><?= htmlspecialchars($cmsVersion ?? '1.7.1') ?></span>
</div>
<div class="col-md-6 mb-3">
<strong>Configuratiestatus:</strong>
<span class="badge bg-success ms-2"><i class="bi bi-check-circle"></i> Lokaal afgeschermd (Git-safe)</span>
</div>
</div>
<p class="text-muted small mb-0">
Lokale configuratiebestanden (zoals <code>config.json</code> en <code>admin.json</code>) zijn uitgesloten van Git.
Hierdoor blijven je instellingen, wachtwoorden en content veilig behouden tijdens het bijwerken.
</p>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-header">
<i class="bi bi-arrow-repeat"></i> CodePress CMS Bijwerken
</div>
<div class="card-body">
<p>Haal automatisch de nieuwste CMS updates op via het Git repository.</p>
<?php if (!empty($gitBranch)): ?>
<div class="mb-3">
<small class="text-muted">Git branch: <code><?= htmlspecialchars($gitBranch) ?></code></small>
</div>
<?php endif; ?>
<form method="POST" action="/admin/update">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
<button type="submit" class="btn btn-primary btn-lg" onclick="return confirm('Weet je zeker dat je het systeem wilt bijwerken naar de nieuwste versie?')">
<i class="bi bi-cloud-download"></i> Systeem nu bijwerken
</button>
</form>
</div>
</div>
+2
View File
@@ -29,6 +29,8 @@ class RequestLogger
'HTTP_CF_CONNECTING_IP',
'HTTP_X_REAL_IP',
'HTTP_CLIENT_IP',
'HTTP_X_CLIENT_IP',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_FORWARDED_FOR',
+36
View File
@@ -2,6 +2,42 @@
// Simple configuration loader
$configJsonPath = __DIR__ . '/../../config.json';
$configExamplePath = __DIR__ . '/../../config.json.example';
// Auto-create config.json if it does not exist
if (!file_exists($configJsonPath)) {
if (file_exists($configExamplePath)) {
@copy($configExamplePath, $configJsonPath);
} else {
$defaultConfig = [
'site_title' => 'CodePress',
'content_dir' => 'content',
'templates_dir' => 'cms/templates',
'active_theme' => 'default',
'default_page' => 'auto',
'language' => [
'default' => 'nl',
'available' => ['nl', 'en']
],
'seo' => [
'description' => 'CodePress CMS - Lightweight file-based content management system',
'keywords' => 'cms, php, content management, file-based'
],
'author' => [
'name' => 'E. Noorlander',
'website' => 'https://noorlander.info'
],
'show_version' => true,
'enabled_plugins' => ['MQTTTracker', 'HTMLBlock'],
'features' => [
'auto_link_pages' => true,
'search_enabled' => true,
'breadcrumbs_enabled' => true
]
];
@file_put_contents($configJsonPath, json_encode($defaultConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
}
if (file_exists($configJsonPath)) {
$jsonContent = file_get_contents($configJsonPath);
+4 -5
View File
@@ -1,9 +1,9 @@
{
"site_title": "CodePress",
"content_dir": "content",
"templates_dir": "cms\/templates",
"templates_dir": "cms/templates",
"active_theme": "default",
"default_page": "newest",
"default_page": "auto",
"language": {
"default": "nl",
"available": [
@@ -17,13 +17,12 @@
},
"author": {
"name": "E. Noorlander",
"website": "https:\/\/noorlander.info"
"website": "https://noorlander.info"
},
"show_version": true,
"enabled_plugins": [
"MQTTTracker",
"HTMLBlock",
"test"
"HTMLBlock"
],
"features": {
"auto_link_pages": true,
+3 -1
View File
@@ -87,7 +87,8 @@ CodePress CMS is een lichtgewicht, file-based content management systeem gebouwd
- **Media beheer** - Uploaden en verwijderen van mediabestanden in `content/-assets/`
- **Gebruikersbeheer** - Gebruikers toevoegen, verwijderen, wachtwoorden wijzigen
- **Handleiding** - Ingebouwde documentatie met API referentie
- **Logging** - Activiteiten log en requests log met IP, pagina, domein; te bekijken en downloaden via `/admin/logs`
- **Systeem Update** - Eenvoudig de site bijwerken via `/admin/update` met één klik via Git pull
- **Git-safe Configuratie** - `config.json` en `admin/config/admin.json` worden automatisch gegenereerd als ze niet bestaan en zijn uitgesloten van Git, waardoor wachtwoorden en instellingen behouden blijven bij updates
- **Bot & AI blokkering** - Automatische 403 voor bekende bots en AI-crawlers
- Session-based authenticatie met bcrypt hashing
- CSRF-bescherming, brute-force lockout (5 pogingen, 15 min)
@@ -315,6 +316,7 @@ Media bestanden (afbeeldingen, PDFs, video, audio) kunnen in elke `content/` sub
| `users` | Gebruikersbeheer |
| `guide` | Handleiding (deze documentatie) |
| `logs` | Log viewer (activiteiten + requests) |
| `update` | Systeem update via Git pull (één-klik update) |
### Editor Functionaliteit
+63
View File
@@ -88,6 +88,10 @@ switch ($route) {
handleConfig($auth, $appConfig);
break;
case 'update':
handleUpdate($auth, $appConfig);
break;
case 'theme':
handleTheme($auth, $appConfig);
break;
@@ -1518,6 +1522,65 @@ function handleLogs(AdminAuth $auth, array $config): void
require __DIR__ . '/../admin/templates/layout.php';
}
function handleUpdate(AdminAuth $auth, array $config): void
{
$user = $auth->getCurrentUser();
$csrf = $auth->getCsrfToken();
$message = '';
$messageType = '';
$updateOutput = '';
// Load CMS version
$versionFile = $config['codepress_root'] . '/version.php';
$cmsVersion = '1.7.1';
if (file_exists($versionFile)) {
$verData = require $versionFile;
$cmsVersion = is_array($verData) ? ($verData['version'] ?? '1.7.1') : '1.7.1';
}
// Get git branch
$gitBranch = 'main';
if (function_exists('exec')) {
@exec('git rev-parse --abbrev-ref HEAD 2>&1', $branchOutput);
if (!empty($branchOutput[0]) && strpos($branchOutput[0], 'fatal') === false) {
$gitBranch = trim($branchOutput[0]);
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
$message = 'Ongeldige CSRF token.';
$messageType = 'danger';
} else {
$root = $config['codepress_root'];
$cmd = 'cd ' . escapeshellarg($root) . ' && git pull origin ' . escapeshellarg($gitBranch) . ' 2>&1';
$outputLines = [];
$returnCode = -1;
if (function_exists('exec')) {
@exec($cmd, $outputLines, $returnCode);
$updateOutput = implode("\n", $outputLines);
} else {
$updateOutput = 'exec() is uitgeschakeld op de PHP server.';
$returnCode = 1;
}
if ($returnCode === 0) {
adminLog($config, 'info', $user['username'] . ' voerde een succesvolle systeemupdate uit');
$message = 'Systeem succesvol bijgewerkt naar de nieuwste versie!';
$messageType = 'success';
} else {
adminLog($config, 'warning', $user['username'] . ' probeerde een systeemupdate uit te voeren: ' . $updateOutput);
$message = 'Fout bij het bijwerken van het systeem. Bekijk de resultaten hieronder.';
$messageType = 'danger';
}
}
}
$route = 'update';
require __DIR__ . '/../admin/templates/layout.php';
}
// --- Frontmatter helpers ---
function parseFrontmatterField(string $content, string $key, string $default = ''): string
+1 -1
View File
@@ -111,7 +111,7 @@ if (!str_starts_with($path, '/-media/') && !str_starts_with($path, '/-assets/'))
$requestLogFile = dirname(__DIR__) . '/admin/storage/logs/requests.log';
$logger = new RequestLogger($requestLogFile);
$logger->log(
$_GET['page'] ?? $config['default_page'] ?? 'index',
$_GET['page'] ?? $cms->getEffectiveDefaultPage(),
RequestLogger::getClientIp(),
$_SERVER['HTTP_USER_AGENT'] ?? '',
$_SERVER['HTTP_REFERER'] ?? '',