diff --git a/.env.example b/.env.example
index 475721d..9290c45 100644
--- a/.env.example
+++ b/.env.example
@@ -3,10 +3,24 @@ DB_HOST="127.0.0.1"
DB_PORT="3306"
DB_NAME="fociz"
DB_USER="root"
-DB_PASS="uw_wachtwoord"
+DB_PASS=""
+
+# Omgeving: 'development' (knippert rood) of 'live' (knippert groen)
+APP_ENV="development"
# Applicatie URL (voor uitnodigingslinks)
APP_URL="https://uw-domein.nl"
# Applicatie Geheime Sleutel (willekeurige tekenreeks)
APP_SECRET="fociz-maria-db-super-secret-key-998877"
+
+# Microsoft 365 / Outlook (Optionele globale defaults; kan ook via web UI worden ingesteld)
+MICROSOFT_CLIENT_ID=""
+MICROSOFT_CLIENT_SECRET=""
+MICROSOFT_TENANT_ID="common"
+
+# Atlassian Jira Cloud (Optionele globale defaults; kan ook via web UI worden ingesteld)
+JIRA_DOMAIN=""
+JIRA_EMAIL=""
+JIRA_API_TOKEN=""
+
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..519aebf
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,3 @@
+# Zorg dat omgevingsspecifieke bestanden nooit worden overschreven bij merges tussen dev en main
+config/config.php merge=ours
+includes/env_badge.php merge=ours
diff --git a/.htaccess b/.htaccess
deleted file mode 100644
index e736250..0000000
--- a/.htaccess
+++ /dev/null
@@ -1,16 +0,0 @@
-# Fociz Apache Configuratie
-DirectoryIndex index.php index.html
-
-# UTF-8 standaard
-AddDefaultCharset UTF-8
-
-# Blokkeer directe toegang tot SQL schema en configuratiebestanden
-
- Order allow,deny
- Deny from all
-
-
-# PHP foutmeldingen in productie (kan uitgeschakeld worden indien gewenst)
-php_flag display_errors Off
-php_value error_reporting E_ALL
-
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..e5450d2
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,10 @@
+FROM php:8.2-apache
+
+# Installeer MariaDB/MySQL drivers
+RUN docker-php-ext-install pdo pdo_mysql mysqli
+
+# Activeer Apache mod_rewrite voor schone URL's
+RUN a2enmod rewrite
+
+# Zorg dat Apache op poort 80 luistert
+EXPOSE 80
\ No newline at end of file
diff --git a/README.md b/README.md
index 6aec5b0..c2b8d66 100644
--- a/README.md
+++ b/README.md
@@ -85,9 +85,9 @@ APP_URL="https://uw-domein.nl"
```
### 3. Automatische Tabellen Aanmaak
-U hoeft **niet** handmatig SQL-commando's uit te voeren: zodra u de website voor het eerst opent in uw browser, detecteert Fociz automatisch MariaDB en worden alle tabellen en relaties uit `schema.sql` vanzelf aangemaakt!
+U hoeft **niet** handmatig SQL-commando's uit te voeren: zodra u de website voor het eerst opent in uw browser, detecteert Fociz automatisch MariaDB en worden alle tabellen en relaties uit `config/schema.sql` vanzelf aangemaakt!
-*(Optioneel kunt u `schema.sql` ook zelf importeren via phpMyAdmin of de MariaDB CLI).*
+*(Optioneel kunt u `config/schema.sql` ook zelf importeren via phpMyAdmin of de MariaDB CLI).*
### 4. Eerste Beheerder Instellen
1. Open uw browser en ga naar `https://uw-domein.nl`.
diff --git a/api/integrations.php b/api/integrations.php
new file mode 100644
index 0000000..57aa15e
--- /dev/null
+++ b/api/integrations.php
@@ -0,0 +1,114 @@
+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'] ?? '';
+ if (empty($domain)) $domain = $saved['jira_domain'] ?? '';
+ if (empty($email)) $email = $saved['jira_email'] ?? '';
+ }
+
+ $res = jira_test_connection($domain, $email, $apiToken);
+
+ if ($res['success'] && $save) {
+ $jiraUser = $res['user'];
+ $settings = json_encode([
+ 'jira_domain' => jira_normalize_domain($domain),
+ 'jira_email' => $email,
+ 'jira_api_token' => $apiToken,
+ 'account_name' => $jiraUser['displayName'] ?? '',
+ 'account_id' => $jiraUser['accountId'] ?? '',
+ ]);
+
+ $stmt = $db->prepare("
+ INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at)
+ VALUES (?, 'JIRA', 1, 0, ?, NOW())
+ ON DUPLICATE KEY UPDATE
+ is_enabled = 1,
+ is_demo_mode = 0,
+ settings = VALUES(settings),
+ last_sync_at = NOW()
+ ");
+ $stmt->execute([$user['id'], $settings]);
+
+ // Wis eventuele oude sessie cache
+ unset($_SESSION["jira_issues_user_{$user['id']}"]);
+ }
+
+ echo json_encode($res);
+ exit;
+ }
+
+ // 2. Jira Issues Ophalen (Toegewezen aan de gebruiker)
+ if ($action === 'get_jira_issues') {
+ $force = !empty($_GET['force']) || !empty($_POST['force']);
+ $res = jira_get_user_issues($user['id'], $force);
+ echo json_encode($res);
+ exit;
+ }
+
+ // 3. Outlook Agenda Afspraken Ophalen
+ if ($action === 'get_outlook_events') {
+ $date = $_GET['date'] ?? date('Y-m-d');
+ $events = microsoft_fetch_today_events($user['id'], $date);
+ echo json_encode([
+ 'success' => true,
+ 'date' => $date,
+ 'events' => $events,
+ 'count' => count($events),
+ ]);
+ exit;
+ }
+
+ // 4. Integratie Ontkoppelen
+ if ($action === 'disconnect') {
+ $input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
+ $provider = strtoupper(trim($input['provider'] ?? ''));
+
+ if ($provider === 'OUTLOOK') {
+ microsoft_disconnect($user['id']);
+ echo json_encode(['success' => true, 'message' => 'Microsoft Outlook ontkoppeld']);
+ exit;
+ }
+
+ if ($provider === 'JIRA') {
+ jira_disconnect($user['id']);
+ echo json_encode(['success' => true, 'message' => 'Atlassian Jira ontkoppeld']);
+ exit;
+ }
+
+ echo json_encode(['error' => 'Onbekende provider']);
+ exit;
+ }
+
+ echo json_encode(['error' => 'Onbekende actie']);
+} catch (Exception $e) {
+ http_response_code(500);
+ echo json_encode(['error' => $e->getMessage()]);
+}
+
diff --git a/api/planner.php b/api/planner.php
index 3e839cc..24a2108 100644
--- a/api/planner.php
+++ b/api/planner.php
@@ -53,11 +53,15 @@ try {
$stmtPlan->execute([$user['id'], $date]);
$planId = $db->lastInsertId();
+ $source = !empty($data['source']) ? substr(trim($data['source']), 0, 50) : 'FOCIZ';
+ $externalId = !empty($data['external_id']) ? substr(trim($data['external_id']), 0, 100) : null;
+ $externalUrl = !empty($data['external_url']) ? trim($data['external_url']) : null;
+
$stmt = $db->prepare("
- INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, status)
- VALUES (?, ?, ?, ?, ?, 'TODO')
+ INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, status, source, external_id, external_url)
+ VALUES (?, ?, ?, ?, ?, 'TODO', ?, ?, ?)
");
- $stmt->execute([$user['id'], $planId, $title, $priority, $timeBlockStart]);
+ $stmt->execute([$user['id'], $planId, $title, $priority, $timeBlockStart, $source, $externalId, $externalUrl]);
$taskId = (int)$db->lastInsertId();
echo json_encode([
diff --git a/auth/callback_microsoft.php b/auth/callback_microsoft.php
new file mode 100644
index 0000000..2e5ac98
--- /dev/null
+++ b/auth/callback_microsoft.php
@@ -0,0 +1,98 @@
+prepare("SELECT settings FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
+$stmt->execute([$user['id']]);
+$row = $stmt->fetch();
+$savedSettings = json_decode($row['settings'] ?? '{}', true) ?: [];
+
+$clientId = !empty($savedSettings['client_id']) ? $savedSettings['client_id'] : fociz_env('MICROSOFT_CLIENT_ID', '');
+$clientSecret = !empty($savedSettings['client_secret']) ? $savedSettings['client_secret'] : fociz_env('MICROSOFT_CLIENT_SECRET', '');
+$tenantId = !empty($savedSettings['tenant_id']) ? $savedSettings['tenant_id'] : fociz_env('MICROSOFT_TENANT_ID', 'common');
+
+if (empty($clientId) || empty($clientSecret)) {
+ header('Location: /integrations.php?error=' . urlencode('Client ID of Client Secret ontbreekt in Fociz. Vul deze eerst in bij de Outlook instellingen.'));
+ exit;
+}
+
+$redirectUri = rtrim(APP_URL, '/') . '/auth/callback_microsoft.php';
+
+// 5. Code omwisselen voor access & refresh tokens
+$tokenResult = microsoft_exchange_code($code, $clientId, $clientSecret, $redirectUri, $tenantId);
+
+if (!$tokenResult['success']) {
+ header('Location: /integrations.php?error=' . urlencode('Token uitwisseling mislukt: ' . $tokenResult['error']));
+ exit;
+}
+
+$accessToken = $tokenResult['access_token'];
+$refreshToken = $tokenResult['refresh_token'];
+$expiresIn = $tokenResult['expires_in'];
+$expiresAt = time() + $expiresIn;
+
+// 6. Microsoft gebruikersprofiel ophalen
+$profile = microsoft_get_user_profile($accessToken);
+$accountEmail = $profile['mail'] ?? ($profile['userPrincipalName'] ?? $user['email']);
+$accountName = $profile['displayName'] ?? 'Microsoft 365 Account';
+
+// 7. Opslaan in MariaDB
+$newSettings = array_merge($savedSettings, [
+ 'client_id' => $clientId,
+ 'client_secret' => $clientSecret,
+ 'tenant_id' => $tenantId,
+ 'expires_at' => $expiresAt,
+ 'account_email' => $accountEmail,
+ 'account_name' => $accountName,
+]);
+
+$saveStmt = $db->prepare("
+ INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, access_token, refresh_token, settings, last_sync_at)
+ VALUES (?, 'OUTLOOK', 1, 0, ?, ?, ?, NOW())
+ ON DUPLICATE KEY UPDATE
+ is_enabled = 1,
+ is_demo_mode = 0,
+ access_token = VALUES(access_token),
+ refresh_token = VALUES(refresh_token),
+ settings = VALUES(settings),
+ last_sync_at = NOW()
+");
+$saveStmt->execute([$user['id'], $accessToken, $refreshToken, json_encode($newSettings)]);
+
+// 8. Succesvol terugkeren naar de Integraties Hub
+header('Location: /integrations.php?success=' . urlencode("Microsoft Outlook succesvol gekoppeld als {$accountEmail}!"));
+exit;
+
diff --git a/auth/oauth_microsoft.php b/auth/oauth_microsoft.php
new file mode 100644
index 0000000..88a008d
--- /dev/null
+++ b/auth/oauth_microsoft.php
@@ -0,0 +1,43 @@
+prepare("SELECT settings FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
+$stmt->execute([$user['id']]);
+$row = $stmt->fetch();
+$savedSettings = json_decode($row['settings'] ?? '{}', true) ?: [];
+
+$clientId = !empty($_GET['client_id'])
+ ? trim($_GET['client_id'])
+ : (!empty($savedSettings['client_id']) ? $savedSettings['client_id'] : fociz_env('MICROSOFT_CLIENT_ID', ''));
+
+$tenantId = !empty($_GET['tenant_id'])
+ ? trim($_GET['tenant_id'])
+ : (!empty($savedSettings['tenant_id']) ? $savedSettings['tenant_id'] : fociz_env('MICROSOFT_TENANT_ID', 'common'));
+
+if (empty($clientId)) {
+ header('Location: /integrations.php?error=missing_ms_client_id');
+ exit;
+}
+
+// Bepaal de dynamische of geconfigureerde redirect URI
+$redirectUri = rtrim(APP_URL, '/') . '/auth/callback_microsoft.php';
+
+// Genereer unieke CSRF state
+$state = bin2hex(random_bytes(16));
+$_SESSION['ms_oauth_state'] = $state;
+$_SESSION['ms_oauth_user_id'] = $user['id'];
+
+// Stuur gebruiker door naar Microsoft login
+$authUrl = microsoft_get_auth_url($clientId, $redirectUri, $tenantId, $state);
+header("Location: {$authUrl}");
+exit;
+
diff --git a/auth/setup.php b/auth/setup.php
index a5b1ecd..47637f5 100644
--- a/auth/setup.php
+++ b/auth/setup.php
@@ -15,6 +15,7 @@ if (has_admin_user()) {
$error = null;
$success = false;
$backupCodes = [];
+$email = '';
// Haal of genereer een tijdelijke sessie-sleutel voor de 2FA configuratie
if (empty($_SESSION['setup_totp_secret'])) {
@@ -148,7 +149,7 @@ $pageTitle = "Fociz — Eerste Initialisatie";
-
+
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..92284f3
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,9 @@
+{
+ "require": {
+ "php": "^8.2",
+ "ext-pdo": "*",
+ "ext-pdo_mysql": "*",
+ "ext-mysqli": "*"
+ }
+}
+
diff --git a/config/config.php b/config/config.php
index fc7cf97..0ae6173 100644
--- a/config/config.php
+++ b/config/config.php
@@ -1,40 +1,93 @@
PDO::ERRMODE_EXCEPTION,
- PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
- PDO::ATTR_EMULATE_PREPARES => false,
- ];
-
- try {
- $pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
- // Zorg ervoor dat tabellen bestaan
- ensure_tables_exist($pdo);
- } catch (PDOException $e) {
- // Als de database zelf nog niet bestaat, probeer deze aan te maken indien gebruiker rechten heeft
- if ($e->getCode() == 1049) {
- try {
- $tempDsn = sprintf('mysql:host=%s;port=%s;charset=%s', DB_HOST, DB_PORT, DB_CHARSET);
- $tempPdo = new PDO($tempDsn, DB_USER, DB_PASS, $options);
- $tempPdo->exec(sprintf('CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci', DB_NAME));
- $pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
- ensure_tables_exist($pdo);
- return $pdo;
- } catch (PDOException $ex) {
- die("Database verbindingsfout: Kan database '" . htmlspecialchars(DB_NAME) . "' niet aanmaken of vinden. Maak deze handmatig aan in MariaDB.");
- }
- }
- die("MariaDB Verbindingsfout: " . htmlspecialchars($e->getMessage()) . " Controleer uw instellingen in config/config.php of .env");
- }
+ if ($pdo !== null) {
+ return $pdo;
}
- return $pdo;
+ // 1. Controleer of de PDO MySQL/MariaDB driver aanwezig is in PHP
+ $availableDrivers = PDO::getAvailableDrivers();
+ if (!in_array('mysql', $availableDrivers)) {
+ $driversList = !empty($availableDrivers) ? implode(', ', $availableDrivers) : 'geen';
+ die("
+
+
+
⚠️ PHP MySQL Driver Ontbreekt (pdo_mysql)
+
De extensie pdo_mysql is niet actief in deze PHP-omgeving.
+
+
+ ");
+ }
+
+ $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?
+ // We proberen eerst direct verbinding te maken met de database.
+ // Als de database al bestaat, lukt dit direct zonder database-aanmaakrechten te vereisen!
+ try {
+ $pdo = new PDO($dsnWithDb, DB_USER, DB_PASS, $options);
+
+ // De database bestaat al! Controleer enkel of de tabellen al aanwezig zijn
+ ensure_tables_exist($pdo);
+ return $pdo;
+
+ } catch (PDOException $e) {
+ // Foutcode 1049 betekent: Database 'DB_NAME' bestaat nog NIET op de MariaDB server
+ if ($e->getCode() == 1049 || str_contains($e->getMessage(), 'Unknown database')) {
+ // Probeer de database eenmalig aan te maken indien de gebruiker CREATE rechten heeft
+ 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
+ ));
+
+ // Nu opnieuw verbinden met de nieuw aangemaakte database
+ $pdo = new PDO($dsnWithDb, DB_USER, DB_PASS, $options);
+ ensure_tables_exist($pdo);
+ return $pdo;
+
+ } catch (PDOException $createEx) {
+ die("
+
+
+
⚠️ Database '" . htmlspecialchars(DB_NAME) . "' bestaat nog niet
+
+ De database " . htmlspecialchars(DB_NAME) . " bestaat nog niet op MariaDB server " . htmlspecialchars(DB_HOST) . " en de gebruiker " . htmlspecialchars(DB_USER) . " heeft geen rechten om automatisch databases aan te maken.
+
+
+ Maak de database " . htmlspecialchars(DB_NAME) . " eenmalig aan via phpMyAdmin, HeidiSQL of de MariaDB CLI:
+
+
+ CREATE DATABASE IF NOT EXISTS `" . htmlspecialchars(DB_NAME) . "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+
- Nog geen taken gepland voor vandaag. Klik op 'Nieuwe Taak' om te beginnen.
+ Nog geen taken gepland voor vandaag. Klik op 'Nieuwe Taak' of importeer een Jira ticket hieronder.