Merge pull request 'Dev' (#7) from dev into main
Reviewed-on: #7
This commit was merged in pull request #7.
This commit is contained in:
+1
-1
@@ -8,5 +8,5 @@ Thumbs.db
|
|||||||
*.log
|
*.log
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
config/config.php
|
|
||||||
config/.env.example
|
config/.env.example
|
||||||
|
config/config.php
|
||||||
@@ -7,22 +7,54 @@
|
|||||||
require_once __DIR__ . '/../includes/auth.php';
|
require_once __DIR__ . '/../includes/auth.php';
|
||||||
require_once __DIR__ . '/../includes/integrations/microsoft.php';
|
require_once __DIR__ . '/../includes/integrations/microsoft.php';
|
||||||
|
|
||||||
$user = require_auth();
|
start_secure_session();
|
||||||
|
|
||||||
|
// 1. CSRF State validatie & Sessieherstel
|
||||||
|
$receivedState = $_GET['state'] ?? '';
|
||||||
|
$stateUserId = !empty($receivedState) ? verify_oauth_state($receivedState) : null;
|
||||||
|
|
||||||
|
// Als de sessie verloren is gegaan tijdens de externe cross-site redirect vanaf Microsoft,
|
||||||
|
// herstellen we de gebruiker veilig aan de hand van de cryptografisch ondertekende OAuth state!
|
||||||
|
if (empty($_SESSION['user_id']) && $stateUserId) {
|
||||||
|
try {
|
||||||
|
$db = get_db_connection();
|
||||||
|
$stmtUser = $db->prepare("SELECT id, name, email, role, two_factor_enabled, created_at FROM users WHERE id = ?");
|
||||||
|
$stmtUser->execute([$stateUserId]);
|
||||||
|
$recoveredUser = $stmtUser->fetch();
|
||||||
|
if ($recoveredUser) {
|
||||||
|
$_SESSION['user_id'] = $recoveredUser['id'];
|
||||||
|
$_SESSION['user_name'] = $recoveredUser['name'];
|
||||||
|
$_SESSION['user_email'] = $recoveredUser['email'];
|
||||||
|
$_SESSION['user_role'] = $recoveredUser['role'];
|
||||||
|
$_SESSION['is_2fa_verified'] = true;
|
||||||
|
issue_remember_token((int)$recoveredUser['id']);
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// Foutafhandeling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Controleer authenticatie
|
||||||
|
$user = get_logged_in_user();
|
||||||
|
if (!$user) {
|
||||||
|
header('Location: /auth/login.php?error=' . urlencode('Sessie verlopen tijdens Microsoft koppeling. Log opnieuw in en probeer het nogmaals.'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$db = get_db_connection();
|
$db = get_db_connection();
|
||||||
|
|
||||||
// 1. Foutafhandeling vanuit Microsoft
|
// 3. Foutafhandeling vanuit Microsoft (bijv. gebruiker weigerde machtiging)
|
||||||
if (!empty($_GET['error'])) {
|
if (!empty($_GET['error'])) {
|
||||||
$errorDescription = $_GET['error_description'] ?? $_GET['error'];
|
$errorDescription = $_GET['error_description'] ?? $_GET['error'];
|
||||||
header('Location: /integrations.php?error=' . urlencode("Microsoft melding: {$errorDescription}"));
|
header('Location: /integrations.php?error=' . urlencode("Microsoft melding: {$errorDescription}"));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. CSRF State validatie
|
// 4. Valideer de state (via HMAC handtekening of fallback sessiestate)
|
||||||
$receivedState = $_GET['state'] ?? '';
|
|
||||||
$expectedState = $_SESSION['ms_oauth_state'] ?? '';
|
$expectedState = $_SESSION['ms_oauth_state'] ?? '';
|
||||||
unset($_SESSION['ms_oauth_state']);
|
unset($_SESSION['ms_oauth_state'], $_SESSION['ms_oauth_user_id']);
|
||||||
|
|
||||||
if (empty($receivedState) || !hash_equals($expectedState, $receivedState)) {
|
if (!$stateUserId && (empty($receivedState) || empty($expectedState) || !hash_equals($expectedState, $receivedState))) {
|
||||||
header('Location: /integrations.php?error=' . urlencode('Ongeldige sessie of CSRF-controle mislukt. Probeer opnieuw in te loggen via Microsoft.'));
|
header('Location: /integrations.php?error=' . urlencode('Ongeldige sessie of CSRF-controle mislukt. Probeer opnieuw in te loggen via Microsoft.'));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$_SESSION['user_email'] = $invite['email'];
|
$_SESSION['user_email'] = $invite['email'];
|
||||||
$_SESSION['user_role'] = $invite['role'];
|
$_SESSION['user_role'] = $invite['role'];
|
||||||
$_SESSION['is_2fa_verified'] = true;
|
$_SESSION['is_2fa_verified'] = true;
|
||||||
|
issue_remember_token($newUserId);
|
||||||
unset($_SESSION['invite_totp_secret']);
|
unset($_SESSION['invite_totp_secret']);
|
||||||
|
|
||||||
header('Location: /planner.php');
|
header('Location: /planner.php');
|
||||||
|
|||||||
+6
-2
@@ -12,8 +12,9 @@ if (!has_admin_user()) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Als al volledig ingelogd en 2FA geverifieerd, ga naar planner
|
// Als al ingelogd (via actieve sessie of persistente cookie), ga direct naar planner
|
||||||
if (!empty($_SESSION['user_id']) && !empty($_SESSION['is_2fa_verified'])) {
|
$currentUser = get_logged_in_user();
|
||||||
|
if ($currentUser) {
|
||||||
header('Location: /planner.php');
|
header('Location: /planner.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -96,6 +97,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$_SESSION['user_role'] = $user['role'];
|
$_SESSION['user_role'] = $user['role'];
|
||||||
$_SESSION['is_2fa_verified'] = true;
|
$_SESSION['is_2fa_verified'] = true;
|
||||||
|
|
||||||
|
// Activeer persistente remember token (30 dagen blijf ingelogd)
|
||||||
|
issue_remember_token((int)$user['id']);
|
||||||
|
|
||||||
unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_user_email'], $_SESSION['pending_2fa_user_name']);
|
unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_user_email'], $_SESSION['pending_2fa_user_name']);
|
||||||
|
|
||||||
header('Location: /planner.php');
|
header('Location: /planner.php');
|
||||||
|
|||||||
@@ -6,6 +6,21 @@
|
|||||||
require_once __DIR__ . '/../includes/auth.php';
|
require_once __DIR__ . '/../includes/auth.php';
|
||||||
start_secure_session();
|
start_secure_session();
|
||||||
|
|
||||||
|
// Vernietig persistente login in database
|
||||||
|
if (!empty($_SESSION['user_id'])) {
|
||||||
|
try {
|
||||||
|
$db = get_db_connection();
|
||||||
|
ensure_remember_token_columns($db);
|
||||||
|
$stmt = $db->prepare("UPDATE users SET remember_token = NULL, remember_token_expires_at = NULL WHERE id = ?");
|
||||||
|
$stmt->execute([$_SESSION['user_id']]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// Stil negeren bij uitloggen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verwijder remember cookie
|
||||||
|
clear_remember_cookie();
|
||||||
|
|
||||||
// Vernietig alle sessie variabelen
|
// Vernietig alle sessie variabelen
|
||||||
$_SESSION = [];
|
$_SESSION = [];
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ if (empty($clientId)) {
|
|||||||
// Bepaal de dynamische of geconfigureerde redirect URI
|
// Bepaal de dynamische of geconfigureerde redirect URI
|
||||||
$redirectUri = rtrim(APP_URL, '/') . '/auth/callback_microsoft.php';
|
$redirectUri = rtrim(APP_URL, '/') . '/auth/callback_microsoft.php';
|
||||||
|
|
||||||
// Genereer unieke CSRF state
|
// Genereer cryptografisch ondertekende OAuth state waarin de user_id en timestamp veilig zijn opgeslagen
|
||||||
$state = bin2hex(random_bytes(16));
|
$state = generate_oauth_state((int)$user['id']);
|
||||||
$_SESSION['ms_oauth_state'] = $state;
|
$_SESSION['ms_oauth_state'] = $state;
|
||||||
$_SESSION['ms_oauth_user_id'] = $user['id'];
|
$_SESSION['ms_oauth_user_id'] = (int)$user['id'];
|
||||||
|
|
||||||
// Stuur gebruiker door naar Microsoft login
|
// Stuur gebruiker door naar Microsoft login
|
||||||
$authUrl = microsoft_get_auth_url($clientId, $redirectUri, $tenantId, $state);
|
$authUrl = microsoft_get_auth_url($clientId, $redirectUri, $tenantId, $state);
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$_SESSION['user_email'] = $email;
|
$_SESSION['user_email'] = $email;
|
||||||
$_SESSION['user_role'] = 'ADMIN';
|
$_SESSION['user_role'] = 'ADMIN';
|
||||||
$_SESSION['is_2fa_verified'] = true;
|
$_SESSION['is_2fa_verified'] = true;
|
||||||
|
issue_remember_token($adminId);
|
||||||
unset($_SESSION['setup_totp_secret']);
|
unset($_SESSION['setup_totp_secret']);
|
||||||
|
|
||||||
$success = true;
|
$success = true;
|
||||||
|
|||||||
+3
-1
@@ -3,7 +3,8 @@
|
|||||||
* Fociz Brainstorm Sessies Overzicht
|
* Fociz Brainstorm Sessies Overzicht
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/includes/header.php';
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
$currentUser = require_auth();
|
||||||
$db = get_db_connection();
|
$db = get_db_connection();
|
||||||
|
|
||||||
// Verwerk nieuwe sessie aanmaken
|
// Verwerk nieuwe sessie aanmaken
|
||||||
@@ -36,6 +37,7 @@ $stmtSessions = $db->query("
|
|||||||
ORDER BY s.created_at DESC
|
ORDER BY s.created_at DESC
|
||||||
");
|
");
|
||||||
$sessions = $stmtSessions->fetchAll();
|
$sessions = $stmtSessions->fetchAll();
|
||||||
|
require_once __DIR__ . '/includes/header.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
|||||||
+7
-6
@@ -1,15 +1,15 @@
|
|||||||
# MariaDB Verbinding
|
# MariaDB Verbinding
|
||||||
DB_HOST="192.168.2.33"
|
DB_HOST="192.168.2.33"
|
||||||
DB_PORT="3306"
|
DB_PORT="3306"
|
||||||
DB_NAME="fociz"
|
DB_NAME="focizdev"
|
||||||
DB_USER="focizlive_user"
|
DB_USER="focizdev_user"
|
||||||
DB_PASS="F0c1z_Live_Secure_2026!"
|
DB_PASS="F0c1z_Dev_Secure_2026!"
|
||||||
|
|
||||||
# Omgeving: 'development' (knippert rood) of 'live' (knippert groen)
|
# Omgeving: 'development' (knippert rood) of 'live' (knippert groen)
|
||||||
APP_ENV="live"
|
APP_ENV="development"
|
||||||
|
|
||||||
# Applicatie URL (voor uitnodigingslinks en OAuth redirects: https://fociz.nl)
|
# Applicatie URL (voor uitnodigingslinks en OAuth redirects: https://dev.fociz.nl of https://fociz.nl)
|
||||||
APP_URL="https://fociz.nl"
|
APP_URL="https://dev.fociz.nl"
|
||||||
|
|
||||||
# Applicatie Geheime Sleutel (willekeurige tekenreeks)
|
# Applicatie Geheime Sleutel (willekeurige tekenreeks)
|
||||||
APP_SECRET="fociz-maria-db-super-secret-key-998877"
|
APP_SECRET="fociz-maria-db-super-secret-key-998877"
|
||||||
@@ -23,3 +23,4 @@ MICROSOFT_TENANT_ID="common"
|
|||||||
JIRA_DOMAIN=""
|
JIRA_DOMAIN=""
|
||||||
JIRA_EMAIL=""
|
JIRA_EMAIL=""
|
||||||
JIRA_API_TOKEN=""
|
JIRA_API_TOKEN=""
|
||||||
|
|
||||||
|
|||||||
+9
-13
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Fociz Applicatie- en Databaseconfiguratie (MAIN / LIVE BRANCH)
|
* Fociz Applicatie- en Databaseconfiguratie
|
||||||
* Laadt automatisch .env (of .env.example indien .env niet bestaat)
|
* Laadt automatisch .env (of .env.example indien .env niet bestaat)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -65,12 +65,12 @@ if (!function_exists('fociz_env')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MariaDB Verbindingsgegevens (Live defaults voor main branch; overschrijfbaar via server .env)
|
// MariaDB Verbindingsgegevens (uit .env of .env.example)
|
||||||
define('DB_HOST', fociz_env('DB_HOST', '192.168.2.33'));
|
define('DB_HOST', fociz_env('DB_HOST', '192.168.2.33'));
|
||||||
define('DB_PORT', fociz_env('DB_PORT', '3306'));
|
define('DB_PORT', fociz_env('DB_PORT', '3306'));
|
||||||
define('DB_NAME', fociz_env('DB_NAME', 'fociz'));
|
define('DB_NAME', fociz_env('DB_NAME', 'focizdev'));
|
||||||
define('DB_USER', fociz_env('DB_USER', 'focizlive_user'));
|
define('DB_USER', fociz_env('DB_USER', 'focizdev_user'));
|
||||||
define('DB_PASS', fociz_env('DB_PASS', 'F0c1z_Live_Secure_2026!'));
|
define('DB_PASS', fociz_env('DB_PASS', 'F0c1z_Dev_Secure_2026!'));
|
||||||
define('DB_CHARSET', 'utf8mb4');
|
define('DB_CHARSET', 'utf8mb4');
|
||||||
|
|
||||||
// Bepaal de dynamische of geconfigureerde applicatie URL
|
// Bepaal de dynamische of geconfigureerde applicatie URL
|
||||||
@@ -81,7 +81,7 @@ if (!function_exists('fociz_detect_app_url')) {
|
|||||||
return rtrim($envUrl, '/');
|
return rtrim($envUrl, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamische detectie op basis van het huidige domein (werkt direct op fociz.nl én dev.fociz.nl)
|
// Dynamische detectie op basis van het huidige domein (werkt direct op dev.fociz.nl én fociz.nl)
|
||||||
if (!empty($_SERVER['HTTP_HOST'])) {
|
if (!empty($_SERVER['HTTP_HOST'])) {
|
||||||
$isHttps = (
|
$isHttps = (
|
||||||
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
|
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
|
||||||
@@ -98,22 +98,18 @@ if (!function_exists('fociz_detect_app_url')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Applicatie instellingen (Live defaults)
|
// Applicatie instellingen
|
||||||
define('APP_NAME', 'Fociz');
|
define('APP_NAME', 'Fociz');
|
||||||
define('APP_ENV', fociz_env('APP_ENV', 'live')); // 'live' voor de main branch
|
define('APP_ENV', fociz_env('APP_ENV', 'development')); // 'development' of 'live'
|
||||||
define('APP_URL', fociz_detect_app_url());
|
define('APP_URL', fociz_detect_app_url());
|
||||||
define('APP_SECRET', fociz_env('APP_SECRET', 'fociz-maria-db-super-secret-key-998877'));
|
define('APP_SECRET', fociz_env('APP_SECRET', 'fociz-maria-db-super-secret-key-998877'));
|
||||||
|
|
||||||
// Tijdzone
|
// Tijdzone
|
||||||
date_default_timezone_set('Europe/Amsterdam');
|
date_default_timezone_set('Europe/Amsterdam');
|
||||||
|
|
||||||
// Foutrapportage: in live verbergen we fouten voor bezoekers
|
// Foutrapportage: in development tonen we fouten direct op het scherm
|
||||||
if (APP_ENV !== 'live') {
|
if (APP_ENV !== 'live') {
|
||||||
ini_set('display_errors', '1');
|
ini_set('display_errors', '1');
|
||||||
ini_set('display_startup_errors', '1');
|
ini_set('display_startup_errors', '1');
|
||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
} else {
|
|
||||||
ini_set('display_errors', '0');
|
|
||||||
ini_set('display_startup_errors', '0');
|
|
||||||
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ CREATE TABLE IF NOT EXISTS `users` (
|
|||||||
`two_factor_secret` VARCHAR(120) NULL,
|
`two_factor_secret` VARCHAR(120) NULL,
|
||||||
`two_factor_enabled` TINYINT(1) NOT NULL DEFAULT 0,
|
`two_factor_enabled` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
`backup_codes` TEXT NULL,
|
`backup_codes` TEXT NULL,
|
||||||
|
`remember_token` VARCHAR(64) NULL,
|
||||||
|
`remember_token_expires_at` DATETIME NULL,
|
||||||
`avatar_url` VARCHAR(255) NULL,
|
`avatar_url` VARCHAR(255) NULL,
|
||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
|||||||
+202
-14
@@ -6,14 +6,42 @@
|
|||||||
require_once __DIR__ . '/../config/database.php';
|
require_once __DIR__ . '/../config/database.php';
|
||||||
require_once __DIR__ . '/totp.php';
|
require_once __DIR__ . '/totp.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bepaalt of de huidige verbinding HTTPS gebruikt (inclusief reverse proxies / OpenResty / Docker)
|
||||||
|
*/
|
||||||
|
function is_https_request(): bool {
|
||||||
|
if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && strtolower($_SERVER['HTTP_X_FORWARDED_SSL']) === 'on') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isset($_SERVER['SERVER_PORT']) && (int)$_SERVER['SERVER_PORT'] === 443) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (defined('APP_URL') && str_starts_with(APP_URL, 'https://')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function start_secure_session(): void {
|
function start_secure_session(): void {
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
// Veilige sessiecookie parameters
|
// Zorg voor een ruime sessieduur (30 dagen)
|
||||||
$isSecure = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on');
|
ini_set('session.gc_maxlifetime', '2592000');
|
||||||
|
ini_set('session.cookie_lifetime', '2592000');
|
||||||
|
ini_set('session.use_strict_mode', '1');
|
||||||
|
ini_set('session.use_only_cookies', '1');
|
||||||
|
|
||||||
|
$isSecure = is_https_request();
|
||||||
session_set_cookie_params([
|
session_set_cookie_params([
|
||||||
'lifetime' => 7 * 24 * 60 * 60, // 7 dagen
|
'lifetime' => 30 * 24 * 60 * 60, // 30 dagen
|
||||||
'path' => '/',
|
'path' => '/',
|
||||||
'secure' => $isSecure,
|
'domain' => '',
|
||||||
|
'secure' => $isSecure,
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
'samesite' => 'Lax'
|
'samesite' => 'Lax'
|
||||||
]);
|
]);
|
||||||
@@ -35,31 +63,136 @@ function has_admin_user(): bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Haalt de huidige ingelogde gebruiker op uit de database
|
* Garandeert dat de kolommen voor persistent sessiebeheer aanwezig zijn in MariaDB
|
||||||
|
*/
|
||||||
|
function ensure_remember_token_columns(PDO $db): void {
|
||||||
|
static $columnsChecked = false;
|
||||||
|
if ($columnsChecked) return;
|
||||||
|
$columnsChecked = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $db->query("SHOW COLUMNS FROM `users` LIKE 'remember_token'");
|
||||||
|
if (!$result || $result->rowCount() === 0) {
|
||||||
|
$db->exec("ALTER TABLE `users` ADD COLUMN `remember_token` VARCHAR(64) NULL AFTER `backup_codes`, ADD COLUMN `remember_token_expires_at` DATETIME NULL AFTER `remember_token`");
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// Stil negeren indien al aanwezig of tijdens migraties
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Genereert en bewaart een persistente 'Onthoud mij' token in MariaDB en plaatst een veilige cookie
|
||||||
|
*/
|
||||||
|
function issue_remember_token(int $userId): string {
|
||||||
|
$rawToken = bin2hex(random_bytes(32));
|
||||||
|
$tokenHash = hash('sha256', $rawToken);
|
||||||
|
$expiresAt = date('Y-m-d H:i:s', time() + (30 * 86400)); // 30 dagen geldig
|
||||||
|
|
||||||
|
$db = get_db_connection();
|
||||||
|
ensure_remember_token_columns($db);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("UPDATE users SET remember_token = ?, remember_token_expires_at = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$tokenHash, $expiresAt, $userId]);
|
||||||
|
|
||||||
|
$isSecure = is_https_request();
|
||||||
|
setcookie('fociz_remember', $rawToken, [
|
||||||
|
'expires' => time() + (30 * 86400),
|
||||||
|
'path' => '/',
|
||||||
|
'secure' => $isSecure,
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $rawToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwijdert de persistente remember cookie
|
||||||
|
*/
|
||||||
|
function clear_remember_cookie(): void {
|
||||||
|
$isSecure = is_https_request();
|
||||||
|
setcookie('fociz_remember', '', [
|
||||||
|
'expires' => time() - 86400,
|
||||||
|
'path' => '/',
|
||||||
|
'secure' => $isSecure,
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Haalt de huidige ingelogde gebruiker op uit de database.
|
||||||
|
* Herstelt automatisch de sessie als een geldige 'fociz_remember' cookie aanwezig is.
|
||||||
*/
|
*/
|
||||||
function get_logged_in_user(): ?array {
|
function get_logged_in_user(): ?array {
|
||||||
start_secure_session();
|
start_secure_session();
|
||||||
|
|
||||||
if (empty($_SESSION['user_id']) || empty($_SESSION['is_2fa_verified'])) {
|
$db = get_db_connection();
|
||||||
return null;
|
ensure_remember_token_columns($db);
|
||||||
|
|
||||||
|
// 1. Controleer actieve PHP sessie
|
||||||
|
if (!empty($_SESSION['user_id']) && !empty($_SESSION['is_2fa_verified'])) {
|
||||||
|
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, created_at FROM users WHERE id = ?");
|
||||||
|
$stmt->execute([$_SESSION['user_id']]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
if ($user) {
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
// Gebruiker bestaat niet meer in database
|
||||||
|
$_SESSION = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$db = get_db_connection();
|
// 2. Controleer persistente 'remember me' cookie (voorkomt ongewenst uitloggen bij refresh of herstart)
|
||||||
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, created_at FROM users WHERE id = ?");
|
if (!empty($_COOKIE['fociz_remember'])) {
|
||||||
$stmt->execute([$_SESSION['user_id']]);
|
$rawToken = trim($_COOKIE['fociz_remember']);
|
||||||
$user = $stmt->fetch();
|
$tokenHash = hash('sha256', $rawToken);
|
||||||
|
|
||||||
return $user ?: null;
|
$stmt = $db->prepare("
|
||||||
|
SELECT id, name, email, role, two_factor_enabled, created_at
|
||||||
|
FROM users
|
||||||
|
WHERE remember_token = ? AND remember_token_expires_at > NOW()
|
||||||
|
");
|
||||||
|
$stmt->execute([$tokenHash]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
// Herstel volledige PHP sessie
|
||||||
|
$_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;
|
||||||
|
|
||||||
|
// Verleng direct de geldigheid van de remember token voor nog eens 30 dagen
|
||||||
|
issue_remember_token((int)$user['id']);
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
} else {
|
||||||
|
clear_remember_cookie();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Beschermt pagina's: dwingt login + 2FA af
|
* Beschermt pagina's: dwingt login + 2FA af
|
||||||
*/
|
*/
|
||||||
function require_auth(): array {
|
function require_auth(bool $json = false): array {
|
||||||
start_secure_session();
|
start_secure_session();
|
||||||
|
|
||||||
|
$isAjax = $json ||
|
||||||
|
(isset($_SERVER['HTTP_ACCEPT']) && str_contains($_SERVER['HTTP_ACCEPT'], 'application/json')) ||
|
||||||
|
(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
|
||||||
|
|
||||||
// 1. Controleer of eerste setup nodig is
|
// 1. Controleer of eerste setup nodig is
|
||||||
if (!has_admin_user()) {
|
if (!has_admin_user()) {
|
||||||
|
if ($isAjax) {
|
||||||
|
http_response_code(401);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Setup vereist']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
header('Location: /auth/setup.php');
|
header('Location: /auth/setup.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -67,6 +200,12 @@ function require_auth(): array {
|
|||||||
// 2. Controleer of gebruiker ingelogd én 2FA geverifieerd is
|
// 2. Controleer of gebruiker ingelogd én 2FA geverifieerd is
|
||||||
$user = get_logged_in_user();
|
$user = get_logged_in_user();
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
|
if ($isAjax) {
|
||||||
|
http_response_code(401);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Niet geauthenticeerd of sessie verlopen']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
header('Location: /auth/login.php');
|
header('Location: /auth/login.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -137,3 +276,52 @@ function verify_csrf_token(?string $token): bool {
|
|||||||
return !empty($token) && !empty($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
|
return !empty($token) && !empty($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Genereert een cryptografisch ondertekende OAuth state parameter waarin de user_id en timestamp veilig zijn opgeslagen.
|
||||||
|
* Dit voorkomt CSRF en maakt sessieherstel mogelijk bij cross-site OAuth redirects.
|
||||||
|
*/
|
||||||
|
function generate_oauth_state(int $userId): string {
|
||||||
|
$secret = defined('APP_SECRET') ? APP_SECRET : (defined('DB_PASS') ? DB_PASS : 'fociz_oauth_salt_2026');
|
||||||
|
$payload = [
|
||||||
|
'uid' => $userId,
|
||||||
|
'ts' => time(),
|
||||||
|
'rnd' => bin2hex(random_bytes(8))
|
||||||
|
];
|
||||||
|
$json = json_encode($payload);
|
||||||
|
$b64 = rtrim(strtr(base64_encode($json), '+/', '-_'), '=');
|
||||||
|
$sig = hash_hmac('sha256', $b64, $secret);
|
||||||
|
return $b64 . '.' . $sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valideert een ontvangen OAuth state parameter en geeft bij succes het geverifieerde user_id terug.
|
||||||
|
* Geeft null terug bij ongeldige handtekening of verlopen state (> 30 minuten).
|
||||||
|
*/
|
||||||
|
function verify_oauth_state(string $state): ?int {
|
||||||
|
$secret = defined('APP_SECRET') ? APP_SECRET : (defined('DB_PASS') ? DB_PASS : 'fociz_oauth_salt_2026');
|
||||||
|
$parts = explode('.', $state, 2);
|
||||||
|
if (count($parts) !== 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
[$b64, $sig] = $parts;
|
||||||
|
$expectedSig = hash_hmac('sha256', $b64, $secret);
|
||||||
|
if (!hash_equals($expectedSig, $sig)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$remainder = strlen($b64) % 4;
|
||||||
|
if ($remainder) {
|
||||||
|
$b64 .= str_repeat('=', 4 - $remainder);
|
||||||
|
}
|
||||||
|
$json = base64_decode(strtr($b64, '-_', '+/'));
|
||||||
|
$payload = json_decode($json, true);
|
||||||
|
if (!is_array($payload) || empty($payload['uid']) || empty($payload['ts'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Geldigheidsduur: maximaal 30 minuten (1800 seconden)
|
||||||
|
if (time() - (int)$payload['ts'] > 1800) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (int)$payload['uid'];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/auth.php';
|
require_once __DIR__ . '/auth.php';
|
||||||
$currentUser = $currentUser ?? get_logged_in_user();
|
$currentUser = $currentUser ?? require_auth();
|
||||||
$currentPage = basename($_SERVER['PHP_SELF']);
|
$currentPage = basename($_SERVER['PHP_SELF']);
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ if (!has_admin_user()) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($_SESSION['user_id']) && !empty($_SESSION['is_2fa_verified'])) {
|
$user = get_logged_in_user();
|
||||||
|
if ($user) {
|
||||||
header('Location: /planner.php');
|
header('Location: /planner.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
* Fociz Team & Uitnodigingen Beheer (Enkel voor Admin)
|
* Fociz Team & Uitnodigingen Beheer (Enkel voor Admin)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/includes/header.php';
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
$adminUser = require_admin();
|
$adminUser = require_admin();
|
||||||
|
$currentUser = $adminUser;
|
||||||
$db = get_db_connection();
|
$db = get_db_connection();
|
||||||
|
|
||||||
$message = null;
|
$message = null;
|
||||||
@@ -63,6 +64,8 @@ $invites = $db->query("
|
|||||||
JOIN users u ON i.created_by_id = u.id
|
JOIN users u ON i.created_by_id = u.id
|
||||||
ORDER BY i.created_at DESC
|
ORDER BY i.created_at DESC
|
||||||
")->fetchAll();
|
")->fetchAll();
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/header.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user