['*'], 'content-manager' => ['dashboard', 'content', 'content-edit', 'content-new', 'content-delete', 'content-dir-create', 'content-dir-rename', 'content-dir-delete', 'content-move', 'content-backup', 'content-restore', 'content-git-init', 'content-git-commit', 'content-git-restore', 'guide', 'logout'], 'bi-manager' => ['dashboard', 'statistics', 'logs', 'guide', 'logout'], 'site-admin' => ['dashboard', 'theme', 'theme-new', 'plugins', 'plugins-new', 'plugins-edit', 'plugins-config', 'plugins-toggle', 'plugins-delete', 'statistics', 'logs', 'update', 'guide', 'logout'], ]; /** * Human-readable role labels. */ public const ROLE_LABELS = [ 'admin' => 'Admin', 'content-manager' => 'Content Beheerder', 'bi-manager' => 'BI Beheerder', 'site-admin' => 'Site Admin', ]; 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']; $examplePath = dirname($path) . '/admin.json.example'; if (!file_exists($path)) { if (file_exists($examplePath)) { @copy($examplePath, $path); } else { $defaultAdminConfig = [ 'users' => [ [ 'username' => 'admin', 'password_hash' => password_hash('admin', PASSWORD_BCRYPT), 'role' => 'admin', 'created' => date('Y-m-d'), ] ], 'security' => [ 'session_timeout' => 1800, 'max_login_attempts' => 5, 'lockout_duration' => 900, ] ]; $dir = dirname($path); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } @file_put_contents($path, json_encode($defaultAdminConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } } $data = file_exists($path) ? json_decode(file_get_contents($path), true) : null; 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; $isHttps = !empty($_SERVER['HTTPS']) || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') || (isset($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on'); session_set_cookie_params([ 'lifetime' => $timeout, 'path' => '/', 'secure' => $isHttps, 'httponly' => true, 'samesite' => 'Strict' ]); 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; } $username = $_SESSION['admin_user']; $userData = [ 'username' => $username, 'role' => $_SESSION['admin_role'] ?? 'admin' ]; // Enrich with profile fields from admin.json $userEntry = $this->findUser($username); if ($userEntry) { $userData['email'] = $userEntry['email'] ?? ''; $userData['author_name'] = $userEntry['author_name'] ?? ''; $userData['author_email'] = $userEntry['author_email'] ?? ''; } return $userData; } /** * Get the role of the current user. */ public function getCurrentRole(): string { return $_SESSION['admin_role'] ?? 'admin'; } /** * Check if the current user has permission to access a route. */ public function hasPermission(string $route): bool { $role = $this->getCurrentRole(); $permissions = self::ROLE_PERMISSIONS[$role] ?? ['dashboard', 'logout']; if (in_array('*', $permissions, true)) { return true; } return in_array($route, $permissions, true); } /** * Get available roles. */ public static function getRoles(): array { return self::ROLE_LABELS; } /** * Get role label. */ public static function getRoleLabel(string $role): string { return self::ROLE_LABELS[$role] ?? $role; } 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); } public function regenerateCsrfToken(): void { $_SESSION['admin_csrf_token'] = bin2hex(random_bytes(32)); } // --- User Management --- public function getUsers(): array { $users = []; foreach ($this->adminConfig['users'] ?? [] as $u) { $role = $u['role'] ?? 'admin'; $users[$u['username']] = [ 'username' => $u['username'], 'role' => $role, 'role_label' => self::getRoleLabel($role), 'created' => $u['created'] ?? '', 'email' => $u['email'] ?? '', 'author_name' => $u['author_name'] ?? '', 'author_email' => $u['author_email'] ?? '', ]; } return $users; } public function addUser(string $username, string $password, string $role = 'admin', string $email = '', string $authorName = '', string $authorEmail = ''): 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.']; } if (!isset(self::ROLE_PERMISSIONS[$role])) { return ['success' => false, 'message' => 'Ongeldige rol.']; } $this->adminConfig['users'][] = [ 'username' => $username, 'password_hash' => password_hash($password, PASSWORD_DEFAULT), 'role' => $role, 'email' => $email, 'author_name' => $authorName, 'author_email' => $authorEmail, 'created' => date('Y-m-d') ]; $this->saveAdminConfig(); $this->log('info', "Gebruiker aangemaakt: {$username} (rol: {$role})"); return ['success' => true, 'message' => 'Gebruiker aangemaakt.']; } /** * Update the profile (email, author_name, author_email) of a user. */ public function updateUserProfile(string $username, string $email = '', string $authorName = '', string $authorEmail = ''): array { foreach ($this->adminConfig['users'] as &$userEntry) { if ($userEntry['username'] === $username) { $userEntry['email'] = $email; $userEntry['author_name'] = $authorName; $userEntry['author_email'] = $authorEmail; $this->saveAdminConfig(); $this->log('info', "Profiel bijgewerkt: {$username}"); return ['success' => true, 'message' => 'Profiel opgeslagen.']; } } return ['success' => false, 'message' => 'Gebruiker niet gevonden.']; } /** * Change the role of an existing user. */ public function changeRole(string $username, string $role): array { if (!isset(self::ROLE_PERMISSIONS[$role])) { return ['success' => false, 'message' => 'Ongeldige rol.']; } foreach ($this->adminConfig['users'] as &$user) { if ($user['username'] === $username) { $user['role'] = $role; $this->saveAdminConfig(); $this->log('info', "Rol gewijzigd: {$username} -> {$role}"); return ['success' => true, 'message' => 'Rol gewijzigd.']; } } return ['success' => false, 'message' => 'Gebruiker niet gevonden.']; } 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.']; } public function changeOwnPassword(string $username, string $currentPassword, string $newPassword): array { $user = $this->findUser($username); if (!$user) { return ['success' => false, 'message' => 'Gebruiker niet gevonden.']; } if (!password_verify($currentPassword, $user['password_hash'])) { return ['success' => false, 'message' => 'Huidig wachtwoord is onjuist.']; } if (strlen($newPassword) < 8) { return ['success' => false, 'message' => 'Nieuw wachtwoord moet minimaal 8 tekens zijn.']; } foreach ($this->adminConfig['users'] as &$u) { if ($u['username'] === $username) { $u['password_hash'] = password_hash($newPassword, PASSWORD_DEFAULT); $this->saveAdminConfig(); $this->log('info', "Eigen wachtwoord gewijzigd: {$username}"); return ['success' => true, 'message' => 'Wachtwoord gewijzigd.']; } } return ['success' => false, 'message' => 'Fout bij wijzigen wachtwoord.']; } // --- 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'); if (!class_exists('RequestLogger')) { $loggerClass = __DIR__ . '/../../cms/core/class/RequestLogger.php'; if (file_exists($loggerClass)) { require_once $loggerClass; } } $ip = class_exists('RequestLogger') ? RequestLogger::getClientIp() : ($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'); file_put_contents($logFile, "[{$timestamp}] [{$level}] [{$ip}] {$message}\n", FILE_APPEND); } }