⚠️ Database Driver Ontbreekt

De extensie pdo_mysql is niet actief in deze PHP-omgeving.

"); } $tzOffset = date('P'); // Dynamische offset conform Europe/Amsterdam (+02:00 in zomertijd, +01:00 in wintertijd) $options = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_TIMEOUT => 5, ]; if (defined('PDO::MYSQL_ATTR_MULTI_STATEMENTS')) { $options[PDO::MYSQL_ATTR_MULTI_STATEMENTS] = true; } if (defined('PDO::MYSQL_ATTR_INIT_COMMAND')) { $options[PDO::MYSQL_ATTR_INIT_COMMAND] = "SET time_zone = '{$tzOffset}'"; } $dsnWithDb = sprintf( 'mysql:host=%s;port=%s;dbname=%s;charset=%s', DB_HOST, DB_PORT, DB_NAME, DB_CHARSET ); // 2. CHECK: Bestaat de database al? try { $pdo = new PDO($dsnWithDb, DB_USER, DB_PASS, $options); // Garandeer dat MariaDB sessietijd 1-op-1 synchroon loopt met PHP tijd (Europe/Amsterdam) $pdo->exec("SET time_zone = '{$tzOffset}'"); ensure_tables_exist($pdo); return $pdo; } catch (PDOException $e) { error_log("MariaDB Verbindingsfout [{$e->getCode()}]: {$e->getMessage()}"); // Foutcode 1049 betekent: Database 'DB_NAME' bestaat nog NIET op de MariaDB server if ($e->getCode() == 1049 || str_contains($e->getMessage(), 'Unknown database')) { try { $serverDsn = sprintf('mysql:host=%s;port=%s;charset=%s', DB_HOST, DB_PORT, DB_CHARSET); $serverPdo = new PDO($serverDsn, DB_USER, DB_PASS, $options); $serverPdo->exec(sprintf( 'CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci', DB_NAME )); $pdo = new PDO($dsnWithDb, DB_USER, DB_PASS, $options); $pdo->exec("SET time_zone = '{$tzOffset}'"); ensure_tables_exist($pdo); return $pdo; } catch (PDOException $createEx) { error_log("Kan database " . DB_NAME . " niet automatisch aanmaken: " . $createEx->getMessage()); } } // Pen Test beveiliging: in live omgeving geen interne IP's of database-details tonen! if (defined('APP_ENV') && APP_ENV === 'live') { die("
⚠️

Database Tijdelijk Niet Bereikbaar

Er kon geen veilige verbinding tot stand worden gebracht met de database. Probeer het over enkele ogenblikken opnieuw.

"); } else { die("

MariaDB Verbindingsfout (" . htmlspecialchars((string)$e->getCode()) . ")

" . htmlspecialchars($e->getMessage()) . "

