CodePress/public/admin.php
Edwin Noorlander 8e18a5d87a 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
2026-02-16 17:01:02 +01:00

450 lines
13 KiB
PHP

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