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
|
||||
- 🎨 **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
|
||||
|
||||
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
|
||||
- 🎨 **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
|
||||
|
||||
@@ -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
|
||||
|
||||
return [
|
||||
'name' => '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',
|
||||
],
|
||||
];
|
||||
// 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',
|
||||
];
|
||||
|
||||
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
|
||||
- **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
|
||||
|
||||
@@ -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
|
||||
|
||||
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