diff --git a/api/integrations.php b/api/integrations.php index 7c8656c..edefbfa 100644 --- a/api/integrations.php +++ b/api/integrations.php @@ -28,7 +28,7 @@ try { $stmt = $db->prepare("SELECT settings FROM integrations WHERE user_id = ? AND provider = 'JIRA'"); $stmt->execute([$user['id']]); $saved = json_decode($stmt->fetchColumn() ?: '{}', true); - $apiToken = $saved['jira_api_token'] ?? ''; + $apiToken = fociz_decrypt($saved['jira_api_token'] ?? ''); if (empty($domain)) $domain = $saved['jira_domain'] ?? ''; if (empty($email)) $email = $saved['jira_email'] ?? ''; } @@ -40,7 +40,7 @@ try { $settings = json_encode([ 'jira_domain' => jira_normalize_domain($domain), 'jira_email' => $email, - 'jira_api_token' => $apiToken, + 'jira_api_token' => fociz_encrypt($apiToken), 'account_name' => $jiraUser['displayName'] ?? '', 'account_id' => $jiraUser['accountId'] ?? '', ]); diff --git a/auth/callback_microsoft.php b/auth/callback_microsoft.php index d86525e..c64ef39 100644 --- a/auth/callback_microsoft.php +++ b/auth/callback_microsoft.php @@ -137,7 +137,12 @@ $saveStmt = $db->prepare(" settings = VALUES(settings), last_sync_at = NOW() "); -$saveStmt->execute([$user['id'], $accessToken, $refreshToken, json_encode($newSettings)]); +$saveStmt->execute([ + $user['id'], + fociz_encrypt($accessToken), + fociz_encrypt($refreshToken), + json_encode($newSettings) +]); // 8. Succesvol terugkeren naar de gewenste pagina of het Persoonlijk Profiel $returnTo = $_SESSION['ms_oauth_return_to'] ?? '/profile.php'; diff --git a/auth/login.php b/auth/login.php index 28f2741..968de55 100644 --- a/auth/login.php +++ b/auth/login.php @@ -22,13 +22,19 @@ if ($currentUser) { $error = null; $infoMessage = null; if (isset($_GET['expired'])) { - $infoMessage = "Uw dagsessie is om 23:59 beëindigd. Log opnieuw in om een nieuwe werkdag te starten."; + $infoMessage = "Uw dagsessie is beëindigd. Log opnieuw in om verder te gaan."; +} +if (isset($_GET['ip_changed'])) { + $infoMessage = "U bent ingelogd vanaf een andere netwerklocatie. Uit veiligheidsoverwegingen dient u opnieuw in te loggen met 2FA."; +} +if (isset($_GET['ip_verified'])) { + $infoMessage = "Nieuwe inloglocatie succesvol geverifieerd! U kunt nu veilig inloggen."; } $step = isset($_SESSION['pending_2fa_user_id']) ? 2 : 1; // Terug naar stap 1 if (isset($_GET['action']) && $_GET['action'] === 'restart') { - unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_user_email']); + unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_user_email'], $_SESSION['pending_2fa_user_name'], $_SESSION['pending_new_ip']); header('Location: /auth/login.php'); exit; } @@ -50,11 +56,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $user = $stmt->fetch(); if ($user && password_verify($password, $user['password_hash'])) { - // Succesvol stap 1: zet tijdelijke pending sessie voor 2FA - $_SESSION['pending_2fa_user_id'] = $user['id']; - $_SESSION['pending_2fa_user_email'] = $user['email']; - $_SESSION['pending_2fa_user_name'] = $user['name']; - $step = 2; + // Controleer of de inloglocatie/IP bekend is + $clientIp = fociz_get_client_ip(); + $isIpKnown = fociz_is_ip_known_for_user((int)$user['id'], $clientIp); + + if (!$isIpKnown) { + // Stuur verificatie-mail en informeer gebruiker + fociz_send_new_ip_verification((int)$user['id'], $user['email'], $user['name'], $clientIp); + $error = "Nieuwe inloglocatie gedetecteerd ({$clientIp}). Er is een verificatie-e-mail gestuurd naar {$user['email']}. Klik op de link in de e-mail om deze locatie toe te staan."; + } else { + // Succesvol stap 1: zet tijdelijke pending sessie voor 2FA + $_SESSION['pending_2fa_user_id'] = $user['id']; + $_SESSION['pending_2fa_user_email'] = $user['email']; + $_SESSION['pending_2fa_user_name'] = $user['name']; + $step = 2; + } } else { $error = "Onjuist e-mailadres of wachtwoord."; } @@ -105,12 +121,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($isValid) { // Volledige login succesvol (geldig tot 23:59:59 van vandaag) + $clientIp = fociz_get_client_ip(); + fociz_register_user_ip((int)$user['id'], $clientIp, true); + $_SESSION['user_id'] = $user['id']; $_SESSION['user_name'] = $user['name']; $_SESSION['user_email'] = $user['email']; $_SESSION['user_role'] = $user['role']; $_SESSION['is_2fa_verified'] = true; $_SESSION['auth_date'] = date('Y-m-d'); + $_SESSION['auth_fingerprint'] = fociz_get_client_fingerprint(); $_SESSION['session_expires_at'] = strtotime('today 23:59:59'); $_SESSION['last_activity'] = time(); @@ -249,10 +269,7 @@ $pageTitle = "Fociz — Inloggen";
- - - Tip: U kunt ook een 8-cijferige herstelcode invoeren. - +
diff --git a/auth/verify-ip.php b/auth/verify-ip.php new file mode 100644 index 0000000..0892c25 --- /dev/null +++ b/auth/verify-ip.php @@ -0,0 +1,135 @@ +prepare(" + SELECT kip.*, u.email, u.name + FROM user_known_ips kip + JOIN users u ON kip.user_id = u.id + WHERE kip.verify_token = ? + "); + $stmt->execute([$token]); + $record = $stmt->fetch(); + + if (!$record) { + $error = "Deze verificatielink is niet gevonden of al gebruikt."; + } elseif (!empty($record['verify_token_expires_at']) && strtotime($record['verify_token_expires_at']) < time()) { + $error = "Deze verificatielink is verlopen (geldigheidsduur was 30 minuten). Probeer opnieuw in te loggen om een nieuwe link te ontvangen."; + } else { + // Verifieer het IP-adres + $updateStmt = $db->prepare(" + UPDATE user_known_ips + SET is_verified = 1, verify_token = NULL, verify_token_expires_at = NULL, last_seen_at = NOW() + WHERE id = ? + "); + $updateStmt->execute([$record['id']]); + + $success = true; + $userEmail = $record['email']; + $ipAddress = $record['ip_address']; + } + } catch (Throwable $e) { + error_log("verify-ip.php error: " . $e->getMessage()); + $error = "Er is een databasefout opgetreden bij het verifiëren."; + } +} + +$pageTitle = "Fociz — Locatie Verificatie"; +?> + + + + + + <?= htmlspecialchars($pageTitle) ?> + + + + + + + + +
+ + +
+
+ +
+

Locatie Verificatie

+

Beveiligde inlogtoegang voor Fociz

+
+ + +
+
+ + Locatie Succesvol Geverifieerd! +
+

+ Het IP-adres is nu geautoriseerd voor uw account (). +

+
+ + + +
+
+ + Verificatie Mislukt +
+

+ +

+
+ + + + +
+ + + + diff --git a/config/database.php b/config/database.php index 7b62a46..0059fff 100644 --- a/config/database.php +++ b/config/database.php @@ -156,6 +156,10 @@ function ensure_tables_exist(PDO $pdo): void { "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", ]; foreach ($colUpgrades as $sql) { try { diff --git a/config/schema.sql b/config/schema.sql index d16c1e3..407feb7 100644 --- a/config/schema.sql +++ b/config/schema.sql @@ -18,6 +18,8 @@ CREATE TABLE IF NOT EXISTS `users` ( `fociz_widget_layout` TEXT NULL, `last_mail_check_at` DATETIME NULL, `last_jira_check_at` DATETIME NULL, + `last_login_ip` VARCHAR(45) NULL, + `last_login_at` DATETIME NULL, `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; @@ -257,3 +259,19 @@ CREATE TABLE IF NOT EXISTS `task_subtasks` ( CONSTRAINT `fk_subtasks_task` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_subtasks_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Fociz Beveiliging: Bekende IP-adressen en verificatie per gebruiker +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`), + CONSTRAINT `fk_known_ips_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/includes/auth.php b/includes/auth.php index a5dc805..7880c79 100644 --- a/includes/auth.php +++ b/includes/auth.php @@ -33,6 +33,188 @@ function is_https_request(): bool { return false; } +/** + * AES-256-GCM / AES-256-CBC encryptie helper voor gevoelige tokens (OAuth, API keys) + */ +function fociz_encrypt(string $plaintext): string { + if (empty($plaintext)) return ''; + $key = hash('sha256', defined('APP_SECRET') ? APP_SECRET : 'fociz-maria-db-super-secret-key-998877', true); + $iv = random_bytes(16); + $encrypted = openssl_encrypt($plaintext, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv); + if ($encrypted === false) return $plaintext; + return 'ENC:' . base64_encode($iv . $encrypted); +} + +/** + * AES-256-CBC decryptie helper; decodeert indien versleuteld, laat legacy plaintext ongemoeid + */ +function fociz_decrypt(string $ciphertext): string { + if (empty($ciphertext)) return ''; + if (!str_starts_with($ciphertext, 'ENC:')) { + return $ciphertext; // Plaintext legacy waarde + } + $raw = base64_decode(substr($ciphertext, 4), true); + if ($raw === false || strlen($raw) < 17) { + return $ciphertext; + } + $iv = substr($raw, 0, 16); + $encrypted = substr($raw, 16); + $key = hash('sha256', defined('APP_SECRET') ? APP_SECRET : 'fociz-maria-db-super-secret-key-998877', true); + $decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv); + return ($decrypted !== false) ? $decrypted : $ciphertext; +} + +/** + * Bepaalt het actuele client IP-adres (rekening houdend met reverse proxies) + */ +function fociz_get_client_ip(): string { + $headers = [ + 'HTTP_CF_CONNECTING_IP', // Cloudflare + 'HTTP_X_REAL_IP', // Nginx reverse proxy + 'HTTP_X_FORWARDED_FOR', // Standard proxy chain + 'REMOTE_ADDR' // Directe verbinding + ]; + + foreach ($headers as $header) { + if (!empty($_SERVER[$header])) { + $ipList = explode(',', $_SERVER[$header]); + $ip = trim($ipList[0]); + if (filter_var($ip, FILTER_VALIDATE_IP)) { + return $ip; + } + } + } + return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; +} + +/** + * Genereert een sessie fingerprint op basis van IP en browser/platform + */ +function fociz_get_client_fingerprint(): string { + $ip = fociz_get_client_ip(); + $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; + // Neem IP en User Agent mee voor strikte sessiebinding + return hash('sha256', "{$ip}|{$ua}"); +} + +/** + * Controleert of een IP-adres bekend en geverifieerd is voor een gebruiker + */ +function fociz_is_ip_known_for_user(int $userId, string $ip): bool { + try { + $db = get_db_connection(); + // Als de gebruiker nog NOOIT een IP heeft opgeslagen, beschouwen we dit als eerste setup + $countStmt = $db->prepare("SELECT COUNT(*) FROM user_known_ips WHERE user_id = ?"); + $countStmt->execute([$userId]); + $totalKnown = (int)$countStmt->fetchColumn(); + if ($totalKnown === 0) { + return true; // Eerste inlog ooit + } + + $stmt = $db->prepare("SELECT is_verified FROM user_known_ips WHERE user_id = ? AND ip_address = ?"); + $stmt->execute([$userId, $ip]); + $row = $stmt->fetch(); + return ($row && !empty($row['is_verified'])); + } catch (Throwable $e) { + error_log("fociz_is_ip_known_for_user error: " . $e->getMessage()); + return true; // Fallback op toegestaan bij databasefout + } +} + +/** + * Registreert of vernieuwt een IP-adres voor een gebruiker + */ +function fociz_register_user_ip(int $userId, string $ip, bool $isVerified = true): void { + try { + $db = get_db_connection(); + $ua = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255); + $stmt = $db->prepare(" + INSERT INTO user_known_ips (user_id, ip_address, user_agent, is_verified, last_seen_at) + VALUES (?, ?, ?, ?, NOW()) + ON DUPLICATE KEY UPDATE + user_agent = VALUES(user_agent), + is_verified = IF(VALUES(is_verified) = 1, 1, is_verified), + last_seen_at = NOW() + "); + $stmt->execute([$userId, $ip, $ua, $isVerified ? 1 : 0]); + + // Werk ook last_login_ip en last_login_at bij op users tabel + $updateUser = $db->prepare("UPDATE users SET last_login_ip = ?, last_login_at = NOW() WHERE id = ?"); + $updateUser->execute([$ip, $userId]); + } catch (Throwable $e) { + error_log("fociz_register_user_ip error: " . $e->getMessage()); + } +} + +/** + * Maakt een verificatietoken aan voor een nieuwe locatie/IP en stuurt een e-mail + */ +function fociz_send_new_ip_verification(int $userId, string $email, string $name, string $ip): bool { + try { + $db = get_db_connection(); + $token = bin2hex(random_bytes(32)); + $expiresAt = date('Y-m-d H:i:s', time() + 1800); // 30 minuten geldig + $ua = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255); + + $stmt = $db->prepare(" + INSERT INTO user_known_ips (user_id, ip_address, user_agent, is_verified, verify_token, verify_token_expires_at) + VALUES (?, ?, ?, 0, ?, ?) + ON DUPLICATE KEY UPDATE + user_agent = VALUES(user_agent), + verify_token = VALUES(verify_token), + verify_token_expires_at = VALUES(verify_token_expires_at) + "); + $stmt->execute([$userId, $ip, $ua, $token, $expiresAt]); + + // Verificatie e-mail opstellen en verzenden + require_once __DIR__ . '/mailer.php'; + $appUrl = defined('APP_URL') ? APP_URL : 'https://www.fociz.nl'; + $verifyUrl = "{$appUrl}/auth/verify-ip.php?token={$token}"; + + $cleanName = !empty($name) ? htmlspecialchars($name) : 'Collega'; + $timeStr = date('d-m-Y H:i'); + + $contentHtml = " +

Beste {$cleanName},

+

+ Er is zojuist geprobeerd in te loggen op uw Fociz-account vanaf een onbekende locatie of nieuw IP-adres: +

+
+ + + + + + + + + +
Nieuw IP-adres:{$ip}
Tijdstip:{$timeStr}
+
+

+ Bent u dit zelf? Klik dan op onderstaande knop om deze locatie te autoriseren. Hierna kunt u direct veilig inloggen. +

+ "; + + $html = FocizMailer::renderEmailTemplate([ + 'title' => 'Nieuwe inloglocatie gedetecteerd', + 'preheader' => "Inlogpoging vanaf nieuw IP {$ip} op Fociz", + 'badge' => 'LOCATIE BEVEILIGING', + 'content_html' => $contentHtml, + 'cta_text' => 'Locatie Bevestigen & Toestaan', + 'cta_url' => $verifyUrl, + 'cta_subtext' => '⏱️ Deze autorisatielink is 30 minuten geldig.', + 'security_notice' => 'Was u dit niet? Verander direct uw wachtwoord en neem contact op met uw systeembeheerder.' + ]); + + $res = FocizMailer::send($email, $name, "Bevestig nieuwe inloglocatie ({$ip}) — Fociz", $html); + return !empty($res['success']); + } catch (Throwable $e) { + error_log("fociz_send_new_ip_verification error: " . $e->getMessage()); + return false; + } +} + function start_secure_session(): void { if (session_status() === PHP_SESSION_NONE) { // Dagsessie: 24 uur (86400s) zodat server-garbage collectors de sessie niet tussentijds wissen @@ -209,13 +391,26 @@ function get_logged_in_user(): ?array { $db = get_db_connection(); ensure_remember_token_columns($db); - // 2. Controleer actieve PHP sessie (mits behorend bij vandaag) + $clientFingerprint = fociz_get_client_fingerprint(); + + // 2. Controleer actieve PHP sessie (mits behorend bij vandaag én hetzelfde IP/fingerprint) if (!empty($_SESSION['user_id']) && !empty($_SESSION['is_2fa_verified'])) { + // IP-verificatie: als het IP/locatie tussentijds verandert, moet er opnieuw worden ingelogd + if (!empty($_SESSION['auth_fingerprint']) && $_SESSION['auth_fingerprint'] !== $clientFingerprint) { + $_SESSION = ['session_was_expired' => true, 'ip_changed' => true]; + clear_day_session_cookie(); + if (session_status() === PHP_SESSION_ACTIVE) { + @session_regenerate_id(true); + } + return null; + } + $stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, avatar_url, theme, notification_settings, default_jira_filter_code, fociz_widget_layout, created_at FROM users WHERE id = ?"); $stmt->execute([$_SESSION['user_id']]); $user = $stmt->fetch(); if ($user) { $_SESSION['auth_date'] = $today; + $_SESSION['auth_fingerprint'] = $clientFingerprint; $_SESSION['session_expires_at'] = strtotime('today 23:59:59'); $_SESSION['last_activity'] = $now; return $user; @@ -233,6 +428,13 @@ function get_logged_in_user(): ?array { $stmt->execute([$dayToken, $today]); $user = $stmt->fetch(); if ($user) { + $currentIp = fociz_get_client_ip(); + // Als het token op een ander, onbekend IP wordt aangeboden, weiger de automatische herstel + if (!fociz_is_ip_known_for_user((int)$user['id'], $currentIp)) { + clear_day_session_cookie(); + return null; + } + // Herstel sessie voor vandaag $_SESSION['user_id'] = $user['id']; $_SESSION['user_name'] = $user['name']; @@ -240,6 +442,7 @@ function get_logged_in_user(): ?array { $_SESSION['user_role'] = $user['role']; $_SESSION['is_2fa_verified'] = true; $_SESSION['auth_date'] = $today; + $_SESSION['auth_fingerprint'] = $clientFingerprint; $_SESSION['session_expires_at'] = strtotime('today 23:59:59'); $_SESSION['last_activity'] = $now; return $user; @@ -287,8 +490,11 @@ function require_auth(bool $json = false): array { exit; } $wasExpired = !empty($_SESSION['session_was_expired']); - unset($_SESSION['session_was_expired']); - if ($wasExpired) { + $ipChanged = !empty($_SESSION['ip_changed']); + unset($_SESSION['session_was_expired'], $_SESSION['ip_changed']); + if ($ipChanged) { + header('Location: /auth/login.php?ip_changed=1'); + } elseif ($wasExpired) { header('Location: /auth/login.php?expired=1'); } else { header('Location: /auth/login.php'); diff --git a/includes/cache.php b/includes/cache.php index 003a620..b458f95 100644 --- a/includes/cache.php +++ b/includes/cache.php @@ -137,3 +137,24 @@ function fociz_cache_delete_pattern(string $prefix): bool { return false; } } + +/** + * Verwijdert alle verlopen items actief uit de cache tabel (AVG/GDPR opschoning) + */ +function fociz_cache_cleanup_expired(?PDO $db = null): int { + try { + $pdo = $db ?: get_db_connection(); + ensure_app_cache_table($pdo); + $stmt = $pdo->prepare("DELETE FROM app_cache WHERE expires_at < ?"); + $stmt->execute([time()]); + return $stmt->rowCount(); + } catch (Throwable $e) { + error_log("fociz_cache_cleanup_expired error: " . $e->getMessage()); + return 0; + } +} + +// Probabilistische automatische GC (5% kans bij elke script execution) +if (mt_rand(1, 20) === 1) { + fociz_cache_cleanup_expired(); +} diff --git a/includes/integrations/jira.php b/includes/integrations/jira.php index 1739bce..a71eaba 100644 --- a/includes/integrations/jira.php +++ b/includes/integrations/jira.php @@ -866,7 +866,8 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array $settings = json_decode($integration['settings'] ?? '{}', true) ?: []; $domain = !empty($settings['jira_domain']) ? $settings['jira_domain'] : jira_get_company_domain(); $email = $settings['jira_email'] ?? fociz_env('JIRA_EMAIL', ''); - $apiToken = $settings['jira_api_token'] ?? fociz_env('JIRA_API_TOKEN', ''); + $apiTokenRaw = $settings['jira_api_token'] ?? fociz_env('JIRA_API_TOKEN', ''); + $apiToken = fociz_decrypt($apiTokenRaw); $accName = $settings['account_name'] ?? 'Atlassian Gebruiker'; if (empty($domain) || empty($email) || empty($apiToken)) { diff --git a/includes/integrations/microsoft.php b/includes/integrations/microsoft.php index 1f83a8c..656fc03 100644 --- a/includes/integrations/microsoft.php +++ b/includes/integrations/microsoft.php @@ -218,23 +218,25 @@ function microsoft_get_valid_token(int $userId, bool $forceRefresh = false): ?st return null; } + $accessToken = fociz_decrypt($integration['access_token']); + $refreshToken = fociz_decrypt($integration['refresh_token'] ?? ''); + $settings = json_decode($integration['settings'] ?? '{}', true) ?: []; $expiresAt = (int)($settings['expires_at'] ?? 0); // Als token nog minstens 4 minuten geldig is én we niet forceren, direct retourneren if (!$forceRefresh && $expiresAt > (time() + 240)) { - return $integration['access_token']; + return $accessToken; } // Token is verlopen of forceRefresh gevraagd; refresh met refresh_token - $refreshToken = $integration['refresh_token'] ?? ''; $appConfig = microsoft_get_app_config(); $clientId = !empty($settings['client_id']) ? $settings['client_id'] : $appConfig['client_id']; $clientSecret = !empty($settings['client_secret']) ? $settings['client_secret'] : $appConfig['client_secret']; $tenantId = !empty($settings['tenant_id']) ? $settings['tenant_id'] : $appConfig['tenant_id']; if (empty($refreshToken) || empty($clientId) || empty($clientSecret)) { - return !$forceRefresh ? ($integration['access_token'] ?? null) : null; + return !$forceRefresh ? $accessToken : null; } $cleanTenant = (!empty($tenantId) && $tenantId !== 'common') ? $tenantId : 'planningsagenda.nl'; @@ -264,15 +266,20 @@ function microsoft_get_valid_token(int $userId, bool $forceRefresh = false): ?st SET access_token = ?, refresh_token = ?, settings = ?, last_sync_at = NOW() WHERE id = ? "); - $updateStmt->execute([$newAccessToken, $newRefreshToken, json_encode($settings), $integration['id']]); + $updateStmt->execute([ + fociz_encrypt($newAccessToken), + fociz_encrypt($newRefreshToken), + json_encode($settings), + $integration['id'] + ]); return $newAccessToken; } error_log("Microsoft token refresh mislukt voor user {$userId}: " . ($response['error'] ?? json_encode($response['json']))); // Fallback: indien het huidige token nog aanwezig is, probeer dit toch te gebruiken - if (!empty($integration['access_token'])) { - return $integration['access_token']; + if (!empty($accessToken)) { + return $accessToken; } return null; } diff --git a/planner.php b/planner.php index da7d95d..1b53817 100644 --- a/planner.php +++ b/planner.php @@ -1296,7 +1296,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
-
+
@@ -1315,7 +1315,7 @@ for ($h = $startHour; $h <= $endHour; $h++) { class="flex items-center space-x-1 text-[11px] font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 bg-indigo-50 dark:bg-indigo-500/10 hover:bg-indigo-100 dark:hover:bg-indigo-500/20 border border-indigo-200 dark:border-indigo-500/20 px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer" title="Plan in als dagtaak" > - + Plan
@@ -1713,6 +1713,146 @@ if ($isAjaxRequest) {
+ + + + + +