Host: " . htmlspecialchars(DB_HOST) . "
Poort: " . htmlspecialchars(DB_PORT) . "
Database: " . htmlspecialchars(DB_NAME) . "
Gebruiker: " . htmlspecialchars(DB_USER) . "
"); } } } /** * Zorgt ervoor dat ALLE benodigde tabellen en kolommen uit schema.sql aanwezig zijn in MariaDB * Voert elke tabel individueel uit zodat ontbrekende tabellen direct aangemaakt worden. */ function ensure_tables_exist(PDO $pdo): void { static $tablesChecked = false; if ($tablesChecked) return; $tablesChecked = true; try { $schemaFile = file_exists(__DIR__ . '/schema.sql') ? (__DIR__ . '/schema.sql') : (__DIR__ . '/../schema.sql'); if (file_exists($schemaFile)) { $sql = file_get_contents($schemaFile); $statements = array_filter(array_map('trim', explode(';', $sql))); foreach ($statements as $stmt) { $stmtClean = preg_replace('/^--.*$/m', '', $stmt); $stmtClean = trim($stmtClean); if (!empty($stmtClean)) { try { $pdo->exec($stmtClean); } catch (Throwable $t) { // Stil negeren indien tabel of relatie al bestaat } } } } // Zorg ervoor dat alle kolommen en upgrades aanwezig zijn $colUpgrades = [ "ALTER TABLE tasks MODIFY external_id VARCHAR(255) NULL", "ALTER TABLE tasks ADD COLUMN external_url TEXT NULL AFTER external_id", "ALTER TABLE users ADD COLUMN remember_token VARCHAR(64) NULL AFTER backup_codes", "ALTER TABLE users ADD COLUMN remember_token_expires_at DATETIME NULL AFTER remember_token", "ALTER TABLE users ADD COLUMN notification_settings TEXT NULL AFTER remember_token_expires_at", "ALTER TABLE users ADD COLUMN theme VARCHAR(20) NOT NULL DEFAULT 'dark' AFTER notification_settings", "ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255) NULL AFTER theme", "ALTER TABLE users MODIFY COLUMN avatar_url MEDIUMTEXT NULL", "ALTER TABLE users ADD COLUMN backup_codes TEXT NULL AFTER two_factor_enabled", "ALTER TABLE users ADD COLUMN default_jira_filter_code VARCHAR(50) NULL AFTER avatar_url", "ALTER TABLE users ADD COLUMN fociz_widget_layout TEXT NULL AFTER default_jira_filter_code", "ALTER TABLE users ADD COLUMN last_mail_check_at DATETIME NULL AFTER fociz_widget_layout", "ALTER TABLE users ADD COLUMN last_jira_check_at DATETIME NULL AFTER last_mail_check_at", "ALTER TABLE board_card_schedules ADD UNIQUE KEY uk_sched_card (card_id)", "CREATE TABLE IF NOT EXISTS `password_resets` (`id` INT AUTO_INCREMENT PRIMARY KEY, `email` VARCHAR(191) NOT NULL, `token` VARCHAR(64) NOT NULL UNIQUE, `expires_at` DATETIME NOT NULL, `used_at` DATETIME NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX (`email`), INDEX (`token`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", "CREATE TABLE IF NOT EXISTS `jira_queue_filters` (`id` INT AUTO_INCREMENT PRIMARY KEY, `filter_code` VARCHAR(50) NOT NULL UNIQUE, `filter_name` VARCHAR(120) NOT NULL, `project_key` VARCHAR(50) NULL, `jql_clause` TEXT NOT NULL, `description` VARCHAR(255) NULL, `is_active` TINYINT(1) NOT NULL DEFAULT 1, `sort_order` INT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", // V2.0: Subtaken & Checklists "CREATE TABLE IF NOT EXISTS `task_subtasks` (`id` INT AUTO_INCREMENT PRIMARY KEY, `task_id` INT NOT NULL, `user_id` INT NOT NULL, `title` VARCHAR(255) NOT NULL, `is_completed` TINYINT(1) NOT NULL DEFAULT 0, `position` INT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX (`task_id`, `position`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", // Beveiliging: IP tracking & locatie-verificatie "ALTER TABLE users ADD COLUMN last_login_ip VARCHAR(45) NULL AFTER last_jira_check_at", "ALTER TABLE users ADD COLUMN last_login_at DATETIME NULL AFTER last_login_ip", "CREATE TABLE IF NOT EXISTS `user_known_ips` (`id` INT AUTO_INCREMENT PRIMARY KEY, `user_id` INT NOT NULL, `ip_address` VARCHAR(45) NOT NULL, `user_agent` VARCHAR(255) NULL, `is_verified` TINYINT(1) NOT NULL DEFAULT 1, `verify_token` VARCHAR(64) NULL, `verify_token_expires_at` DATETIME NULL, `first_seen_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `last_seen_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_user_ip` (`user_id`, `ip_address`), INDEX (`verify_token`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", // SOC 2 & ISO 27001: Onwijzigbare Audit Trail "CREATE TABLE IF NOT EXISTS `audit_logs` (`id` INT AUTO_INCREMENT PRIMARY KEY, `user_id` INT NULL, `user_email` VARCHAR(191) NULL, `action` VARCHAR(64) NOT NULL, `resource_type` VARCHAR(64) NULL, `resource_id` INT NULL, `details` TEXT NULL, `ip_address` VARCHAR(45) NOT NULL, `user_agent` VARCHAR(255) NULL, `status` ENUM('SUCCESS', 'FAILED', 'WARNING', 'PENDING') NOT NULL DEFAULT 'SUCCESS', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX (`user_id`), INDEX (`action`), INDEX (`created_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", ]; foreach ($colUpgrades as $sql) { try { $pdo->exec($sql); } catch (Throwable $t) { // Stil negeren indien kolom al bestaat } } // Zorg dat de standaard Jira Servicedesk wachtrijfilters (DX en SD) aanwezig zijn try { $filterCount = (int)$pdo->query("SELECT COUNT(*) FROM jira_queue_filters")->fetchColumn(); if ($filterCount === 0) { $insertFilterStmt = $pdo->prepare(" INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order) VALUES (?, ?, ?, ?, ?, 1, ?) "); $insertFilterStmt->execute([ 'DX', 'DX — Koppelingen, SSO & Migratie', 'SD', '"Onderwerp logging[dropdown]" in (Koppelingen, SSO-koppeling, Rest-endpoints, Datadump, UWV-koppeling, Migratie)', 'Servicedesk tickets met betrekking tot koppelingen, endpoints en datamigraties.', 1 ]); $insertFilterStmt->execute([ 'SD', 'SD — Regulier Servicedesk', 'SD', '("Onderwerp logging[Dropdown]" = EMPTY OR "Onderwerp logging[Dropdown]" != Koppelingen AND "Onderwerp logging[Dropdown]" != Migratie AND "Onderwerp logging[Dropdown]" != SSO-koppeling AND "Onderwerp logging[Dropdown]" != UWV-koppeling AND "Onderwerp logging[Dropdown]" != Rest-endpoints AND "Onderwerp logging[Dropdown]" != Datadump)', 'Reguliere servicedesk incidenten, algemene meldingen en gebruikersvragen.', 2 ]); } } catch (Throwable $e) { // Stil negeren indien tabel nog in opbouw is } // Zorg dat er ten minste één standaard bord met standaard lanes aanwezig is try { $boardCount = (int)$pdo->query("SELECT COUNT(*) FROM boards")->fetchColumn(); if ($boardCount === 0) { $firstAdminId = (int)$pdo->query("SELECT id FROM users ORDER BY (role = 'ADMIN') DESC, id ASC LIMIT 1")->fetchColumn(); if ($firstAdminId > 0) { $pdo->prepare("INSERT INTO boards (title, description, created_by_id) VALUES (?, ?, ?)") ->execute(['Hoofdbord', 'Standaard team- en takenbord voor opdrachten', $firstAdminId]); $boardId = (int)$pdo->lastInsertId(); // Maak beheerder lid van het bord $pdo->prepare("INSERT INTO board_members (board_id, user_id, role) VALUES (?, ?, 'ADMIN')") ->execute([$boardId, $firstAdminId]); // Maak de 4 standaard lanes aan $defaultLanes = [ ['title' => 'Open', 'color' => 'slate', 'pos' => 0], ['title' => 'In uitvoering', 'color' => 'amber', 'pos' => 1], ['title' => 'Testen', 'color' => 'purple', 'pos' => 2], ['title' => 'Klaar', 'color' => 'emerald', 'pos' => 3], ]; $laneStmt = $pdo->prepare("INSERT INTO board_lanes (board_id, title, color, position) VALUES (?, ?, ?, ?)"); foreach ($defaultLanes as $l) { $laneStmt->execute([$boardId, $l['title'], $l['color'], $l['pos']]); } } } // Zorg dat eerdere audit logs die in UTC (+00:00) zijn weggeschreven worden gesynchroniseerd naar Europe/Amsterdam (+2 uur) try { $stmtCheck = $pdo->query("SELECT setting_value FROM system_settings WHERE setting_key = 'audit_logs_tz_fixed' LIMIT 1"); $fixed = $stmtCheck ? $stmtCheck->fetchColumn() : null; if (!$fixed) { // Pas bestaande audit logs eenmalig aan zodat de weergave klopt met de lokale tijd $pdo->exec("UPDATE audit_logs SET created_at = DATE_ADD(created_at, INTERVAL 2 HOUR)"); $pdo->prepare(" INSERT INTO system_settings (setting_key, setting_value, created_at, updated_at) VALUES ('audit_logs_tz_fixed', '1', NOW(), NOW()) ON DUPLICATE KEY UPDATE setting_value = '1', updated_at = NOW() ")->execute(); } } catch (Throwable $e) { // Stil negeren indien system_settings tabel nog niet bestaat } } catch (Throwable $e) { error_log("Fout bij controleren tabellen: " . $e->getMessage()); } }