Add admin console with login, dashboard, content/config/plugin/user management
File-based admin panel accessible at /admin.php with: - Session-based auth with bcrypt hashing and brute-force protection - Dashboard with site statistics and quick actions - Content manager: browse, create, edit, delete files - Config editor with JSON validation - Plugin overview with status indicators - User management: add, remove, change passwords - CSRF protection on all forms, path traversal prevention - Updated README (NL/EN) and guides with admin documentation
This commit is contained in:
33
README.en.md
33
README.en.md
@@ -17,6 +17,7 @@ A lightweight, file-based content management system built with PHP.
|
|||||||
- ⚙️ **JSON Configuration** - Easy configuration via JSON
|
- ⚙️ **JSON Configuration** - Easy configuration via JSON
|
||||||
- 🎨 **Bootstrap 5** - Modern UI framework
|
- 🎨 **Bootstrap 5** - Modern UI framework
|
||||||
- 🔒 **Security** - Secure content management (100/100 security score)
|
- 🔒 **Security** - Secure content management (100/100 security score)
|
||||||
|
- 🛡️ **Admin Console** - Built-in admin panel for content, config, plugins and user management
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
@@ -47,6 +48,13 @@ codepress/
|
|||||||
│ ├── markdown_content.mustache
|
│ ├── markdown_content.mustache
|
||||||
│ ├── php_content.mustache
|
│ ├── php_content.mustache
|
||||||
│ └── html_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
|
├── public/ # Web root
|
||||||
│ ├── assets/
|
│ ├── assets/
|
||||||
│ │ ├── css/
|
│ │ ├── css/
|
||||||
@@ -56,7 +64,8 @@ codepress/
|
|||||||
│ │ ├── nl.homepage.md # Dutch homepage
|
│ │ ├── nl.homepage.md # Dutch homepage
|
||||||
│ │ ├── en.homepage.md # English homepage
|
│ │ ├── en.homepage.md # English homepage
|
||||||
│ │ └── [lang].[page].md # Multi-language pages
|
│ │ └── [lang].[page].md # Multi-language pages
|
||||||
│ └── index.php # Entry point
|
│ ├── index.php # Website entry point
|
||||||
|
│ └── admin.php # Admin entry point
|
||||||
├── config.json # Configuration
|
├── config.json # Configuration
|
||||||
├── version.php # Version tracking
|
├── version.php # Version tracking
|
||||||
└── README.md # This file
|
└── 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.
|
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
|
## 📖 Documentation
|
||||||
|
|
||||||
- **[Guide (NL)](guide/nl.codepress.md)** - Dutch documentation
|
- **[Guide (NL)](guide/nl.codepress.md)** - Dutch documentation
|
||||||
|
|||||||
33
README.md
33
README.md
@@ -17,6 +17,7 @@ Een lichtgewicht, file-based content management systeem gebouwd met PHP.
|
|||||||
- ⚙️ **JSON Configuratie** - Eenvoudige configuratie via JSON
|
- ⚙️ **JSON Configuratie** - Eenvoudige configuratie via JSON
|
||||||
- 🎨 **Bootstrap 5** - Moderne UI framework
|
- 🎨 **Bootstrap 5** - Moderne UI framework
|
||||||
- 🔒 **Security** - Beveiligde content management
|
- 🔒 **Security** - Beveiligde content management
|
||||||
|
- 🛡️ **Admin Console** - Ingebouwd admin paneel voor content, config, plugins en gebruikersbeheer
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
@@ -47,12 +48,20 @@ codepress/
|
|||||||
│ │ ├── pagina1.md
|
│ │ ├── pagina1.md
|
||||||
│ │ └── pagina2.php
|
│ │ └── pagina2.php
|
||||||
│ └── homepage.md
|
│ └── 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
|
├── public/ # Web root
|
||||||
│ ├── assets/
|
│ ├── assets/
|
||||||
│ │ ├── css/
|
│ │ ├── css/
|
||||||
│ │ ├── js/
|
│ │ ├── js/
|
||||||
│ │ └── favicon.svg
|
│ │ └── favicon.svg
|
||||||
│ └── index.php
|
│ ├── index.php # Website entry point
|
||||||
|
│ └── admin.php # Admin entry point
|
||||||
├── config.json # Configuratie
|
├── config.json # Configuratie
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
@@ -230,6 +239,28 @@ Hoofd CMS class die alle content management functionaliteit beheert.
|
|||||||
- File metadata tracking
|
- File metadata tracking
|
||||||
- Responsive template rendering
|
- 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
|
## 📖 Documentatie
|
||||||
|
|
||||||
- **[Handleiding (NL)](guide/nl.md)** - Gedetailleerde handleiding
|
- **[Handleiding (NL)](guide/nl.md)** - Gedetailleerde handleiding
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
15
admin-console/config/admin.json
Normal file
15
admin-console/config/admin.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,57 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => 'CodePress Admin Console',
|
'name' => 'CodePress Admin',
|
||||||
'version' => '1.0.0',
|
'version' => '1.0.0',
|
||||||
'debug' => $_ENV['APP_DEBUG'] ?? false,
|
'debug' => $_ENV['APP_DEBUG'] ?? false,
|
||||||
'timezone' => 'Europe/Amsterdam',
|
'timezone' => 'Europe/Amsterdam',
|
||||||
|
|
||||||
// Security
|
// Paths
|
||||||
'security' => [
|
'admin_root' => __DIR__ . '/../',
|
||||||
'jwt_secret' => $_ENV['JWT_SECRET'] ?? throw new \RuntimeException('JWT_SECRET environment variable must be set'),
|
'codepress_root' => __DIR__ . '/../../',
|
||||||
'jwt_expiration' => 3600, // 1 hour
|
'content_dir' => __DIR__ . '/../../content/',
|
||||||
'session_timeout' => 1800, // 30 minutes
|
'config_json' => __DIR__ . '/../../config.json',
|
||||||
'max_login_attempts' => 5,
|
'plugins_dir' => __DIR__ . '/../../plugins/',
|
||||||
'lockout_duration' => 900, // 15 minutes
|
'admin_config' => __DIR__ . '/admin.json',
|
||||||
],
|
'log_file' => __DIR__ . '/../storage/logs/admin.log',
|
||||||
|
];
|
||||||
// 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',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|||||||
279
admin-console/src/AdminAuth.php
Normal file
279
admin-console/src/AdminAuth.php
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AdminAuth - File-based authentication for CodePress Admin
|
||||||
|
*/
|
||||||
|
class AdminAuth
|
||||||
|
{
|
||||||
|
private array $config;
|
||||||
|
private array $adminConfig;
|
||||||
|
private string $lockFile;
|
||||||
|
|
||||||
|
public function __construct(array $appConfig)
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace CodePress\Admin\Controllers;
|
|
||||||
|
|
||||||
use CodePress\Admin\Services\AuthService;
|
|
||||||
use CodePress\Admin\Services\LoggerService;
|
|
||||||
|
|
||||||
class AuthController {
|
|
||||||
private AuthService $authService;
|
|
||||||
private LoggerService $logger;
|
|
||||||
|
|
||||||
public function __construct() {
|
|
||||||
$this->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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
admin-console/storage/logs/login_attempts.json
Normal file
1
admin-console/storage/logs/login_attempts.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"admi":{"count":1,"last_attempt":1771257322}}
|
||||||
116
admin-console/templates/layout.php
Normal file
116
admin-console/templates/layout.php
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CodePress Admin</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="assets/css/bootstrap-icons.css">
|
||||||
|
<style>
|
||||||
|
body { background-color: #f5f6fa; min-height: 100vh; }
|
||||||
|
.admin-sidebar { background-color: #0a369d; min-height: 100vh; width: 240px; position: fixed; top: 0; left: 0; z-index: 100; }
|
||||||
|
.admin-sidebar .nav-link { color: rgba(255,255,255,0.8); padding: 0.75rem 1.25rem; border-radius: 0; }
|
||||||
|
.admin-sidebar .nav-link:hover { color: #fff; background-color: rgba(255,255,255,0.1); }
|
||||||
|
.admin-sidebar .nav-link.active { color: #fff; background-color: rgba(255,255,255,0.2); border-left: 3px solid #fff; }
|
||||||
|
.admin-sidebar .nav-link i { width: 24px; text-align: center; margin-right: 0.5rem; }
|
||||||
|
.admin-main { margin-left: 240px; padding: 2rem; }
|
||||||
|
.admin-brand { color: #fff; padding: 1.25rem; font-size: 1.1rem; border-bottom: 1px solid rgba(255,255,255,0.15); }
|
||||||
|
.admin-brand i { margin-right: 0.5rem; }
|
||||||
|
.stat-card { border: none; border-radius: 0.5rem; }
|
||||||
|
.stat-card .stat-icon { font-size: 2rem; opacity: 0.7; }
|
||||||
|
.admin-user { color: rgba(255,255,255,0.6); padding: 0.75rem 1.25rem; font-size: 0.85rem; border-top: 1px solid rgba(255,255,255,0.15); position: absolute; bottom: 0; width: 100%; }
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-sidebar { width: 100%; min-height: auto; position: relative; }
|
||||||
|
.admin-main { margin-left: 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="admin-sidebar d-flex flex-column">
|
||||||
|
<div class="admin-brand">
|
||||||
|
<i class="bi bi-gear-fill"></i> CodePress Admin
|
||||||
|
</div>
|
||||||
|
<ul class="nav flex-column mt-2">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <?= ($route ?? '') === 'dashboard' || ($route ?? '') === '' ? 'active' : '' ?>" href="admin.php?route=dashboard">
|
||||||
|
<i class="bi bi-speedometer2"></i> Dashboard
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <?= ($route ?? '') === 'content' || str_starts_with($route ?? '', 'content') ? 'active' : '' ?>" href="admin.php?route=content">
|
||||||
|
<i class="bi bi-file-earmark-text"></i> Content
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <?= ($route ?? '') === 'config' ? 'active' : '' ?>" href="admin.php?route=config">
|
||||||
|
<i class="bi bi-sliders"></i> Configuratie
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <?= ($route ?? '') === 'plugins' ? 'active' : '' ?>" href="admin.php?route=plugins">
|
||||||
|
<i class="bi bi-plug"></i> Plugins
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <?= ($route ?? '') === 'users' ? 'active' : '' ?>" href="admin.php?route=users">
|
||||||
|
<i class="bi bi-people"></i> Gebruikers
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item mt-3">
|
||||||
|
<a class="nav-link" href="index.php" target="_blank">
|
||||||
|
<i class="bi bi-box-arrow-up-right"></i> Website bekijken
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-warning" href="admin.php?route=logout">
|
||||||
|
<i class="bi bi-box-arrow-left"></i> Uitloggen
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="admin-user">
|
||||||
|
<i class="bi bi-person-circle"></i> <?= htmlspecialchars($user['username'] ?? '') ?>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<main class="admin-main">
|
||||||
|
<?php if (!empty($message)): ?>
|
||||||
|
<div class="alert alert-<?= $messageType ?? 'info' ?> alert-dismissible fade show" role="alert">
|
||||||
|
<?= htmlspecialchars($message) ?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$currentRoute = $route ?? 'dashboard';
|
||||||
|
switch ($currentRoute) {
|
||||||
|
case 'dashboard':
|
||||||
|
case '':
|
||||||
|
require __DIR__ . '/pages/dashboard.php';
|
||||||
|
break;
|
||||||
|
case 'content':
|
||||||
|
require __DIR__ . '/pages/content.php';
|
||||||
|
break;
|
||||||
|
case 'content-edit':
|
||||||
|
require __DIR__ . '/pages/content-edit.php';
|
||||||
|
break;
|
||||||
|
case 'content-new':
|
||||||
|
require __DIR__ . '/pages/content-new.php';
|
||||||
|
break;
|
||||||
|
case 'config':
|
||||||
|
require __DIR__ . '/pages/config.php';
|
||||||
|
break;
|
||||||
|
case 'plugins':
|
||||||
|
require __DIR__ . '/pages/plugins.php';
|
||||||
|
break;
|
||||||
|
case 'users':
|
||||||
|
require __DIR__ . '/pages/users.php';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="assets/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
59
admin-console/templates/login.php
Normal file
59
admin-console/templates/login.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CodePress Admin - Login</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="assets/css/bootstrap-icons.css">
|
||||||
|
<style>
|
||||||
|
body { background-color: #f5f6fa; }
|
||||||
|
.login-card { max-width: 400px; margin: 10vh auto; }
|
||||||
|
.login-header { background-color: #0a369d; color: #fff; padding: 2rem; text-align: center; border-radius: 0.5rem 0.5rem 0 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="login-card">
|
||||||
|
<div class="login-header">
|
||||||
|
<h4><i class="bi bi-shield-lock"></i> CodePress Admin</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card border-0 shadow-sm" style="border-radius: 0 0 0.5rem 0.5rem;">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<?php if (!empty($error)): ?>
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<?= htmlspecialchars($error) ?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="POST" action="admin.php?route=login">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">Gebruikersnaam</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="bi bi-person"></i></span>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required autofocus>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Wachtwoord</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-box-arrow-in-right"></i> Inloggen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-center text-muted mt-3 small">
|
||||||
|
<a href="index.php" class="text-decoration-none"><i class="bi bi-arrow-left"></i> Terug naar website</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="assets/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
19
admin-console/templates/pages/config.php
Normal file
19
admin-console/templates/pages/config.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<h2 class="mb-4"><i class="bi bi-sliders"></i> Configuratie</h2>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="bi bi-filetype-json"></i> config.json
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="admin.php?route=config">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||||
|
<div class="mb-3">
|
||||||
|
<textarea name="config_content" class="form-control font-monospace" rows="25" style="font-size: 0.9rem; tab-size: 4;"><?= htmlspecialchars($siteConfig) ?></textarea>
|
||||||
|
<small class="form-text text-muted">Bewerk de JSON configuratie. Ongeldige JSON wordt niet opgeslagen.</small>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-lg"></i> Opslaan
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
27
admin-console/templates/pages/content-edit.php
Normal file
27
admin-console/templates/pages/content-edit.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2><i class="bi bi-pencil"></i> <?= htmlspecialchars($fileName) ?></h2>
|
||||||
|
<a href="admin.php?route=content&dir=<?= urlencode(dirname($_GET['file'] ?? '')) ?>" class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="bi bi-arrow-left"></i> Terug
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="admin.php?route=content-edit&file=<?= urlencode($_GET['file'] ?? '') ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span class="badge bg-secondary"><?= strtoupper($fileExt) ?></span>
|
||||||
|
<small class="text-muted"><?= htmlspecialchars($_GET['file'] ?? '') ?></small>
|
||||||
|
</div>
|
||||||
|
<textarea name="content" class="form-control font-monospace" rows="25" style="font-size: 0.9rem; tab-size: 4;"><?= htmlspecialchars($fileContent) ?></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-lg"></i> Opslaan
|
||||||
|
</button>
|
||||||
|
<a href="admin.php?route=content&dir=<?= urlencode(dirname($_GET['file'] ?? '')) ?>" class="btn btn-outline-secondary">Annuleren</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
46
admin-console/templates/pages/content-new.php
Normal file
46
admin-console/templates/pages/content-new.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2><i class="bi bi-plus-lg"></i> Nieuwe pagina</h2>
|
||||||
|
<a href="admin.php?route=content&dir=<?= urlencode($dir ?? '') ?>" class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="bi bi-arrow-left"></i> Terug
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="admin.php?route=content-new&dir=<?= urlencode($dir ?? '') ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label for="filename" class="form-label">Bestandsnaam</label>
|
||||||
|
<input type="text" class="form-control" id="filename" name="filename" placeholder="bijv. mijn-pagina" required>
|
||||||
|
<small class="form-text text-muted">Extensie wordt automatisch toegevoegd.</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="type" class="form-label">Type</label>
|
||||||
|
<select class="form-select" id="type" name="type">
|
||||||
|
<option value="md" selected>Markdown (.md)</option>
|
||||||
|
<option value="php">PHP (.php)</option>
|
||||||
|
<option value="html">HTML (.html)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php if (!empty($dir)): ?>
|
||||||
|
<div class="mb-3">
|
||||||
|
<small class="text-muted">Map: <?= htmlspecialchars($dir) ?></small>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="content" class="form-label">Inhoud</label>
|
||||||
|
<textarea name="content" id="content" class="form-control font-monospace" rows="20" style="font-size: 0.9rem; tab-size: 4;" placeholder="# Mijn nieuwe pagina
|
||||||
|
|
||||||
|
Schrijf hier je inhoud..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-lg"></i> Aanmaken
|
||||||
|
</button>
|
||||||
|
<a href="admin.php?route=content&dir=<?= urlencode($dir ?? '') ?>" class="btn btn-outline-secondary">Annuleren</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
98
admin-console/templates/pages/content.php
Normal file
98
admin-console/templates/pages/content.php
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2><i class="bi bi-file-earmark-text"></i> Content</h2>
|
||||||
|
<a href="admin.php?route=content-new&dir=<?= urlencode($subdir) ?>" class="btn btn-primary btn-sm">
|
||||||
|
<i class="bi bi-plus-lg"></i> Nieuw bestand
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($subdir)): ?>
|
||||||
|
<?php
|
||||||
|
$parentDir = dirname($subdir);
|
||||||
|
$parentLink = $parentDir === '.' ? '' : $parentDir;
|
||||||
|
?>
|
||||||
|
<nav aria-label="breadcrumb" class="mb-3">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="admin.php?route=content"><i class="bi bi-house"></i></a></li>
|
||||||
|
<?php
|
||||||
|
$crumbPath = '';
|
||||||
|
foreach (explode('/', $subdir) as $i => $crumb):
|
||||||
|
$crumbPath .= ($crumbPath ? '/' : '') . $crumb;
|
||||||
|
?>
|
||||||
|
<li class="breadcrumb-item <?= $crumbPath === $subdir ? 'active' : '' ?>">
|
||||||
|
<?php if ($crumbPath === $subdir): ?>
|
||||||
|
<?= htmlspecialchars($crumb) ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<a href="admin.php?route=content&dir=<?= urlencode($crumbPath) ?>"><?= htmlspecialchars($crumb) ?></a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Naam</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Grootte</th>
|
||||||
|
<th>Gewijzigd</th>
|
||||||
|
<th style="width: 120px;">Acties</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($items)): ?>
|
||||||
|
<tr><td colspan="5" class="text-muted text-center py-4">Geen bestanden gevonden.</td></tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($items as $item): ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<?php if ($item['is_dir']): ?>
|
||||||
|
<a href="admin.php?route=content&dir=<?= urlencode($item['path']) ?>">
|
||||||
|
<i class="bi bi-folder-fill text-warning"></i> <?= htmlspecialchars($item['name']) ?>
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<a href="admin.php?route=content-edit&file=<?= urlencode($item['path']) ?>">
|
||||||
|
<?php
|
||||||
|
$icon = match($item['extension']) {
|
||||||
|
'md' => 'bi-file-text text-primary',
|
||||||
|
'php' => 'bi-file-code text-success',
|
||||||
|
'html' => 'bi-file-earmark text-info',
|
||||||
|
default => 'bi-file text-muted'
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
<i class="bi <?= $icon ?>"></i> <?= htmlspecialchars($item['name']) ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if ($item['is_dir']): ?>
|
||||||
|
<span class="badge bg-warning text-dark">Map</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge bg-secondary"><?= strtoupper($item['extension']) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-muted"><?= $item['size'] ?></td>
|
||||||
|
<td class="text-muted"><?= $item['modified'] ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if (!$item['is_dir']): ?>
|
||||||
|
<a href="admin.php?route=content-edit&file=<?= urlencode($item['path']) ?>" class="btn btn-sm btn-outline-primary" title="Bewerken">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form method="POST" action="admin.php?route=content-delete&file=<?= urlencode($item['path']) ?>" class="d-inline" onsubmit="return confirm('Weet je zeker dat je dit bestand wilt verwijderen?')">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger" title="Verwijderen">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
78
admin-console/templates/pages/dashboard.php
Normal file
78
admin-console/templates/pages/dashboard.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<h2 class="mb-4"><i class="bi bi-speedometer2"></i> Dashboard</h2>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card shadow-sm">
|
||||||
|
<div class="card-body d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h6 class="text-muted mb-1">Pagina's</h6>
|
||||||
|
<h3 class="mb-0"><?= $stats['pages'] ?></h3>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-file-earmark-text stat-icon text-primary"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card shadow-sm">
|
||||||
|
<div class="card-body d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h6 class="text-muted mb-1">Mappen</h6>
|
||||||
|
<h3 class="mb-0"><?= $stats['directories'] ?></h3>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-folder stat-icon text-warning"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card shadow-sm">
|
||||||
|
<div class="card-body d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h6 class="text-muted mb-1">Plugins</h6>
|
||||||
|
<h3 class="mb-0"><?= $stats['plugins'] ?></h3>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-plug stat-icon text-success"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card shadow-sm">
|
||||||
|
<div class="card-body d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h6 class="text-muted mb-1">Content grootte</h6>
|
||||||
|
<h3 class="mb-0"><?= $stats['content_size'] ?></h3>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-hdd stat-icon text-info"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header"><i class="bi bi-info-circle"></i> Site informatie</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-sm mb-0">
|
||||||
|
<tr><td class="text-muted">Site titel</td><td><?= htmlspecialchars($siteConfig['site_title'] ?? 'CodePress') ?></td></tr>
|
||||||
|
<tr><td class="text-muted">Standaard taal</td><td><?= htmlspecialchars($siteConfig['language']['default'] ?? 'nl') ?></td></tr>
|
||||||
|
<tr><td class="text-muted">Auteur</td><td><?= htmlspecialchars($siteConfig['author']['name'] ?? '-') ?></td></tr>
|
||||||
|
<tr><td class="text-muted">PHP versie</td><td><?= $stats['php_version'] ?></td></tr>
|
||||||
|
<tr><td class="text-muted">Config geladen</td><td><?= $stats['config_exists'] ? '<span class="badge bg-success">Ja</span>' : '<span class="badge bg-danger">Nee</span>' ?></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header"><i class="bi bi-lightning"></i> Snelle acties</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a href="admin.php?route=content-new" class="btn btn-outline-primary"><i class="bi bi-plus-lg"></i> Nieuwe pagina</a>
|
||||||
|
<a href="admin.php?route=config" class="btn btn-outline-secondary"><i class="bi bi-sliders"></i> Configuratie bewerken</a>
|
||||||
|
<a href="admin.php?route=content" class="btn btn-outline-info"><i class="bi bi-folder2-open"></i> Content beheren</a>
|
||||||
|
<a href="index.php" target="_blank" class="btn btn-outline-success"><i class="bi bi-box-arrow-up-right"></i> Website bekijken</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
44
admin-console/templates/pages/plugins.php
Normal file
44
admin-console/templates/pages/plugins.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<h2 class="mb-4"><i class="bi bi-plug"></i> Plugins</h2>
|
||||||
|
|
||||||
|
<?php if (empty($plugins)): ?>
|
||||||
|
<div class="alert alert-info">Geen plugins gevonden in de plugins map.</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="row g-4">
|
||||||
|
<?php foreach ($plugins as $plugin): ?>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<strong><i class="bi bi-plug"></i> <?= htmlspecialchars($plugin['name']) ?></strong>
|
||||||
|
<?php if ($plugin['enabled']): ?>
|
||||||
|
<span class="badge bg-success">Actief</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge bg-secondary">Inactief</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-sm mb-0">
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Hoofdbestand</td>
|
||||||
|
<td>
|
||||||
|
<?= $plugin['has_main'] ? '<i class="bi bi-check-circle text-success"></i> Aanwezig' : '<i class="bi bi-x-circle text-danger"></i> Ontbreekt' ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Configuratie</td>
|
||||||
|
<td>
|
||||||
|
<?= $plugin['has_config'] ? '<i class="bi bi-check-circle text-success"></i> Aanwezig' : '<i class="bi bi-dash-circle text-muted"></i> Geen' ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">README</td>
|
||||||
|
<td>
|
||||||
|
<?= $plugin['has_readme'] ? '<i class="bi bi-check-circle text-success"></i> Aanwezig' : '<i class="bi bi-dash-circle text-muted"></i> Geen' ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
93
admin-console/templates/pages/users.php
Normal file
93
admin-console/templates/pages/users.php
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<h2 class="mb-4"><i class="bi bi-people"></i> Gebruikers</h2>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Users list -->
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header"><i class="bi bi-list"></i> Huidige gebruikers</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Gebruikersnaam</th>
|
||||||
|
<th>Rol</th>
|
||||||
|
<th>Aangemaakt</th>
|
||||||
|
<th style="width: 160px;">Acties</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($users as $u): ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<i class="bi bi-person-circle"></i>
|
||||||
|
<?= htmlspecialchars($u['username']) ?>
|
||||||
|
<?php if ($u['username'] === $user['username']): ?>
|
||||||
|
<span class="badge bg-info">Jij</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><span class="badge bg-primary"><?= htmlspecialchars($u['role']) ?></span></td>
|
||||||
|
<td class="text-muted"><?= htmlspecialchars($u['created']) ?></td>
|
||||||
|
<td>
|
||||||
|
<!-- Change password -->
|
||||||
|
<form method="POST" action="admin.php?route=users" class="d-inline">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||||
|
<input type="hidden" name="action" value="change_password">
|
||||||
|
<input type="hidden" name="pw_username" value="<?= htmlspecialchars($u['username']) ?>">
|
||||||
|
<div class="input-group input-group-sm d-inline-flex" style="width: auto;">
|
||||||
|
<input type="password" name="new_password" placeholder="Nieuw ww" class="form-control form-control-sm" style="width: 100px;" required minlength="8">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-warning" title="Wachtwoord wijzigen">
|
||||||
|
<i class="bi bi-key"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php if ($u['username'] !== $user['username']): ?>
|
||||||
|
<form method="POST" action="admin.php?route=users" class="d-inline ms-1" onsubmit="return confirm('Weet je zeker dat je deze gebruiker wilt verwijderen?')">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="delete_username" value="<?= htmlspecialchars($u['username']) ?>">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger" title="Verwijderen">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add user form -->
|
||||||
|
<div class="col-md-5">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header"><i class="bi bi-person-plus"></i> Gebruiker toevoegen</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="admin.php?route=users">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||||
|
<input type="hidden" name="action" value="add">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">Gebruikersnaam</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Wachtwoord</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required minlength="8">
|
||||||
|
<small class="form-text text-muted">Minimaal 8 tekens.</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="role" class="form-label">Rol</label>
|
||||||
|
<select class="form-select" id="role" name="role">
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
<option value="editor">Editor</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-person-plus"></i> Toevoegen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -50,6 +50,17 @@ CodePress is a lightweight, file-based Content Management System built with PHP
|
|||||||
- **Dynamic layouts** with YAML frontmatter
|
- **Dynamic layouts** with YAML frontmatter
|
||||||
- **Sidebar support** with plugin integration and toggle function via breadcrumb
|
- **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
|
## Installation
|
||||||
|
|
||||||
1. Clone or download CodePress files
|
1. Clone or download CodePress files
|
||||||
|
|||||||
@@ -50,6 +50,17 @@ CodePress CMS is een lichtgewicht, file-based content management systeem gebouwd
|
|||||||
- **Dynamic layouts** met YAML frontmatter
|
- **Dynamic layouts** met YAML frontmatter
|
||||||
- **Sidebar support** met plugin integratie en toggle functie via breadcrumb
|
- **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
|
## Installatie
|
||||||
|
|
||||||
1. Upload bestanden naar webserver
|
1. Upload bestanden naar webserver
|
||||||
|
|||||||
449
public/admin.php
Normal file
449
public/admin.php
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CodePress Admin Console - Entry Point
|
||||||
|
* Access via: /admin.php?route=login|dashboard|content|config|plugins|users|logout
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Security headers
|
||||||
|
header('X-Content-Type-Options: nosniff');
|
||||||
|
header('X-Frame-Options: SAMEORIGIN');
|
||||||
|
header('X-XSS-Protection: 1; mode=block');
|
||||||
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||||
|
header_remove('X-Powered-By');
|
||||||
|
|
||||||
|
// Load admin components
|
||||||
|
$appConfig = require __DIR__ . '/../admin-console/config/app.php';
|
||||||
|
require_once __DIR__ . '/../admin-console/src/AdminAuth.php';
|
||||||
|
|
||||||
|
$auth = new AdminAuth($appConfig);
|
||||||
|
|
||||||
|
// Routing
|
||||||
|
$route = $_GET['route'] ?? '';
|
||||||
|
|
||||||
|
// Public routes (no auth required)
|
||||||
|
if ($route === 'login') {
|
||||||
|
handleLogin($auth);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other routes require authentication
|
||||||
|
if (!$auth->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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user