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:
2026-02-16 17:01:02 +01:00
parent 1cd9c8841d
commit 8e18a5d87a
20 changed files with 1420 additions and 172 deletions

View File

@@ -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
}

View 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
}
}

View File

@@ -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',
];

View 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);
}
}

View File

@@ -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";
}
}

View File

@@ -0,0 +1 @@
{"admi":{"count":1,"last_attempt":1771257322}}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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; ?>

View 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>