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; $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; } 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); } public function regenerateCsrfToken(): void { $_SESSION['admin_csrf_token'] = bin2hex(random_bytes(32)); } // --- 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.']; } 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'); $ip = $_SERVER['REMOTE_ADDR'] ?? 'cli'; file_put_contents($logFile, "[{$timestamp}] [{$level}] [{$ip}] {$message}\n", FILE_APPEND); } }