Merge branch 'dev' into main met behoud van live omgevingsconfiguratie en opschoning van overbodige bestanden

This commit is contained in:
2026-09-08 14:45:02 +02:00
22 changed files with 2051 additions and 340 deletions
+7 -4
View File
@@ -13,7 +13,6 @@ function start_secure_session(): void {
session_set_cookie_params([
'lifetime' => 7 * 24 * 60 * 60, // 7 dagen
'path' => '/',
'domain' => '',
'secure' => $isSecure,
'httponly' => true,
'samesite' => 'Lax'
@@ -26,9 +25,13 @@ function start_secure_session(): void {
* Controleert of er al een beheerder bestaat in MariaDB
*/
function has_admin_user(): bool {
$db = get_db_connection();
$stmt = $db->query("SELECT COUNT(*) FROM users WHERE role = 'ADMIN'");
return ((int)$stmt->fetchColumn()) > 0;
try {
$db = get_db_connection();
$stmt = $db->query("SELECT COUNT(*) FROM users WHERE role = 'ADMIN'");
return ((int)$stmt->fetchColumn()) > 0;
} catch (Throwable $e) {
return false;
}
}
/**
+8 -13
View File
@@ -9,23 +9,19 @@
* - OP DE DEV OMGEVING: $environment = 'development'; (Rood met 'Development' + radar ping)
* - OP DE MAIN OMGEVING: $environment = 'live'; (Groen met 'Live' + radar ping)
*
* U kunt de waarde hieronder aanpassen, of instellen via APP_ENV in .env.
* U kunt dit hieronder aanpassen, of instellen via APP_ENV in .env / .env.example!
*/
// Bepaal de omgeving: 'development' of 'live' (of 'main')
$environment = 'live'; // <-- ZET DIT OP DE MAIN SERVER OP 'live'
require_once __DIR__ . '/../config/config.php';
// Optionele fallback als het in .env gedefinieerd is
if (getenv('APP_ENV')) {
$envVal = strtolower(trim(getenv('APP_ENV')));
if (in_array($envVal, ['live', 'production', 'prod', 'main'])) {
$environment = 'live';
} elseif (in_array($envVal, ['dev', 'development', 'staging', 'test'])) {
$environment = 'development';
}
// Standaard waarde voor de main branch: 'live' (kan overschreven worden via APP_ENV)
$environment = defined('APP_ENV') ? APP_ENV : 'live';
if (function_exists('fociz_env') && fociz_env('APP_ENV')) {
$environment = fociz_env('APP_ENV');
}
$isLive = in_array(strtolower($environment), ['live', 'production', 'prod', 'main']);
$isLive = in_array(strtolower(trim($environment)), ['live', 'production', 'prod', 'main']);
?>
<!-- FLASHY OMGEVINGSINDICATOR LINKS BOVENIN -->
@@ -66,4 +62,3 @@ $isLive = in_array(strtolower($environment), ['live', 'production', 'prod', 'mai
</div>
<?php endif; ?>
</div>
+159
View File
@@ -0,0 +1,159 @@
<?php
/**
* Fociz Pure PHP HTTP Client Helper
* Ondersteunt cURL met automatische fallback naar stream wrappers (file_get_contents).
*/
if (!function_exists('fociz_http_request')) {
/**
* Voert een HTTP request uit en retourneert de statuscode, body en geparsede JSON.
*
* @param string $url
* @param array $options [
* 'method' => 'GET'|'POST'|'PUT'|'DELETE',
* 'headers' => ['Header-Name: Value'] of ['Header-Name' => 'Value'],
* 'body' => string|array,
* 'timeout' => int (seconden, default 12),
* 'verify_ssl' => bool (default true)
* ]
* @return array ['status' => int, 'body' => string, 'json' => ?array, 'error' => ?string]
*/
function fociz_http_request(string $url, array $options = []): array {
$method = strtoupper($options['method'] ?? 'GET');
$timeout = (int)($options['timeout'] ?? 12);
$verifySsl = $options['verify_ssl'] ?? true;
// Normaliseer headers naar 'Name: Value' regels
$rawHeaders = $options['headers'] ?? [];
$headerLines = [];
foreach ($rawHeaders as $k => $v) {
if (is_numeric($k)) {
$headerLines[] = $v;
} else {
$headerLines[] = "{$k}: {$v}";
}
}
// Bereid payload voor
$body = $options['body'] ?? null;
if (is_array($body)) {
$hasJsonHeader = false;
foreach ($headerLines as $h) {
if (stripos($h, 'application/json') !== false) {
$hasJsonHeader = true;
break;
}
}
if ($hasJsonHeader) {
$body = json_encode($body);
} else {
$body = http_build_query($body);
$headerLines[] = 'Content-Type: application/x-www-form-urlencoded';
}
}
// Voeg standaard User-Agent toe als die er niet is
$hasUserAgent = false;
foreach ($headerLines as $h) {
if (stripos($h, 'User-Agent:') !== false) {
$hasUserAgent = true;
break;
}
}
if (!$hasUserAgent) {
$headerLines[] = 'User-Agent: Fociz-Integrations/1.0';
}
// 1. Probeer cURL indien beschikbaar
if (extension_loaded('curl')) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 6);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headerLines);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifySsl);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifySsl ? 2 : 0);
if ($body !== null && in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$responseBody = curl_exec($ch);
$curlError = curl_error($ch);
$statusCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($responseBody === false && !empty($curlError)) {
return [
'status' => $statusCode ?: 0,
'body' => '',
'json' => null,
'error' => "cURL fout: {$curlError}"
];
}
$parsedJson = json_decode((string)$responseBody, true);
return [
'status' => $statusCode,
'body' => (string)$responseBody,
'json' => is_array($parsedJson) ? $parsedJson : null,
'error' => null
];
}
// 2. Fallback: PHP Streams (file_get_contents)
$contextOptions = [
'http' => [
'method' => $method,
'header' => implode("\r\n", $headerLines),
'timeout' => $timeout,
'ignore_errors' => true // Zorgt dat we ook bij 4xx / 5xx responses de body en statuscode kunnen lezen
],
'ssl' => [
'verify_peer' => $verifySsl,
'verify_peer_name' => $verifySsl
]
];
if ($body !== null && in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) {
$contextOptions['http']['content'] = $body;
}
$context = stream_context_create($contextOptions);
$responseBody = @file_get_contents($url, false, $context);
$statusCode = 0;
if (!empty($http_response_header)) {
// Zoek de laatste HTTP status regel (bij eventuele redirects)
foreach (array_reverse($http_response_header) as $hdr) {
if (preg_match('#HTTP/\d\.\d\s+(\d{3})#i', $hdr, $matches)) {
$statusCode = (int)$matches[1];
break;
}
}
}
if ($responseBody === false) {
$lastErr = error_get_last();
return [
'status' => $statusCode ?: 0,
'body' => '',
'json' => null,
'error' => 'Stream fout: ' . ($lastErr['message'] ?? 'Kan verbinding niet voltooien')
];
}
$parsedJson = json_decode((string)$responseBody, true);
return [
'status' => $statusCode,
'body' => (string)$responseBody,
'json' => is_array($parsedJson) ? $parsedJson : null,
'error' => null
];
}
}
+311
View File
@@ -0,0 +1,311 @@
<?php
/**
* Atlassian Jira Cloud REST API Integratie voor Fociz
* Beheert verbindingstest, ophalen van aan de gebruiker toegewezen issues en import naar dagtaken.
*/
require_once __DIR__ . '/../http.php';
require_once __DIR__ . '/../../config/database.php';
/**
* Normaliseert het Jira domein naar een schone URL zonder slash aan het einde
*/
function jira_normalize_domain(string $domain): string {
$domain = trim($domain);
if (!preg_match('#^https?://#i', $domain)) {
$domain = 'https://' . $domain;
}
return rtrim($domain, '/');
}
/**
* Genereert de Basic Authentication header voor Atlassian REST API
*/
function jira_get_auth_header(string $email, string $apiToken): string {
return 'Basic ' . base64_encode(trim($email) . ':' . trim($apiToken));
}
/**
* Test de verbinding met Atlassian Jira Cloud via /rest/api/3/myself
*/
function jira_test_connection(string $domain, string $email, string $apiToken): array {
$cleanDomain = jira_normalize_domain($domain);
if (empty($cleanDomain) || empty($email) || empty($apiToken)) {
return [
'success' => false,
'error' => 'Vul een Jira domein, e-mailadres en Atlassian API token in.',
];
}
$authHeader = jira_get_auth_header($email, $apiToken);
$url = $cleanDomain . '/rest/api/3/myself';
$response = fociz_http_request($url, [
'method' => 'GET',
'headers' => [
'Authorization' => $authHeader,
'Accept' => 'application/json',
],
'timeout' => 10,
]);
// Fallback naar api v2 indien v3 niet beschikbaar is
if ($response['status'] === 404) {
$urlV2 = $cleanDomain . '/rest/api/2/myself';
$response = fociz_http_request($urlV2, [
'method' => 'GET',
'headers' => [
'Authorization' => $authHeader,
'Accept' => 'application/json',
],
'timeout' => 10,
]);
}
if ($response['status'] === 200 && is_array($response['json'])) {
$user = $response['json'];
return [
'success' => true,
'user' => [
'displayName' => $user['displayName'] ?? 'Atlassian Gebruiker',
'emailAddress' => $user['emailAddress'] ?? $email,
'accountId' => $user['accountId'] ?? ($user['key'] ?? ''),
'active' => $user['active'] ?? true,
'timeZone' => $user['timeZone'] ?? 'Europe/Amsterdam',
],
];
}
$errMsg = 'Verbinding mislukt met Jira.';
if ($response['status'] === 401) {
$errMsg = 'Authenticatie geweigerd (HTTP 401). Controleer uw e-mailadres en zorg dat u een geldig Atlassian API token gebruikt.';
} elseif ($response['status'] === 403) {
$errMsg = 'Toegang geweigerd (HTTP 403). Uw account heeft onvoldoende rechten op dit Jira domein.';
} elseif ($response['status'] === 404) {
$errMsg = 'Jira Cloud domein niet gevonden (HTTP 404). Controleer het ingevoerde domein (bijv. uwbedrijf.atlassian.net).';
} elseif (!empty($response['error'])) {
$errMsg = "Netwerkfout: {$response['error']}";
}
return [
'success' => false,
'error' => $errMsg,
'status' => $response['status'],
];
}
/**
* Haalt alle actieve issues op die zijn toegewezen aan de ingelogde Jira gebruiker
*/
function jira_fetch_assigned_issues(string $domain, string $email, string $apiToken, int $maxResults = 50): array {
$cleanDomain = jira_normalize_domain($domain);
$authHeader = jira_get_auth_header($email, $apiToken);
// JQL: Alle openstaande taken toegewezen aan mij, gesorteerd op recent bijgewerkt
$jql = 'assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC';
$params = [
'jql' => $jql,
'maxResults' => min($maxResults, 100),
'fields' => 'summary,status,priority,issuetype,parent,project,duedate,updated',
];
$url = $cleanDomain . '/rest/api/3/search?' . http_build_query($params);
$response = fociz_http_request($url, [
'method' => 'GET',
'headers' => [
'Authorization' => $authHeader,
'Accept' => 'application/json',
],
'timeout' => 12,
]);
// Fallback naar api v2
if ($response['status'] === 404) {
$urlV2 = $cleanDomain . '/rest/api/2/search?' . http_build_query($params);
$response = fociz_http_request($urlV2, [
'method' => 'GET',
'headers' => [
'Authorization' => $authHeader,
'Accept' => 'application/json',
],
'timeout' => 12,
]);
}
if ($response['status'] !== 200 || !isset($response['json']['issues'])) {
return [
'success' => false,
'error' => $response['json']['errorMessages'][0] ?? ($response['error'] ?? 'Kan Jira tickets niet ophalen'),
'issues' => [],
];
}
$rawIssues = $response['json']['issues'];
$issues = [];
foreach ($rawIssues as $item) {
$fields = $item['fields'] ?? [];
$key = $item['key'] ?? '';
$summary = $fields['summary'] ?? '(Geen titel)';
$statusName = $fields['status']['name'] ?? 'To Do';
$priorityName = $fields['priority']['name'] ?? 'Medium';
$issueTypeName = $fields['issuetype']['name'] ?? 'Taak';
$projectName = $fields['project']['name'] ?? 'Project';
$dueDate = $fields['duedate'] ?? null;
$issues[] = [
'key' => $key,
'summary' => $summary,
'status' => $statusName,
'priority' => $priorityName,
'issue_type' => $issueTypeName,
'project' => $projectName,
'due_date' => $dueDate,
'url' => "{$cleanDomain}/browse/{$key}",
'source' => 'JIRA_LIVE',
];
}
return [
'success' => true,
'issues' => $issues,
'total' => count($issues),
];
}
/**
* Haalt de Jira tickets op voor een Fociz gebruiker
* Schakelt naadloos tussen live Jira tickets en sandbox demotickets
*/
function jira_get_user_issues(int $userId, bool $forceRefresh = false): array {
$db = get_db_connection();
$stmt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'JIRA' AND is_enabled = 1");
$stmt->execute([$userId]);
$integration = $stmt->fetch();
// Als de integratie niet is ingesteld of in demo mode staat, geef interactieve fallback issues terug
if (!$integration || !empty($integration['is_demo_mode'])) {
return [
'is_demo' => true,
'success' => true,
'issues' => [
[
'key' => 'FOC-101',
'summary' => 'Tweestapsverificatie (2FA TOTP) flow implementeren',
'status' => 'In Progress',
'priority' => 'High',
'issue_type' => 'Story',
'project' => 'Fociz Platform',
'due_date' => date('Y-m-d', strtotime('+2 days')),
'url' => '#',
'source' => 'SANDBOX',
],
[
'key' => 'FOC-102',
'summary' => 'Interactief whiteboard canvas voor brainstormsessies',
'status' => 'To Do',
'priority' => 'High',
'issue_type' => 'Feature',
'project' => 'Fociz Platform',
'due_date' => date('Y-m-d', strtotime('+4 days')),
'url' => '#',
'source' => 'SANDBOX',
],
[
'key' => 'FOC-103',
'summary' => 'Koppeling met Microsoft Outlook Calendar & Teams Graph API',
'status' => 'In Review',
'priority' => 'Medium',
'issue_type' => 'Task',
'project' => 'Integrations Hub',
'due_date' => date('Y-m-d'),
'url' => '#',
'source' => 'SANDBOX',
],
[
'key' => 'FOC-104',
'summary' => 'Roadmap kwartaalweergave en voortgangsindicatoren',
'status' => 'To Do',
'priority' => 'Medium',
'issue_type' => 'Story',
'project' => 'Roadmap Module',
'due_date' => date('Y-m-d', strtotime('+7 days')),
'url' => '#',
'source' => 'SANDBOX',
],
],
];
}
$settings = json_decode($integration['settings'] ?? '{}', true) ?: [];
$domain = $settings['jira_domain'] ?? fociz_env('JIRA_DOMAIN', '');
$email = $settings['jira_email'] ?? fociz_env('JIRA_EMAIL', '');
$apiToken = $settings['jira_api_token'] ?? fociz_env('JIRA_API_TOKEN', '');
if (empty($domain) || empty($email) || empty($apiToken)) {
return [
'is_demo' => false,
'success' => false,
'error' => 'Jira configuratie is onvolledig.',
'issues' => [],
];
}
// Sessiecache voor 90 seconden om onnodige Jira API calls bij snelle pagina refreshes te beperken
if (session_status() === PHP_SESSION_NONE) {
@session_start();
}
$cacheKey = "jira_issues_user_{$userId}";
$cached = $_SESSION[$cacheKey] ?? null;
if (!$forceRefresh && $cached && is_array($cached) && ($cached['timestamp'] ?? 0) > (time() - 90)) {
return [
'is_demo' => false,
'success' => true,
'issues' => $cached['issues'],
'from_cache' => true,
];
}
$result = jira_fetch_assigned_issues($domain, $email, $apiToken, 50);
if ($result['success']) {
$_SESSION[$cacheKey] = [
'timestamp' => time(),
'issues' => $result['issues'],
];
// Werk last_sync_at bij
$stmtUpdate = $db->prepare("UPDATE integrations SET last_sync_at = NOW() WHERE id = ?");
$stmtUpdate->execute([$integration['id']]);
return [
'is_demo' => false,
'success' => true,
'issues' => $result['issues'],
];
}
return [
'is_demo' => false,
'success' => false,
'error' => $result['error'],
'issues' => [],
];
}
/**
* Ontkoppelt Jira integratie voor een gebruiker
*/
function jira_disconnect(int $userId): bool {
$db = get_db_connection();
$stmt = $db->prepare("DELETE FROM integrations WHERE user_id = ? AND provider = 'JIRA'");
if (session_status() === PHP_SESSION_NONE) {
@session_start();
}
unset($_SESSION["jira_issues_user_{$userId}"]);
return $stmt->execute([$userId]);
}
+260
View File
@@ -0,0 +1,260 @@
<?php
/**
* Microsoft 365 / Graph API Integratie voor Fociz
* Beheert OAuth2 authenticatie, token refresh en kalendersynchronisatie.
*/
require_once __DIR__ . '/../http.php';
require_once __DIR__ . '/../../config/database.php';
const MS_OAUTH_AUTHORIZE_URL = 'https://login.microsoftonline.com/%s/oauth2/v2.0/authorize';
const MS_OAUTH_TOKEN_URL = 'https://login.microsoftonline.com/%s/oauth2/v2.0/token';
const MS_GRAPH_BASE_URL = 'https://graph.microsoft.com/v1.0';
const MS_DEFAULT_SCOPES = 'offline_access openid profile email User.Read Calendars.Read';
/**
* Genereert de Microsoft OAuth2 login URL
*/
function microsoft_get_auth_url(string $clientId, string $redirectUri, string $tenantId = 'common', string $state = ''): string {
$tenant = !empty($tenantId) ? trim($tenantId) : 'common';
$baseUrl = sprintf(MS_OAUTH_AUTHORIZE_URL, urlencode($tenant));
$params = [
'client_id' => trim($clientId),
'response_type' => 'code',
'redirect_uri' => trim($redirectUri),
'response_mode' => 'query',
'scope' => MS_DEFAULT_SCOPES,
'state' => $state,
'prompt' => 'select_account',
];
return $baseUrl . '?' . http_build_query($params);
}
/**
* Wisselt de ontvangen autorisatiecode om voor access- en refresh tokens
*/
function microsoft_exchange_code(string $code, string $clientId, string $clientSecret, string $redirectUri, string $tenantId = 'common'): array {
$tenant = !empty($tenantId) ? trim($tenantId) : 'common';
$tokenUrl = sprintf(MS_OAUTH_TOKEN_URL, urlencode($tenant));
$payload = [
'client_id' => trim($clientId),
'client_secret' => trim($clientSecret),
'code' => trim($code),
'redirect_uri' => trim($redirectUri),
'grant_type' => 'authorization_code',
'scope' => MS_DEFAULT_SCOPES,
];
$response = fociz_http_request($tokenUrl, [
'method' => 'POST',
'body' => $payload,
'timeout' => 15,
]);
if ($response['status'] !== 200 || empty($response['json']['access_token'])) {
$errorMsg = $response['json']['error_description'] ?? ($response['error'] ?? 'Onbekende fout bij token uitwisseling');
return [
'success' => false,
'error' => $errorMsg,
];
}
return [
'success' => true,
'access_token' => $response['json']['access_token'],
'refresh_token' => $response['json']['refresh_token'] ?? null,
'expires_in' => (int)($response['json']['expires_in'] ?? 3600),
];
}
/**
* Haalt het Microsoft gebruikersprofiel op via Graph API
*/
function microsoft_get_user_profile(string $accessToken): ?array {
$response = fociz_http_request(MS_GRAPH_BASE_URL . '/me', [
'method' => 'GET',
'headers' => [
'Authorization' => "Bearer {$accessToken}",
'Accept' => 'application/json',
],
'timeout' => 10,
]);
if ($response['status'] === 200 && is_array($response['json'])) {
return [
'displayName' => $response['json']['displayName'] ?? 'Microsoft 365 Gebruiker',
'mail' => $response['json']['mail'] ?? ($response['json']['userPrincipalName'] ?? ''),
'id' => $response['json']['id'] ?? '',
];
}
return null;
}
/**
* Haalt een geldige access_token op voor de gebruiker; vernieuwt automatisch indien verlopen
*/
function microsoft_get_valid_token(int $userId): ?string {
$db = get_db_connection();
$stmt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1");
$stmt->execute([$userId]);
$integration = $stmt->fetch();
if (!$integration || empty($integration['access_token'])) {
return null;
}
$settings = json_decode($integration['settings'] ?? '{}', true) ?: [];
$expiresAt = (int)($settings['expires_at'] ?? 0);
// Als token nog minstens 4 minuten geldig is, direct retourneren
if ($expiresAt > (time() + 240)) {
return $integration['access_token'];
}
// Token is verlopen of verloopt bijna; refresh met refresh_token
$refreshToken = $integration['refresh_token'] ?? '';
$clientId = $settings['client_id'] ?? fociz_env('MICROSOFT_CLIENT_ID', '');
$clientSecret = $settings['client_secret'] ?? fociz_env('MICROSOFT_CLIENT_SECRET', '');
$tenantId = $settings['tenant_id'] ?? fociz_env('MICROSOFT_TENANT_ID', 'common');
if (empty($refreshToken) || empty($clientId) || empty($clientSecret)) {
return null;
}
$tokenUrl = sprintf(MS_OAUTH_TOKEN_URL, urlencode(!empty($tenantId) ? $tenantId : 'common'));
$payload = [
'client_id' => $clientId,
'client_secret' => $clientSecret,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'scope' => MS_DEFAULT_SCOPES,
];
$response = fociz_http_request($tokenUrl, [
'method' => 'POST',
'body' => $payload,
'timeout' => 15,
]);
if ($response['status'] === 200 && !empty($response['json']['access_token'])) {
$newAccessToken = $response['json']['access_token'];
$newRefreshToken = $response['json']['refresh_token'] ?? $refreshToken;
$expiresIn = (int)($response['json']['expires_in'] ?? 3600);
$settings['expires_at'] = time() + $expiresIn;
$updateStmt = $db->prepare("
UPDATE integrations
SET access_token = ?, refresh_token = ?, settings = ?, last_sync_at = NOW()
WHERE id = ?
");
$updateStmt->execute([$newAccessToken, $newRefreshToken, json_encode($settings), $integration['id']]);
return $newAccessToken;
}
error_log("Microsoft token refresh mislukt voor user {$userId}: " . ($response['error'] ?? json_encode($response['json'])));
return null;
}
/**
* Haalt agenda-afspraken van Microsoft Outlook op voor de opgegeven datum
*/
function microsoft_fetch_today_events(int $userId, ?string $date = null): array {
$targetDate = $date ?: date('Y-m-d');
$token = microsoft_get_valid_token($userId);
if (!$token) {
return [];
}
// Venster van 00:00:00 tot 23:59:59 lokale tijd
$startIso = "{$targetDate}T00:00:00";
$endIso = "{$targetDate}T23:59:59";
$url = MS_GRAPH_BASE_URL . '/me/calendarView?' . http_build_query([
'startDateTime' => $startIso,
'endDateTime' => $endIso,
'$select' => 'id,subject,bodyPreview,start,end,location,isAllDay,webLink,onlineMeeting,isCancelled',
'$orderby' => 'start/dateTime',
'$top' => '50',
]);
$response = fociz_http_request($url, [
'method' => 'GET',
'headers' => [
'Authorization' => "Bearer {$token}",
'Accept' => 'application/json',
'Prefer' => 'outlook.timezone="Europe/Amsterdam"',
],
'timeout' => 12,
]);
if ($response['status'] !== 200 || empty($response['json']['value'])) {
return [];
}
$rawEvents = $response['json']['value'];
$events = [];
foreach ($rawEvents as $e) {
// Annuleringen overslaan
if (!empty($e['isCancelled'])) {
continue;
}
$rawStart = $e['start']['dateTime'] ?? '';
$rawEnd = $e['end']['dateTime'] ?? '';
// Formatteer naar H:i (bijv. 09:30)
$startTime = '';
$endTime = '';
if (!empty($rawStart)) {
$startTime = date('H:i', strtotime($rawStart));
}
if (!empty($rawEnd)) {
$endTime = date('H:i', strtotime($rawEnd));
}
// Teams of meeting URL detecteren
$meetingUrl = $e['onlineMeeting']['joinUrl'] ?? ($e['webLink'] ?? 'https://outlook.office.com');
if (empty($meetingUrl) && !empty($e['bodyPreview'])) {
if (preg_match('#https://teams\.microsoft\.com/l/meetup-join/[^\s"\'<>]+#i', $e['bodyPreview'], $m)) {
$meetingUrl = $m[0];
}
}
$locationName = $e['location']['displayName'] ?? '';
if (empty($locationName) && !empty($e['onlineMeeting']['joinUrl'])) {
$locationName = 'Microsoft Teams';
}
$events[] = [
'id' => $e['id'] ?? uniqid('ms_'),
'subject' => !empty($e['subject']) ? $e['subject'] : '(Geen onderwerp)',
'start' => $startTime ?: '09:00',
'end' => $endTime ?: '10:00',
'start_raw' => $rawStart,
'end_raw' => $rawEnd,
'location' => $locationName ?: 'Geen locatie',
'meeting_url' => $meetingUrl,
'is_all_day' => !empty($e['isAllDay']),
'source' => 'OUTLOOK_LIVE',
];
}
return $events;
}
/**
* Ontkoppelt Microsoft integratie voor een gebruiker
*/
function microsoft_disconnect(int $userId): bool {
$db = get_db_connection();
$stmt = $db->prepare("DELETE FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
return $stmt->execute([$userId]);
}
+1 -1
View File
@@ -76,7 +76,7 @@ class TOTP {
/**
* Verifieert een ingevoerde 6-cijferige 2FA code (met 1 tijdstap speling naar voor/achter)
*/
public static function verifyCode(string $secret, string $userInputCode, int $window = 1): boolean {
public static function verifyCode(string $secret, string $userInputCode, int $window = 1): bool {
$userInputCode = trim(str_replace(' ', '', $userInputCode));
if (strlen($userInputCode) !== 6 || !ctype_digit($userInputCode)) {
return false;