Files
Fociz/config/database.php
T

237 lines
14 KiB
PHP

<?php
/**
* MariaDB PDO Databaseverbinding & Automatische Tabellencontrole
*/
// Blokkeer directe HTTP toegang (Pen Test Beveiliging)
if (basename($_SERVER['SCRIPT_FILENAME'] ?? '') === 'database.php') {
http_response_code(403);
exit('Toegang geweigerd.');
}
require_once __DIR__ . '/config.php';
function get_db_connection(): PDO {
static $pdo = null;
if ($pdo !== null) {
return $pdo;
}
// 1. Controleer of de PDO MySQL/MariaDB driver aanwezig is in PHP
$availableDrivers = PDO::getAvailableDrivers();
if (!in_array('mysql', $availableDrivers)) {
error_log("PDO MySQL driver (pdo_mysql) ontbreekt in PHP.");
die("
<div style='font-family: sans-serif; background: #0c0f17; color: #f1f5f9; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px;'>
<div style='max-width: 580px; background: #111827; border: 1px solid #ef4444; border-radius: 16px; padding: 30px;'>
<h2 style='color: #ef4444; margin-top: 0;'>⚠️ Database Driver Ontbreekt</h2>
<p style='color: #94a3b8; font-size: 14px;'>De extensie <strong>pdo_mysql</strong> is niet actief in deze PHP-omgeving.</p>
</div>
</div>
");
}
$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;
}
$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);
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);
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("
<div style='font-family: sans-serif; background: #0c0f17; color: #fff; padding: 40px; text-align: center; min-height: 100vh; display: flex; align-items: center; justify-content: center;'>
<div style='max-width: 480px; background: #111827; border: 1px solid #334155; border-radius: 16px; padding: 30px; text-align: center;'>
<div style='width: 48px; height: 48px; margin: 0 auto 16px; border-radius: 12px; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); display: flex; align-items: center; justify-content: center; color: #f87171; font-weight: bold;'>⚠️</div>
<h2 style='color: #f87171; margin-top: 0; font-size: 18px;'>Database Tijdelijk Niet Bereikbaar</h2>
<p style='color: #94a3b8; font-size: 13px; line-height: 1.6;'>
Er kon geen veilige verbinding tot stand worden gebracht met de database. Probeer het over enkele ogenblikken opnieuw.
</p>
</div>
</div>
");
} else {
die("
<div style='font-family: sans-serif; background: #0c0f17; color: #fff; padding: 40px; text-align: center; min-height: 100vh; display: flex; align-items: center; justify-content: center;'>
<div style='max-width: 520px; background: #111827; border: 1px solid #ef4444; border-radius: 16px; padding: 30px; text-align: left;'>
<h2 style='color: #ef4444; margin-top: 0;'>MariaDB Verbindingsfout (" . htmlspecialchars((string)$e->getCode()) . ")</h2>
<p style='color: #cbd5e1; font-size: 14px;'>" . htmlspecialchars($e->getMessage()) . "</p>
<div style='background: #0b0f17; border: 1px solid #1f2937; padding: 15px; border-radius: 10px; font-size: 13px; margin-top: 15px;'>
Host: <strong>" . htmlspecialchars(DB_HOST) . "</strong><br>
Poort: <strong>" . htmlspecialchars(DB_PORT) . "</strong><br>
Database: <strong>" . htmlspecialchars(DB_NAME) . "</strong><br>
Gebruiker: <strong>" . htmlspecialchars(DB_USER) . "</strong>
</div>
</div>
</div>
");
}
}
}
/**
* 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 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']]);
}
}
}
} catch (Throwable $e) {
// Stil negeren indien tabellen nog in opbouw zijn
}
} catch (Throwable $e) {
error_log("Fout bij controleren tabellen: " . $e->getMessage());
}
}