diff --git a/README.en.md b/README.en.md
index ff2cbae..bbd7b07 100644
--- a/README.en.md
+++ b/README.en.md
@@ -17,6 +17,7 @@ A lightweight, file-based content management system built with PHP.
- ⚙️ **JSON Configuration** - Easy configuration via JSON
- 🎨 **Bootstrap 5** - Modern UI framework
- 🔒 **Security** - Secure content management (100/100 security score)
+- 🛡️ **Admin Console** - Built-in admin panel for content, config, plugins and user management
## 🚀 Quick Start
@@ -47,6 +48,13 @@ codepress/
│ ├── markdown_content.mustache
│ ├── php_content.mustache
│ └── html_content.mustache
+├── admin-console/ # Admin panel
+│ ├── config/
+│ │ ├── app.php # Admin configuration
+│ │ └── admin.json # Users & security
+│ ├── src/
+│ │ └── AdminAuth.php # Authentication service
+│ └── templates/ # Admin templates
├── public/ # Web root
│ ├── assets/
│ │ ├── css/
@@ -56,7 +64,8 @@ codepress/
│ │ ├── nl.homepage.md # Dutch homepage
│ │ ├── en.homepage.md # English homepage
│ │ └── [lang].[page].md # Multi-language pages
-│ └── index.php # Entry point
+│ ├── index.php # Website entry point
+│ └── admin.php # Admin entry point
├── config.json # Configuration
├── version.php # Version tracking
└── README.md # This file
@@ -315,6 +324,28 @@ See [pentest/PENTEST.md](pentest/PENTEST.md) for detailed security report.
See [function-test/test-report.md](function-test/test-report.md) for detailed test results.
+## 🛡️ Admin Console
+
+CodePress includes a built-in admin panel for managing your website.
+
+**Access:** `/admin.php` | **Default login:** `admin` / `admin`
+
+### Modules
+- **Dashboard** - Overview with statistics and quick actions
+- **Content** - Browse, create, edit and delete files
+- **Configuration** - Edit `config.json` with JSON validation
+- **Plugins** - Overview of installed plugins
+- **Users** - Add, remove users and change passwords
+
+### Security
+- Session-based authentication with bcrypt password hashing
+- CSRF protection on all forms
+- Brute-force protection (5 attempts, 15 min lockout)
+- Path traversal protection
+- Session timeout (30 min)
+
+> **Important:** Change the default password immediately after installation via Users.
+
## 📖 Documentation
- **[Guide (NL)](guide/nl.codepress.md)** - Dutch documentation
diff --git a/README.md b/README.md
index 141f12d..d7183fb 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ Een lichtgewicht, file-based content management systeem gebouwd met PHP.
- ⚙️ **JSON Configuratie** - Eenvoudige configuratie via JSON
- 🎨 **Bootstrap 5** - Moderne UI framework
- 🔒 **Security** - Beveiligde content management
+- 🛡️ **Admin Console** - Ingebouwd admin paneel voor content, config, plugins en gebruikersbeheer
## 🚀 Quick Start
@@ -47,12 +48,20 @@ codepress/
│ │ ├── pagina1.md
│ │ └── pagina2.php
│ └── homepage.md
+├── admin-console/ # Admin paneel
+│ ├── config/
+│ │ ├── app.php # Admin configuratie
+│ │ └── admin.json # Gebruikers & security
+│ ├── src/
+│ │ └── AdminAuth.php # Authenticatie service
+│ └── templates/ # Admin templates
├── public/ # Web root
│ ├── assets/
│ │ ├── css/
│ │ ├── js/
│ │ └── favicon.svg
-│ └── index.php
+│ ├── index.php # Website entry point
+│ └── admin.php # Admin entry point
├── config.json # Configuratie
└── README.md
```
@@ -230,6 +239,28 @@ Hoofd CMS class die alle content management functionaliteit beheert.
- File metadata tracking
- Responsive template rendering
+## 🛡️ Admin Console
+
+CodePress bevat een ingebouwd admin paneel voor het beheren van je website.
+
+**Toegang:** `/admin.php` | **Standaard login:** `admin` / `admin`
+
+### Modules
+- **Dashboard** - Overzicht met statistieken en snelle acties
+- **Content** - Bestanden browsen, aanmaken, bewerken en verwijderen
+- **Configuratie** - `config.json` bewerken met JSON-validatie
+- **Plugins** - Overzicht van geinstalleerde plugins
+- **Gebruikers** - Gebruikers toevoegen, verwijderen en wachtwoorden wijzigen
+
+### Beveiliging
+- Session-based authenticatie met bcrypt password hashing
+- CSRF-bescherming op alle formulieren
+- Brute-force bescherming (5 pogingen, 15 min lockout)
+- Path traversal bescherming
+- Session timeout (30 min)
+
+> **Belangrijk:** Wijzig het standaard wachtwoord direct na installatie via Gebruikers.
+
## 📖 Documentatie
- **[Handleiding (NL)](guide/nl.md)** - Gedetailleerde handleiding
diff --git a/admin-console/composer.json b/admin-console/composer.json
deleted file mode 100644
index e6fbb08..0000000
--- a/admin-console/composer.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "name": "codepress/admin-console",
- "description": "Admin Console for CodePress CMS",
- "type": "project",
- "require": {
- "php": ">=8.4",
- "firebase/php-jwt": "^6.10",
- "phpmailer/phpmailer": "^6.9",
- "monolog/monolog": "^3.5"
- },
- "require-dev": {
- "phpunit/phpunit": "^11.0",
- "squizlabs/php_codesniffer": "^3.10"
- },
- "autoload": {
- "psr-4": {
- "CodePress\\Admin\\": "src/"
- }
- },
- "autoload-dev": {
- "psr-4": {
- "CodePress\\Admin\\Tests\\": "tests/"
- }
- },
- "scripts": {
- "start": "php -S localhost:8081 -t public",
- "test": "phpunit",
- "lint": "phpcs --standard=PSR12 src/",
- "lint-fix": "phpcbf --standard=PSR12 src/"
- },
- "license": "MIT",
- "authors": [
- {
- "name": "Edwin Noorlander",
- "email": "edwin@noorlander.info"
- }
- ],
- "minimum-stability": "stable",
- "prefer-stable": true
-}
\ No newline at end of file
diff --git a/admin-console/config/admin.json b/admin-console/config/admin.json
new file mode 100644
index 0000000..0526dfc
--- /dev/null
+++ b/admin-console/config/admin.json
@@ -0,0 +1,15 @@
+{
+ "users": [
+ {
+ "username": "admin",
+ "password_hash": "$2y$12$nUpoaCNZZFL8kOTHNC85q.dTy0hWRmPoF3dAn4GcvSXERMioYr5b6",
+ "role": "admin",
+ "created": "2025-01-01"
+ }
+ ],
+ "security": {
+ "session_timeout": 1800,
+ "max_login_attempts": 5,
+ "lockout_duration": 900
+ }
+}
diff --git a/admin-console/config/app.php b/admin-console/config/app.php
index 9aa6c2a..5bebf66 100644
--- a/admin-console/config/app.php
+++ b/admin-console/config/app.php
@@ -1,57 +1,17 @@
'CodePress Admin Console',
+ 'name' => 'CodePress Admin',
'version' => '1.0.0',
'debug' => $_ENV['APP_DEBUG'] ?? false,
'timezone' => 'Europe/Amsterdam',
- // Security
- 'security' => [
- 'jwt_secret' => $_ENV['JWT_SECRET'] ?? throw new \RuntimeException('JWT_SECRET environment variable must be set'),
- 'jwt_expiration' => 3600, // 1 hour
- 'session_timeout' => 1800, // 30 minutes
- 'max_login_attempts' => 5,
- 'lockout_duration' => 900, // 15 minutes
- ],
-
- // Database
- 'database' => [
- 'type' => 'sqlite',
- 'path' => __DIR__ . '/../database/admin.db',
- 'backup_path' => __DIR__ . '/../storage/backups/',
- ],
-
- // CodePress Integration
- 'codepress' => [
- 'path' => __DIR__ . '/../../',
- 'content_dir' => __DIR__ . '/../../public/content/',
- 'templates_dir' => __DIR__ . '/../../engine/templates/',
- 'plugins_dir' => __DIR__ . '/../../plugins/',
- ],
-
- // Email
- 'mail' => [
- 'host' => $_ENV['MAIL_HOST'] ?? 'localhost',
- 'port' => $_ENV['MAIL_PORT'] ?? 587,
- 'username' => $_ENV['MAIL_USERNAME'] ?? '',
- 'password' => $_ENV['MAIL_PASSWORD'] ?? '',
- 'from' => $_ENV['MAIL_FROM'] ?? 'admin@codepress.local',
- 'from_name' => 'CodePress Admin',
- ],
-
- // Storage
- 'storage' => [
- 'uploads_path' => __DIR__ . '/../storage/uploads/',
- 'logs_path' => __DIR__ . '/../storage/logs/',
- 'cache_path' => __DIR__ . '/../storage/cache/',
- ],
-
- // UI Settings
- 'ui' => [
- 'theme' => 'bootstrap',
- 'items_per_page' => 20,
- 'date_format' => 'd-m-Y H:i',
- 'timezone' => 'Europe/Amsterdam',
- ],
-];
\ No newline at end of file
+ // Paths
+ 'admin_root' => __DIR__ . '/../',
+ 'codepress_root' => __DIR__ . '/../../',
+ 'content_dir' => __DIR__ . '/../../content/',
+ 'config_json' => __DIR__ . '/../../config.json',
+ 'plugins_dir' => __DIR__ . '/../../plugins/',
+ 'admin_config' => __DIR__ . '/admin.json',
+ 'log_file' => __DIR__ . '/../storage/logs/admin.log',
+];
diff --git a/admin-console/src/AdminAuth.php b/admin-console/src/AdminAuth.php
new file mode 100644
index 0000000..f9e0b9d
--- /dev/null
+++ b/admin-console/src/AdminAuth.php
@@ -0,0 +1,279 @@
+config = $appConfig;
+ $this->adminConfig = $this->loadAdminConfig();
+ $this->lockFile = dirname($appConfig['log_file']) . '/login_attempts.json';
+ $this->startSession();
+ }
+
+ private function loadAdminConfig(): array
+ {
+ $path = $this->config['admin_config'];
+ if (!file_exists($path)) {
+ return ['users' => [], 'security' => []];
+ }
+ $data = json_decode(file_get_contents($path), true);
+ return is_array($data) ? $data : ['users' => [], 'security' => []];
+ }
+
+ public function saveAdminConfig(): void
+ {
+ file_put_contents(
+ $this->config['admin_config'],
+ json_encode($this->adminConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
+ );
+ }
+
+ private function startSession(): void
+ {
+ if (session_status() === PHP_SESSION_NONE) {
+ $timeout = $this->adminConfig['security']['session_timeout'] ?? 1800;
+ session_set_cookie_params([
+ 'lifetime' => $timeout,
+ 'path' => '/',
+ 'secure' => isset($_SERVER['HTTPS']),
+ 'httponly' => true,
+ 'samesite' => 'Lax'
+ ]);
+ session_start();
+ }
+
+ // Check session timeout
+ if (isset($_SESSION['admin_last_activity'])) {
+ $timeout = $this->adminConfig['security']['session_timeout'] ?? 1800;
+ if (time() - $_SESSION['admin_last_activity'] > $timeout) {
+ $this->logout();
+ return;
+ }
+ }
+
+ if ($this->isAuthenticated()) {
+ $_SESSION['admin_last_activity'] = time();
+ }
+ }
+
+ public function login(string $username, string $password): array
+ {
+ // Check brute-force lockout
+ $lockout = $this->checkLockout($username);
+ if ($lockout['locked']) {
+ return [
+ 'success' => false,
+ 'message' => 'Account tijdelijk vergrendeld. Probeer over ' . $lockout['remaining'] . ' seconden opnieuw.'
+ ];
+ }
+
+ // Find user
+ $user = $this->findUser($username);
+ if (!$user || !password_verify($password, $user['password_hash'])) {
+ $this->recordFailedAttempt($username);
+ $this->log('warning', "Mislukte inlogpoging: {$username}");
+ return ['success' => false, 'message' => 'Onjuiste gebruikersnaam of wachtwoord.'];
+ }
+
+ // Success - clear failed attempts
+ $this->clearFailedAttempts($username);
+
+ // Set session
+ $_SESSION['admin_user'] = $username;
+ $_SESSION['admin_role'] = $user['role'] ?? 'admin';
+ $_SESSION['admin_last_activity'] = time();
+ $_SESSION['admin_csrf_token'] = bin2hex(random_bytes(32));
+
+ $this->log('info', "Ingelogd: {$username}");
+ return ['success' => true, 'message' => 'Ingelogd.'];
+ }
+
+ public function logout(): void
+ {
+ $user = $_SESSION['admin_user'] ?? 'unknown';
+ $_SESSION = [];
+ if (ini_get('session.use_cookies')) {
+ $params = session_get_cookie_params();
+ setcookie(session_name(), '', time() - 42000,
+ $params['path'], $params['domain'],
+ $params['secure'], $params['httponly']
+ );
+ }
+ session_destroy();
+ $this->log('info', "Uitgelogd: {$user}");
+ }
+
+ public function isAuthenticated(): bool
+ {
+ return isset($_SESSION['admin_user']);
+ }
+
+ public function getCurrentUser(): ?array
+ {
+ if (!$this->isAuthenticated()) {
+ return null;
+ }
+ return [
+ 'username' => $_SESSION['admin_user'],
+ 'role' => $_SESSION['admin_role'] ?? 'admin'
+ ];
+ }
+
+ public function getCsrfToken(): string
+ {
+ if (!isset($_SESSION['admin_csrf_token'])) {
+ $_SESSION['admin_csrf_token'] = bin2hex(random_bytes(32));
+ }
+ return $_SESSION['admin_csrf_token'];
+ }
+
+ public function verifyCsrf(string $token): bool
+ {
+ return isset($_SESSION['admin_csrf_token']) && hash_equals($_SESSION['admin_csrf_token'], $token);
+ }
+
+ // --- User Management ---
+
+ public function getUsers(): array
+ {
+ return array_map(function ($u) {
+ return [
+ 'username' => $u['username'],
+ 'role' => $u['role'] ?? 'admin',
+ 'created' => $u['created'] ?? ''
+ ];
+ }, $this->adminConfig['users'] ?? []);
+ }
+
+ public function addUser(string $username, string $password, string $role = 'admin'): array
+ {
+ if ($this->findUser($username)) {
+ return ['success' => false, 'message' => 'Gebruiker bestaat al.'];
+ }
+ if (strlen($password) < 8) {
+ return ['success' => false, 'message' => 'Wachtwoord moet minimaal 8 tekens zijn.'];
+ }
+
+ $this->adminConfig['users'][] = [
+ 'username' => $username,
+ 'password_hash' => password_hash($password, PASSWORD_DEFAULT),
+ 'role' => $role,
+ 'created' => date('Y-m-d')
+ ];
+ $this->saveAdminConfig();
+ $this->log('info', "Gebruiker aangemaakt: {$username}");
+ return ['success' => true, 'message' => 'Gebruiker aangemaakt.'];
+ }
+
+ public function deleteUser(string $username): array
+ {
+ if ($username === ($_SESSION['admin_user'] ?? '')) {
+ return ['success' => false, 'message' => 'Je kunt jezelf niet verwijderen.'];
+ }
+
+ $this->adminConfig['users'] = array_values(array_filter(
+ $this->adminConfig['users'],
+ fn($u) => $u['username'] !== $username
+ ));
+ $this->saveAdminConfig();
+ $this->log('info', "Gebruiker verwijderd: {$username}");
+ return ['success' => true, 'message' => 'Gebruiker verwijderd.'];
+ }
+
+ public function changePassword(string $username, string $newPassword): array
+ {
+ if (strlen($newPassword) < 8) {
+ return ['success' => false, 'message' => 'Wachtwoord moet minimaal 8 tekens zijn.'];
+ }
+ foreach ($this->adminConfig['users'] as &$user) {
+ if ($user['username'] === $username) {
+ $user['password_hash'] = password_hash($newPassword, PASSWORD_DEFAULT);
+ $this->saveAdminConfig();
+ $this->log('info', "Wachtwoord gewijzigd: {$username}");
+ return ['success' => true, 'message' => 'Wachtwoord gewijzigd.'];
+ }
+ }
+ return ['success' => false, 'message' => 'Gebruiker niet gevonden.'];
+ }
+
+ // --- Private helpers ---
+
+ private function findUser(string $username): ?array
+ {
+ foreach ($this->adminConfig['users'] ?? [] as $user) {
+ if ($user['username'] === $username) {
+ return $user;
+ }
+ }
+ return null;
+ }
+
+ private function checkLockout(string $username): array
+ {
+ $attempts = $this->getFailedAttempts();
+ $maxAttempts = $this->adminConfig['security']['max_login_attempts'] ?? 5;
+ $lockoutDuration = $this->adminConfig['security']['lockout_duration'] ?? 900;
+
+ if (!isset($attempts[$username])) {
+ return ['locked' => false];
+ }
+
+ $record = $attempts[$username];
+ if ($record['count'] >= $maxAttempts) {
+ $elapsed = time() - $record['last_attempt'];
+ if ($elapsed < $lockoutDuration) {
+ return ['locked' => true, 'remaining' => $lockoutDuration - $elapsed];
+ }
+ // Lockout expired
+ $this->clearFailedAttempts($username);
+ }
+
+ return ['locked' => false];
+ }
+
+ private function recordFailedAttempt(string $username): void
+ {
+ $attempts = $this->getFailedAttempts();
+ if (!isset($attempts[$username])) {
+ $attempts[$username] = ['count' => 0, 'last_attempt' => 0];
+ }
+ $attempts[$username]['count']++;
+ $attempts[$username]['last_attempt'] = time();
+ file_put_contents($this->lockFile, json_encode($attempts));
+ }
+
+ private function clearFailedAttempts(string $username): void
+ {
+ $attempts = $this->getFailedAttempts();
+ unset($attempts[$username]);
+ file_put_contents($this->lockFile, json_encode($attempts));
+ }
+
+ private function getFailedAttempts(): array
+ {
+ if (!file_exists($this->lockFile)) {
+ return [];
+ }
+ $data = json_decode(file_get_contents($this->lockFile), true);
+ return is_array($data) ? $data : [];
+ }
+
+ private function log(string $level, string $message): void
+ {
+ $logFile = $this->config['log_file'];
+ $dir = dirname($logFile);
+ if (!is_dir($dir)) {
+ mkdir($dir, 0755, true);
+ }
+ $timestamp = date('Y-m-d H:i:s');
+ $ip = $_SERVER['REMOTE_ADDR'] ?? 'cli';
+ file_put_contents($logFile, "[{$timestamp}] [{$level}] [{$ip}] {$message}\n", FILE_APPEND);
+ }
+}
diff --git a/admin-console/src/Controllers/AuthController.php b/admin-console/src/Controllers/AuthController.php
deleted file mode 100644
index dd2bd91..0000000
--- a/admin-console/src/Controllers/AuthController.php
+++ /dev/null
@@ -1,80 +0,0 @@
-authService = new AuthService();
- $this->logger = new LoggerService();
- }
-
- public function login() {
- if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- $username = $_POST['username'] ?? '';
- $password = $_POST['password'] ?? '';
- $remember = isset($_POST['remember']);
-
- $result = $this->authService->login($username, $password, $remember);
-
- if ($result['success']) {
- $this->logger->info("User logged in: {$username}");
- $this->jsonResponse(['success' => true, 'redirect' => '/admin/dashboard']);
- } else {
- $this->logger->warning("Failed login attempt: {$username}");
- $this->jsonResponse(['success' => false, 'message' => $result['message']]);
- }
- }
-
- $this->renderView('auth/login');
- }
-
- public function logout() {
- $this->authService->logout();
- $this->logger->info("User logged out");
- header('Location: /admin/login');
- exit;
- }
-
- public function profile() {
- if (!$this->authService->isAuthenticated()) {
- header('Location: /admin/login');
- exit;
- }
-
- $user = $this->authService->getCurrentUser();
-
- if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- $email = $_POST['email'] ?? '';
- $currentPassword = $_POST['current_password'] ?? '';
- $newPassword = $_POST['new_password'] ?? '';
-
- $result = $this->authService->updateProfile($user['id'], $email, $currentPassword, $newPassword);
-
- if ($result['success']) {
- $this->logger->info("Profile updated: {$user['username']}");
- $this->jsonResponse(['success' => true, 'message' => 'Profile updated successfully']);
- } else {
- $this->jsonResponse(['success' => false, 'message' => $result['message']]);
- }
- }
-
- $this->renderView('auth/profile', ['user' => $user]);
- }
-
- private function jsonResponse(array $data) {
- header('Content-Type: application/json');
- echo json_encode($data);
- exit;
- }
-
- private function renderView(string $view, array $data = []) {
- extract($data);
- require __DIR__ . "/../../public/templates/{$view}.php";
- }
-}
\ No newline at end of file
diff --git a/admin-console/storage/logs/login_attempts.json b/admin-console/storage/logs/login_attempts.json
new file mode 100644
index 0000000..551b521
--- /dev/null
+++ b/admin-console/storage/logs/login_attempts.json
@@ -0,0 +1 @@
+{"admi":{"count":1,"last_attempt":1771257322}}
\ No newline at end of file
diff --git a/admin-console/templates/layout.php b/admin-console/templates/layout.php
new file mode 100644
index 0000000..5870995
--- /dev/null
+++ b/admin-console/templates/layout.php
@@ -0,0 +1,116 @@
+
+
+
+
+
+ CodePress Admin
+
+
+
+
+
+
+
+
+
+
+
+
+ = htmlspecialchars($message) ?>
+
+
+
+
+
+
+
+
+
+
diff --git a/admin-console/templates/login.php b/admin-console/templates/login.php
new file mode 100644
index 0000000..1a4631e
--- /dev/null
+++ b/admin-console/templates/login.php
@@ -0,0 +1,59 @@
+
+
+
+
+
+ CodePress Admin - Login
+
+
+
+
+
+
+
+
+
diff --git a/admin-console/templates/pages/config.php b/admin-console/templates/pages/config.php
new file mode 100644
index 0000000..911e8cf
--- /dev/null
+++ b/admin-console/templates/pages/config.php
@@ -0,0 +1,19 @@
+ Configuratie
+
+
diff --git a/admin-console/templates/pages/content-edit.php b/admin-console/templates/pages/content-edit.php
new file mode 100644
index 0000000..96e92b5
--- /dev/null
+++ b/admin-console/templates/pages/content-edit.php
@@ -0,0 +1,27 @@
+
+
= htmlspecialchars($fileName) ?>
+
+ Terug
+
+
+
+
+
+
+
+
diff --git a/admin-console/templates/pages/content-new.php b/admin-console/templates/pages/content-new.php
new file mode 100644
index 0000000..c6ec3b2
--- /dev/null
+++ b/admin-console/templates/pages/content-new.php
@@ -0,0 +1,46 @@
+
+
Nieuwe pagina
+
+ Terug
+
+
+
+
+
+
+
+
diff --git a/admin-console/templates/pages/content.php b/admin-console/templates/pages/content.php
new file mode 100644
index 0000000..c0761bc
--- /dev/null
+++ b/admin-console/templates/pages/content.php
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
diff --git a/admin-console/templates/pages/dashboard.php b/admin-console/templates/pages/dashboard.php
new file mode 100644
index 0000000..2d0e0c3
--- /dev/null
+++ b/admin-console/templates/pages/dashboard.php
@@ -0,0 +1,78 @@
+ Dashboard
+
+
+
+
+
+
+
Pagina's
+ = $stats['pages'] ?>
+
+
+
+
+
+
+
+
+
+
Mappen
+ = $stats['directories'] ?>
+
+
+
+
+
+
+
+
+
+
Plugins
+ = $stats['plugins'] ?>
+
+
+
+
+
+
+
+
+
+
Content grootte
+ = $stats['content_size'] ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Site titel | = htmlspecialchars($siteConfig['site_title'] ?? 'CodePress') ?> |
+ | Standaard taal | = htmlspecialchars($siteConfig['language']['default'] ?? 'nl') ?> |
+ | Auteur | = htmlspecialchars($siteConfig['author']['name'] ?? '-') ?> |
+ | PHP versie | = $stats['php_version'] ?> |
+ | Config geladen | = $stats['config_exists'] ? 'Ja' : 'Nee' ?> |
+
+
+
+
+
+
diff --git a/admin-console/templates/pages/plugins.php b/admin-console/templates/pages/plugins.php
new file mode 100644
index 0000000..af09503
--- /dev/null
+++ b/admin-console/templates/pages/plugins.php
@@ -0,0 +1,44 @@
+ Plugins
+
+
+ Geen plugins gevonden in de plugins map.
+
+
+
+
+
+
+
+
+
+ | Hoofdbestand |
+
+ = $plugin['has_main'] ? ' Aanwezig' : ' Ontbreekt' ?>
+ |
+
+
+ | Configuratie |
+
+ = $plugin['has_config'] ? ' Aanwezig' : ' Geen' ?>
+ |
+
+
+ | README |
+
+ = $plugin['has_readme'] ? ' Aanwezig' : ' Geen' ?>
+ |
+
+
+
+
+
+
+
+
diff --git a/admin-console/templates/pages/users.php b/admin-console/templates/pages/users.php
new file mode 100644
index 0000000..d05b8dc
--- /dev/null
+++ b/admin-console/templates/pages/users.php
@@ -0,0 +1,93 @@
+ Gebruikers
+
+
diff --git a/guide/en.codepress.md b/guide/en.codepress.md
index bf3e8ee..1977c24 100644
--- a/guide/en.codepress.md
+++ b/guide/en.codepress.md
@@ -50,6 +50,17 @@ CodePress is a lightweight, file-based Content Management System built with PHP
- **Dynamic layouts** with YAML frontmatter
- **Sidebar support** with plugin integration and toggle function via breadcrumb
+### 🛡️ Admin Console
+- Built-in admin panel at `/admin.php`
+- **Dashboard** with statistics and quick actions
+- **Content management** - Browse, create, edit and delete files
+- **Configuration editor** - Edit `config.json` with JSON validation
+- **Plugin overview** - Status of all installed plugins
+- **User management** - Add, remove users and change passwords
+- Session-based authentication with bcrypt hashing
+- CSRF protection, brute-force lockout (5 attempts, 15 min)
+- Default login: `admin` / `admin` (change immediately after installation)
+
## Installation
1. Clone or download CodePress files
diff --git a/guide/nl.codepress.md b/guide/nl.codepress.md
index a78d4a5..7f47544 100644
--- a/guide/nl.codepress.md
+++ b/guide/nl.codepress.md
@@ -50,6 +50,17 @@ CodePress CMS is een lichtgewicht, file-based content management systeem gebouwd
- **Dynamic layouts** met YAML frontmatter
- **Sidebar support** met plugin integratie en toggle functie via breadcrumb
+### 🛡️ Admin Console
+- Ingebouwd admin paneel op `/admin.php`
+- **Dashboard** met statistieken en snelle acties
+- **Content beheer** - Bestanden browsen, aanmaken, bewerken en verwijderen
+- **Configuratie editor** - `config.json` bewerken met JSON-validatie
+- **Plugin overzicht** - Status van alle geinstalleerde plugins
+- **Gebruikersbeheer** - Gebruikers toevoegen, verwijderen, wachtwoorden wijzigen
+- Session-based authenticatie met bcrypt hashing
+- CSRF-bescherming, brute-force lockout (5 pogingen, 15 min)
+- Standaard login: `admin` / `admin` (wijzig direct na installatie)
+
## Installatie
1. Upload bestanden naar webserver
diff --git a/public/admin.php b/public/admin.php
new file mode 100644
index 0000000..e259fba
--- /dev/null
+++ b/public/admin.php
@@ -0,0 +1,449 @@
+isAuthenticated()) {
+ header('Location: admin.php?route=login');
+ exit;
+}
+
+// Authenticated routes
+switch ($route) {
+ case 'logout':
+ $auth->logout();
+ header('Location: admin.php?route=login');
+ exit;
+
+ case 'dashboard':
+ case '':
+ handleDashboard($auth, $appConfig);
+ break;
+
+ case 'content':
+ handleContent($auth, $appConfig);
+ break;
+
+ case 'content-edit':
+ handleContentEdit($auth, $appConfig);
+ break;
+
+ case 'content-new':
+ handleContentNew($auth, $appConfig);
+ break;
+
+ case 'content-delete':
+ handleContentDelete($auth, $appConfig);
+ break;
+
+ case 'config':
+ handleConfig($auth, $appConfig);
+ break;
+
+ case 'plugins':
+ handlePlugins($auth, $appConfig);
+ break;
+
+ case 'users':
+ handleUsers($auth, $appConfig);
+ break;
+
+ default:
+ header('Location: admin.php?route=dashboard');
+ exit;
+}
+
+// --- Route Handlers ---
+
+function handleLogin(AdminAuth $auth): void
+{
+ $error = '';
+
+ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $username = trim($_POST['username'] ?? '');
+ $password = $_POST['password'] ?? '';
+
+ $result = $auth->login($username, $password);
+ if ($result['success']) {
+ header('Location: admin.php?route=dashboard');
+ exit;
+ }
+ $error = $result['message'];
+ }
+
+ require __DIR__ . '/../admin-console/templates/login.php';
+}
+
+function handleDashboard(AdminAuth $auth, array $config): void
+{
+ $user = $auth->getCurrentUser();
+ $csrf = $auth->getCsrfToken();
+
+ // Gather stats
+ $contentDir = $config['content_dir'];
+ $pluginsDir = $config['plugins_dir'];
+ $configJson = $config['config_json'];
+
+ $stats = [
+ 'pages' => countFiles($contentDir, ['md', 'php', 'html']),
+ 'directories' => countDirs($contentDir),
+ 'plugins' => countDirs($pluginsDir),
+ 'config_exists' => file_exists($configJson),
+ 'content_size' => formatSize(dirSize($contentDir)),
+ 'php_version' => PHP_VERSION,
+ ];
+
+ // Load site config
+ $siteConfig = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
+
+ require __DIR__ . '/../admin-console/templates/layout.php';
+}
+
+function handleContent(AdminAuth $auth, array $config): void
+{
+ $user = $auth->getCurrentUser();
+ $csrf = $auth->getCsrfToken();
+ $contentDir = $config['content_dir'];
+ $subdir = $_GET['dir'] ?? '';
+
+ // Prevent path traversal
+ $subdir = str_replace(['../', '..\\'], '', $subdir);
+ $fullPath = rtrim($contentDir, '/') . '/' . $subdir;
+
+ if (!is_dir($fullPath)) {
+ $fullPath = $contentDir;
+ $subdir = '';
+ }
+
+ $items = scanContentDir($fullPath, $subdir);
+ $route = 'content';
+
+ require __DIR__ . '/../admin-console/templates/layout.php';
+}
+
+function handleContentEdit(AdminAuth $auth, array $config): void
+{
+ $user = $auth->getCurrentUser();
+ $csrf = $auth->getCsrfToken();
+ $contentDir = $config['content_dir'];
+ $file = $_GET['file'] ?? '';
+ $file = str_replace(['../', '..\\'], '', $file);
+ $filePath = rtrim($contentDir, '/') . '/' . $file;
+ $message = '';
+ $messageType = '';
+
+ // Validate path
+ $realPath = realpath($filePath);
+ $realContentDir = realpath($contentDir);
+ if (!$realPath || !$realContentDir || strpos($realPath, $realContentDir) !== 0) {
+ header('Location: admin.php?route=content');
+ exit;
+ }
+
+ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
+ $message = 'Ongeldige CSRF token.';
+ $messageType = 'danger';
+ } else {
+ $content = $_POST['content'] ?? '';
+ file_put_contents($filePath, $content);
+ $message = 'Bestand opgeslagen.';
+ $messageType = 'success';
+ }
+ }
+
+ $fileContent = file_get_contents($filePath);
+ $fileName = basename($filePath);
+ $fileExt = pathinfo($filePath, PATHINFO_EXTENSION);
+ $route = 'content-edit';
+
+ require __DIR__ . '/../admin-console/templates/layout.php';
+}
+
+function handleContentNew(AdminAuth $auth, array $config): void
+{
+ $user = $auth->getCurrentUser();
+ $csrf = $auth->getCsrfToken();
+ $contentDir = $config['content_dir'];
+ $dir = $_GET['dir'] ?? '';
+ $dir = str_replace(['../', '..\\'], '', $dir);
+ $message = '';
+ $messageType = '';
+
+ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
+ $message = 'Ongeldige CSRF token.';
+ $messageType = 'danger';
+ } else {
+ $filename = trim($_POST['filename'] ?? '');
+ $content = $_POST['content'] ?? '';
+ $type = $_POST['type'] ?? 'md';
+
+ if (empty($filename)) {
+ $message = 'Bestandsnaam is verplicht.';
+ $messageType = 'danger';
+ } else {
+ // Sanitize filename
+ $filename = preg_replace('/[^a-zA-Z0-9._-]/', '-', $filename);
+ if (!preg_match('/\.(md|php|html)$/', $filename)) {
+ $filename .= '.' . $type;
+ }
+ $targetDir = rtrim($contentDir, '/') . '/' . $dir;
+ $filePath = $targetDir . '/' . $filename;
+
+ if (file_exists($filePath)) {
+ $message = 'Bestand bestaat al.';
+ $messageType = 'danger';
+ } else {
+ if (!is_dir($targetDir)) {
+ mkdir($targetDir, 0755, true);
+ }
+ file_put_contents($filePath, $content);
+ header('Location: admin.php?route=content&dir=' . urlencode($dir));
+ exit;
+ }
+ }
+ }
+ }
+
+ $route = 'content-new';
+ require __DIR__ . '/../admin-console/templates/layout.php';
+}
+
+function handleContentDelete(AdminAuth $auth, array $config): void
+{
+ $contentDir = $config['content_dir'];
+ $file = $_GET['file'] ?? '';
+ $file = str_replace(['../', '..\\'], '', $file);
+ $filePath = rtrim($contentDir, '/') . '/' . $file;
+
+ $realPath = realpath($filePath);
+ $realContentDir = realpath($contentDir);
+
+ if ($_SERVER['REQUEST_METHOD'] === 'POST'
+ && $auth->verifyCsrf($_POST['csrf_token'] ?? '')
+ && $realPath && $realContentDir
+ && strpos($realPath, $realContentDir) === 0
+ ) {
+ if (is_file($filePath)) {
+ unlink($filePath);
+ }
+ }
+
+ $dir = dirname($file);
+ header('Location: admin.php?route=content&dir=' . urlencode($dir === '.' ? '' : $dir));
+ exit;
+}
+
+function handleConfig(AdminAuth $auth, array $config): void
+{
+ $user = $auth->getCurrentUser();
+ $csrf = $auth->getCsrfToken();
+ $configJson = $config['config_json'];
+ $message = '';
+ $messageType = '';
+
+ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
+ $message = 'Ongeldige CSRF token.';
+ $messageType = 'danger';
+ } else {
+ $jsonContent = $_POST['config_content'] ?? '';
+ $parsed = json_decode($jsonContent, true);
+ if ($parsed === null && json_last_error() !== JSON_ERROR_NONE) {
+ $message = 'Ongeldige JSON: ' . json_last_error_msg();
+ $messageType = 'danger';
+ } else {
+ file_put_contents($configJson, json_encode($parsed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
+ $message = 'Configuratie opgeslagen.';
+ $messageType = 'success';
+ $jsonContent = null; // reload from file
+ }
+ }
+ }
+
+ $siteConfig = file_exists($configJson) ? file_get_contents($configJson) : '{}';
+ if (isset($jsonContent)) {
+ $siteConfig = $jsonContent;
+ }
+
+ $route = 'config';
+ require __DIR__ . '/../admin-console/templates/layout.php';
+}
+
+function handlePlugins(AdminAuth $auth, array $config): void
+{
+ $user = $auth->getCurrentUser();
+ $csrf = $auth->getCsrfToken();
+ $pluginsDir = $config['plugins_dir'];
+ $plugins = [];
+
+ if (is_dir($pluginsDir)) {
+ foreach (scandir($pluginsDir) as $item) {
+ if ($item[0] === '.') continue;
+ $pluginPath = $pluginsDir . '/' . $item;
+ if (!is_dir($pluginPath)) continue;
+
+ $hasConfig = file_exists($pluginPath . '/config.json');
+ $pluginConfig = $hasConfig ? json_decode(file_get_contents($pluginPath . '/config.json'), true) : [];
+ $hasMainFile = file_exists($pluginPath . '/' . $item . '.php');
+ $hasReadme = file_exists($pluginPath . '/README.md');
+
+ $plugins[] = [
+ 'name' => $item,
+ 'path' => $pluginPath,
+ 'enabled' => $pluginConfig['enabled'] ?? true,
+ 'config' => $pluginConfig,
+ 'has_config' => $hasConfig,
+ 'has_main' => $hasMainFile,
+ 'has_readme' => $hasReadme,
+ ];
+ }
+ }
+
+ $route = 'plugins';
+ require __DIR__ . '/../admin-console/templates/layout.php';
+}
+
+function handleUsers(AdminAuth $auth, array $config): void
+{
+ $user = $auth->getCurrentUser();
+ $csrf = $auth->getCsrfToken();
+ $message = '';
+ $messageType = '';
+
+ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
+ $message = 'Ongeldige CSRF token.';
+ $messageType = 'danger';
+ } else {
+ $action = $_POST['action'] ?? '';
+
+ if ($action === 'add') {
+ $result = $auth->addUser(
+ trim($_POST['username'] ?? ''),
+ $_POST['password'] ?? '',
+ $_POST['role'] ?? 'admin'
+ );
+ $message = $result['message'];
+ $messageType = $result['success'] ? 'success' : 'danger';
+ } elseif ($action === 'delete') {
+ $result = $auth->deleteUser($_POST['delete_username'] ?? '');
+ $message = $result['message'];
+ $messageType = $result['success'] ? 'success' : 'danger';
+ } elseif ($action === 'change_password') {
+ $result = $auth->changePassword(
+ $_POST['pw_username'] ?? '',
+ $_POST['new_password'] ?? ''
+ );
+ $message = $result['message'];
+ $messageType = $result['success'] ? 'success' : 'danger';
+ }
+ }
+ }
+
+ $users = $auth->getUsers();
+ $route = 'users';
+ require __DIR__ . '/../admin-console/templates/layout.php';
+}
+
+// --- Helper functions ---
+
+function countFiles(string $dir, array $extensions): int
+{
+ $count = 0;
+ if (!is_dir($dir)) return 0;
+ $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS));
+ foreach ($iterator as $file) {
+ if ($file->isFile() && in_array($file->getExtension(), $extensions)) {
+ $count++;
+ }
+ }
+ return $count;
+}
+
+function countDirs(string $dir): int
+{
+ if (!is_dir($dir)) return 0;
+ $count = 0;
+ foreach (scandir($dir) as $item) {
+ if ($item[0] !== '.' && is_dir($dir . '/' . $item)) $count++;
+ }
+ return $count;
+}
+
+function dirSize(string $dir): int
+{
+ $size = 0;
+ if (!is_dir($dir)) return 0;
+ $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS));
+ foreach ($iterator as $file) {
+ if ($file->isFile()) $size += $file->getSize();
+ }
+ return $size;
+}
+
+function formatSize(int $bytes): string
+{
+ $units = ['B', 'KB', 'MB', 'GB'];
+ $i = 0;
+ while ($bytes >= 1024 && $i < count($units) - 1) {
+ $bytes /= 1024;
+ $i++;
+ }
+ return round($bytes, 1) . ' ' . $units[$i];
+}
+
+function scanContentDir(string $fullPath, string $subdir): array
+{
+ $items = [];
+ if (!is_dir($fullPath)) return $items;
+
+ foreach (scandir($fullPath) as $item) {
+ if ($item[0] === '.') continue;
+ $itemPath = $fullPath . '/' . $item;
+ $relativePath = $subdir ? $subdir . '/' . $item : $item;
+
+ $items[] = [
+ 'name' => $item,
+ 'path' => $relativePath,
+ 'is_dir' => is_dir($itemPath),
+ 'size' => is_file($itemPath) ? formatSize(filesize($itemPath)) : '',
+ 'modified' => date('d-m-Y H:i', filemtime($itemPath)),
+ 'extension' => is_file($itemPath) ? pathinfo($item, PATHINFO_EXTENSION) : '',
+ ];
+ }
+
+ // Sort: directories first, then files alphabetically
+ usort($items, function ($a, $b) {
+ if ($a['is_dir'] !== $b['is_dir']) return $b['is_dir'] - $a['is_dir'];
+ return strcasecmp($a['name'], $b['name']);
+ });
+
+ return $items;
+}