Compare commits

..

2 Commits

18 changed files with 413 additions and 163 deletions
+15 -2
View File
@@ -27,6 +27,10 @@ if (!empty($_GET['success'])) {
// POST Verwerking VÓÓR header output zodat redirects en opslaan vlekkeloos werken // POST Verwerking VÓÓR header output zodat redirects en opslaan vlekkeloos werken
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf_token($_POST['csrf_token'] ?? null)) {
$message = "Beveiligingsfout: CSRF-token ongeldig of verlopen. Ververs de pagina en probeer opnieuw.";
$messageType = 'error';
} else {
$action = $_POST['action'] ?? ''; $action = $_POST['action'] ?? '';
// 1. Centrale Azure App Registratie opslaan // 1. Centrale Azure App Registratie opslaan
@@ -194,7 +198,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$currentAi = get_ai_settings(); $currentAi = get_ai_settings();
if ($apiKeyIn !== '' && $apiKeyIn !== '••••••••••••••••') { if ($apiKeyIn !== '' && $apiKeyIn !== '••••••••••••••••') {
set_system_setting('ai_api_key', $apiKeyIn); set_system_setting('ai_api_key', fociz_encrypt($apiKeyIn));
} }
set_system_setting('ai_enabled', $aiEnabled); set_system_setting('ai_enabled', $aiEnabled);
@@ -224,6 +228,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
} }
} }
}
// Haal actuele configuratie op // Haal actuele configuratie op
$appConfig = microsoft_get_app_config(); $appConfig = microsoft_get_app_config();
@@ -425,6 +430,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" class="mt-6 space-y-5"> <form method="POST" class="mt-6 space-y-5">
<input type="hidden" name="action" value="save_azure"> <input type="hidden" name="action" value="save_azure">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<!-- Redirect URI Kopieerbox --> <!-- Redirect URI Kopieerbox -->
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 space-y-2"> <div class="p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 space-y-2">
@@ -563,6 +569,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" class="mt-6 space-y-4"> <form method="POST" class="mt-6 space-y-4">
<input type="hidden" name="action" value="save_jira_domain"> <input type="hidden" name="action" value="save_jira_domain">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div> <div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5"> <label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
@@ -697,8 +704,9 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" action="/admin.php" onsubmit="return confirm('Weet u zeker dat u het filter \'<?= htmlspecialchars(addslashes($filter['filter_name'])) ?>\' wilt verwijderen?');" class="inline"> <form method="POST" action="/admin.php" onsubmit="return confirm('Weet u zeker dat u het filter \'<?= htmlspecialchars(addslashes($filter['filter_name'])) ?>\' wilt verwijderen?');" class="inline">
<input type="hidden" name="action" value="delete_jira_queue_filter"> <input type="hidden" name="action" value="delete_jira_queue_filter">
<input type="hidden" name="filter_id" value="<?= (int)$filter['id'] ?>"> <input type="hidden" name="filter_id" value="<?= (int)$filter['id'] ?>">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<button type="submit" class="p-2 rounded-xl text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition cursor-pointer" title="Verwijderen"> <button type="submit" class="p-2 rounded-xl text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition cursor-pointer" title="Verwijderen">
<i data-lucide="trash-2" class="w-4 h-4"></i> <i data-lucide="trash-2" class="w-4 h-4 inline"></i>
</button> </button>
</form> </form>
</div> </div>
@@ -724,6 +732,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" action="/admin.php" class="space-y-4"> <form method="POST" action="/admin.php" class="space-y-4">
<input type="hidden" name="action" value="save_user_jira_assignments"> <input type="hidden" name="action" value="save_user_jira_assignments">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="overflow-x-auto rounded-2xl border border-slate-200 dark:border-slate-800"> <div class="overflow-x-auto rounded-2xl border border-slate-200 dark:border-slate-800">
<table class="w-full text-left text-xs"> <table class="w-full text-left text-xs">
@@ -805,6 +814,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" action="/admin.php" class="mt-4 space-y-4"> <form method="POST" action="/admin.php" class="mt-4 space-y-4">
<input type="hidden" name="action" value="save_jira_queue_filter"> <input type="hidden" name="action" value="save_jira_queue_filter">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<input type="hidden" id="filterModal_id" name="filter_id" value="0"> <input type="hidden" id="filterModal_id" name="filter_id" value="0">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
@@ -881,6 +891,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" action="/admin.php" class="space-y-4"> <form method="POST" action="/admin.php" class="space-y-4">
<input type="hidden" name="action" value="save_workday_settings"> <input type="hidden" name="action" value="save_workday_settings">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- Werkdag uren --> <!-- Werkdag uren -->
@@ -993,6 +1004,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" action="/admin.php" class="space-y-4"> <form method="POST" action="/admin.php" class="space-y-4">
<input type="hidden" name="action" value="save_ai_settings"> <input type="hidden" name="action" value="save_ai_settings">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="p-3.5 rounded-2xl bg-amber-50/70 dark:bg-amber-950/20 border border-amber-200/70 dark:border-amber-800/40"> <div class="p-3.5 rounded-2xl bg-amber-50/70 dark:bg-amber-950/20 border border-amber-200/70 dark:border-amber-800/40">
<label class="flex items-center space-x-2 text-xs text-amber-900 dark:text-amber-200 cursor-pointer select-none"> <label class="flex items-center space-x-2 text-xs text-amber-900 dark:text-amber-200 cursor-pointer select-none">
@@ -1080,6 +1092,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" action="/admin.php" class="flex flex-col sm:flex-row gap-2.5"> <form method="POST" action="/admin.php" class="flex flex-col sm:flex-row gap-2.5">
<input type="hidden" name="action" value="send_test_email"> <input type="hidden" name="action" value="send_test_email">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="relative flex-1"> <div class="relative flex-1">
<input <input
type="email" type="email"
+12 -1
View File
@@ -431,7 +431,9 @@ try {
$targetLaneId = (int)($input['target_lane_id'] ?? 0); $targetLaneId = (int)($input['target_lane_id'] ?? 0);
$targetPosition = (int)($input['target_position'] ?? 0); $targetPosition = (int)($input['target_position'] ?? 0);
$card = $db->query("SELECT * FROM board_cards WHERE id = {$cardId}")->fetch(); $stmtCard = $db->prepare("SELECT * FROM board_cards WHERE id = ?");
$stmtCard->execute([$cardId]);
$card = $stmtCard->fetch();
if (!$card) { if (!$card) {
echo json_encode(['success' => false, 'error' => 'Taak niet gevonden.']); echo json_encode(['success' => false, 'error' => 'Taak niet gevonden.']);
exit; exit;
@@ -443,6 +445,15 @@ try {
exit; exit;
} }
// Valideer of de doellane daadwerkelijk bij hetzelfde bord hoort (voorkom IDOR/lane hijacking)
$stmtLane = $db->prepare("SELECT id FROM board_lanes WHERE id = ? AND board_id = ?");
$stmtLane->execute([$targetLaneId, (int)$card['board_id']]);
if (!$stmtLane->fetch()) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Ongeldige doellane voor dit bord.']);
exit;
}
$stmt = $db->prepare("UPDATE board_cards SET lane_id = ?, position = ? WHERE id = ?"); $stmt = $db->prepare("UPDATE board_cards SET lane_id = ?, position = ? WHERE id = ?");
$stmt->execute([$targetLaneId, $targetPosition, $cardId]); $stmt->execute([$targetLaneId, $targetPosition, $cardId]);
+40 -2
View File
@@ -47,11 +47,25 @@ try {
exit; exit;
} }
// 2. Stemmen / Upvote op kaart // 2. Stemmen / Upvote op kaart (met bescherming tegen oneindig stemmen)
if ($action === 'vote_card') { if ($action === 'vote_card') {
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
$cardId = (int)($data['card_id'] ?? 0); $cardId = (int)($data['card_id'] ?? 0);
if (!isset($_SESSION['voted_cards'])) {
$_SESSION['voted_cards'] = [];
}
if (in_array($cardId, $_SESSION['voted_cards'])) {
$stmtVotes = $db->prepare("SELECT votes FROM brainstorm_cards WHERE id = ?");
$stmtVotes->execute([$cardId]);
$votes = (int)$stmtVotes->fetchColumn();
echo json_encode(['success' => false, 'error' => 'U heeft al op deze kaart gestemd.', 'votes' => $votes]);
exit;
}
$_SESSION['voted_cards'][] = $cardId;
$stmt = $db->prepare("UPDATE brainstorm_cards SET votes = votes + 1 WHERE id = ?"); $stmt = $db->prepare("UPDATE brainstorm_cards SET votes = votes + 1 WHERE id = ?");
$stmt->execute([$cardId]); $stmt->execute([$cardId]);
@@ -63,11 +77,35 @@ try {
exit; exit;
} }
// 3. Kaart verwijderen // 3. Kaart verwijderen (met autorisatiecontrole: auteur, sessie-eigenaar of beheerder)
if ($action === 'delete_card') { if ($action === 'delete_card') {
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
$cardId = (int)($data['card_id'] ?? 0); $cardId = (int)($data['card_id'] ?? 0);
$stmtCheck = $db->prepare("
SELECT c.*, s.created_by_id as session_owner_id
FROM brainstorm_cards c
JOIN brainstorm_sessions s ON c.session_id = s.id
WHERE c.id = ?
");
$stmtCheck->execute([$cardId]);
$card = $stmtCheck->fetch();
if (!$card) {
echo json_encode(['error' => 'Kaart niet gevonden']);
exit;
}
$isAuthor = ($card['author_name'] === $user['name']);
$isSessionOwner = ((int)$card['session_owner_id'] === (int)$user['id']);
$isAdmin = ($user['role'] === 'ADMIN');
if (!$isAuthor && !$isSessionOwner && !$isAdmin) {
http_response_code(403);
echo json_encode(['error' => 'Geen toestemming om deze kaart te verwijderen.']);
exit;
}
$stmt = $db->prepare("DELETE FROM brainstorm_cards WHERE id = ?"); $stmt = $db->prepare("DELETE FROM brainstorm_cards WHERE id = ?");
$stmt->execute([$cardId]); $stmt->execute([$cardId]);
+8 -4
View File
@@ -9,16 +9,20 @@ require_once __DIR__ . '/../../config/database.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
// Beveiligingscontrole: Verifieer geheime webhook token indien geconfigureerd in .env // Beveiligingscontrole: Verifieer geheime webhook token. Weiger toegang als secret ontbreekt.
$configuredSecret = fociz_env('JIRA_WEBHOOK_SECRET', ''); $configuredSecret = fociz_env('JIRA_WEBHOOK_SECRET', '');
if (!empty($configuredSecret)) { if (empty($configuredSecret)) {
http_response_code(403);
echo json_encode(['error' => 'Jira webhooks zijn niet ingeschakeld of geconfigureerd op deze server.']);
exit;
}
$incomingSecret = $_GET['secret'] ?? ($_SERVER['HTTP_X_ATLASSIAN_WEBHOOK_SECRET'] ?? ''); $incomingSecret = $_GET['secret'] ?? ($_SERVER['HTTP_X_ATLASSIAN_WEBHOOK_SECRET'] ?? '');
if (!hash_equals($configuredSecret, $incomingSecret)) { if (empty($incomingSecret) || !hash_equals($configuredSecret, $incomingSecret)) {
http_response_code(401); http_response_code(401);
echo json_encode(['error' => 'Onbevoegde webhook aanroep']); echo json_encode(['error' => 'Onbevoegde webhook aanroep']);
exit; exit;
} }
}
$rawPayload = file_get_contents('php://input'); $rawPayload = file_get_contents('php://input');
$data = json_decode($rawPayload, true); $data = json_decode($rawPayload, true);
+1
View File
File diff suppressed because one or more lines are too long
+16 -2
View File
@@ -18,11 +18,24 @@ $error = null;
$submitted = false; $submitted = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = strtolower(trim($_POST['email'] ?? '')); $clientIp = fociz_get_client_ip();
$ipRateKey = "forgot_ip_{$clientIp}";
$ipLimit = fociz_check_rate_limit($ipRateKey, 5, 900);
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) { $email = strtolower(trim($_POST['email'] ?? ''));
$emailRateKey = "forgot_email_{$email}";
$emailLimit = fociz_check_rate_limit($emailRateKey, 3, 900);
if (!verify_csrf_token($_POST['csrf_token'] ?? null)) {
$error = "Sessie verlopen of beveiligingstoken ongeldig. Ververs de pagina en probeer opnieuw.";
} elseif (!$ipLimit['allowed'] || !$emailLimit['allowed']) {
$retryAfter = max($ipLimit['retry_after'], $emailLimit['retry_after']);
$error = "Te veel herstelaanvragen. Probeer het over " . ceil($retryAfter / 60) . " minuten opnieuw.";
} elseif (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error = "Vul een geldig e-mailadres in."; $error = "Vul een geldig e-mailadres in.";
} else { } else {
fociz_hit_rate_limit($ipRateKey, 900);
fociz_hit_rate_limit($emailRateKey, 900);
try { try {
$db = get_db_connection(); $db = get_db_connection();
@@ -176,6 +189,7 @@ $pageTitle = "Fociz — Wachtwoord Vergeten";
</div> </div>
<?php else: ?> <?php else: ?>
<form method="POST" action="/auth/forgot-password.php" class="space-y-4"> <form method="POST" action="/auth/forgot-password.php" class="space-y-4">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div> <div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Uw e-mailadres</label> <label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Uw e-mailadres</label>
<div class="relative"> <div class="relative">
+13 -1
View File
@@ -191,7 +191,7 @@ $pageTitle = "Fociz — Uitnodiging Accepteren";
</p> </p>
<div class="inline-block p-3 bg-white rounded-2xl shadow-xs border border-slate-200 dark:border-slate-800"> <div class="inline-block p-3 bg-white rounded-2xl shadow-xs border border-slate-200 dark:border-slate-800">
<img src="<?= htmlspecialchars($qrCodeUrl) ?>" alt="2FA QR Code" class="w-40 h-40 mx-auto"> <div id="qrcode" class="w-40 h-40 mx-auto flex items-center justify-center"></div>
</div> </div>
<div class="bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl p-2.5 text-xs text-slate-600 dark:text-slate-400 max-w-sm mx-auto font-mono"> <div class="bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl p-2.5 text-xs text-slate-600 dark:text-slate-400 max-w-sm mx-auto font-mono">
@@ -213,8 +213,20 @@ $pageTitle = "Fociz — Uitnodiging Accepteren";
</form> </form>
</div> </div>
<script src="/assets/js/qrcode.min.js"></script>
<script> <script>
if (typeof lucide !== 'undefined') lucide.createIcons(); if (typeof lucide !== 'undefined') lucide.createIcons();
const qrContainer = document.getElementById('qrcode');
if (qrContainer) {
new QRCode(qrContainer, {
text: <?= json_encode($otpAuthUrl) ?>,
width: 160,
height: 160,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
});
}
</script> </script>
</body> </body>
</html> </html>
+31 -5
View File
@@ -42,13 +42,24 @@ if (isset($_GET['action']) && $_GET['action'] === 'restart') {
// POST Verwerking // POST Verwerking
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$db = get_db_connection(); $db = get_db_connection();
$clientIp = fociz_get_client_ip();
$ipRateKey = "login_ip_{$clientIp}";
$ipLimit = fociz_check_rate_limit($ipRateKey, 10, 900);
if (!verify_csrf_token($_POST['csrf_token'] ?? null)) {
$error = "Sessie verlopen of beveiligingstoken ongeldig. Ververs de pagina en probeer opnieuw.";
} elseif (!$ipLimit['allowed']) {
$error = "Te veel inlogpogingen vanaf dit netwerkadres. Probeer het over " . ceil($ipLimit['retry_after'] / 60) . " minuten opnieuw.";
} elseif ($step === 1) {
// STAP 1: Wachtwoord verificatie // STAP 1: Wachtwoord verificatie
if ($step === 1) {
$email = strtolower(trim($_POST['email'] ?? '')); $email = strtolower(trim($_POST['email'] ?? ''));
$password = $_POST['password'] ?? ''; $password = $_POST['password'] ?? '';
$emailRateKey = "login_email_{$email}";
$emailLimit = fociz_check_rate_limit($emailRateKey, 5, 900);
if (empty($email) || empty($password)) { if (!$emailLimit['allowed']) {
$error = "Dit account is tijdelijk geblokkeerd wegens te veel mislukte pogingen. Probeer het over " . ceil($emailLimit['retry_after'] / 60) . " minuten opnieuw.";
} elseif (empty($email) || empty($password)) {
$error = "Vul e-mailadres en wachtwoord in."; $error = "Vul e-mailadres en wachtwoord in.";
} else { } else {
$stmt = $db->prepare("SELECT id, name, email, password_hash, two_factor_secret, two_factor_enabled FROM users WHERE email = ?"); $stmt = $db->prepare("SELECT id, name, email, password_hash, two_factor_secret, two_factor_enabled FROM users WHERE email = ?");
@@ -56,8 +67,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$user = $stmt->fetch(); $user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) { if ($user && password_verify($password, $user['password_hash'])) {
fociz_clear_rate_limit($emailRateKey);
// Controleer of de inloglocatie/IP bekend is // Controleer of de inloglocatie/IP bekend is
$clientIp = fociz_get_client_ip();
$isIpKnown = fociz_is_ip_known_for_user((int)$user['id'], $clientIp); $isIpKnown = fociz_is_ip_known_for_user((int)$user['id'], $clientIp);
if (!$isIpKnown) { if (!$isIpKnown) {
@@ -72,6 +83,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$step = 2; $step = 2;
} }
} else { } else {
fociz_hit_rate_limit($ipRateKey, 900);
if (!empty($email)) {
fociz_hit_rate_limit($emailRateKey, 900);
}
$error = "Onjuist e-mailadres of wachtwoord."; $error = "Onjuist e-mailadres of wachtwoord.";
} }
} }
@@ -86,7 +101,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
exit; exit;
} }
if (empty($totpCode)) { $totpRateKey = "login_totp_{$userId}";
$totpLimit = fociz_check_rate_limit($totpRateKey, 5, 900);
if (!$totpLimit['allowed']) {
$error = "Te veel ongeldige verificatiecodes. Probeer het over " . ceil($totpLimit['retry_after'] / 60) . " minuten opnieuw.";
} elseif (empty($totpCode)) {
$error = "Vul de 6-cijferige 2FA code in uit uw authenticator app."; $error = "Vul de 6-cijferige 2FA code in uit uw authenticator app.";
} else { } else {
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?"); $stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
@@ -120,8 +140,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
if ($isValid) { if ($isValid) {
fociz_clear_rate_limit($totpRateKey);
fociz_clear_rate_limit($ipRateKey);
// Volledige login succesvol (geldig tot 23:59:59 van vandaag) // Volledige login succesvol (geldig tot 23:59:59 van vandaag)
$clientIp = fociz_get_client_ip();
fociz_register_user_ip((int)$user['id'], $clientIp, true); fociz_register_user_ip((int)$user['id'], $clientIp, true);
$_SESSION['user_id'] = $user['id']; $_SESSION['user_id'] = $user['id'];
@@ -142,6 +164,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Location: /planner.php'); header('Location: /planner.php');
exit; exit;
} else { } else {
fociz_hit_rate_limit($totpRateKey, 900);
fociz_hit_rate_limit($ipRateKey, 900);
$error = "Onjuiste 2FA code. Controleer uw authenticator app en probeer opnieuw."; $error = "Onjuiste 2FA code. Controleer uw authenticator app en probeer opnieuw.";
} }
} }
@@ -229,6 +253,7 @@ $pageTitle = "Fociz — Inloggen";
<?php if ($step === 1): ?> <?php if ($step === 1): ?>
<!-- STAP 1: E-mail & Wachtwoord --> <!-- STAP 1: E-mail & Wachtwoord -->
<form method="POST" action="/auth/login.php" class="space-y-4"> <form method="POST" action="/auth/login.php" class="space-y-4">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div> <div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">E-mailadres</label> <label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">E-mailadres</label>
<div class="relative"> <div class="relative">
@@ -256,6 +281,7 @@ $pageTitle = "Fociz — Inloggen";
<!-- STAP 2: 2FA TOTP Code --> <!-- STAP 2: 2FA TOTP Code -->
<form method="POST" action="/auth/login.php" class="space-y-5"> <form method="POST" action="/auth/login.php" class="space-y-5">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="text-center"> <div class="text-center">
<div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-indigo-50 dark:bg-indigo-950/40 text-indigo-600 dark:text-indigo-400 mb-2"> <div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-indigo-50 dark:bg-indigo-950/40 text-indigo-600 dark:text-indigo-400 mb-2">
<i data-lucide="key" class="w-6 h-6"></i> <i data-lucide="key" class="w-6 h-6"></i>
+13 -1
View File
@@ -214,7 +214,7 @@ $pageTitle = "Fociz — Eerste Initialisatie";
</p> </p>
<div class="inline-block p-3 bg-white rounded-2xl shadow-xs border border-slate-200 dark:border-slate-800"> <div class="inline-block p-3 bg-white rounded-2xl shadow-xs border border-slate-200 dark:border-slate-800">
<img src="<?= htmlspecialchars($qrCodeUrl) ?>" alt="2FA QR Code" class="w-44 h-44 mx-auto"> <div id="qrcode" class="w-44 h-44 mx-auto flex items-center justify-center"></div>
</div> </div>
<div class="bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl p-2.5 text-xs text-slate-600 dark:text-slate-400 max-w-sm mx-auto font-mono flex items-center justify-between"> <div class="bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl p-2.5 text-xs text-slate-600 dark:text-slate-400 max-w-sm mx-auto font-mono flex items-center justify-between">
@@ -238,8 +238,20 @@ $pageTitle = "Fociz — Eerste Initialisatie";
<?php endif; ?> <?php endif; ?>
</div> </div>
<script src="/assets/js/qrcode.min.js"></script>
<script> <script>
if (typeof lucide !== 'undefined') lucide.createIcons(); if (typeof lucide !== 'undefined') lucide.createIcons();
const qrContainer = document.getElementById('qrcode');
if (qrContainer) {
new QRCode(qrContainer, {
text: <?= json_encode($otpAuthUrl) ?>,
width: 176,
height: 176,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
});
}
</script> </script>
</body> </body>
</html> </html>
+4 -6
View File
@@ -17,6 +17,7 @@ if (!headers_sent()) {
header('X-XSS-Protection: 1; mode=block'); header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin'); header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: camera=(), microphone=(), geolocation=()'); header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.tailwindcss.com https://unpkg.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'");
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
} }
@@ -100,7 +101,7 @@ 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', $isDevEnv ? 'focizdev' : 'fociz')); define('DB_NAME', fociz_env('DB_NAME', $isDevEnv ? 'focizdev' : 'fociz'));
define('DB_USER', fociz_env('DB_USER', $isDevEnv ? 'focizdev_user' : 'focizlive_user')); define('DB_USER', fociz_env('DB_USER', $isDevEnv ? 'focizdev_user' : 'focizlive_user'));
define('DB_PASS', fociz_env('DB_PASS', $isDevEnv ? 'F0c1z_Dev_Secure_2026!' : 'F0c1z_Live_Secure_2026!')); define('DB_PASS', fociz_env('DB_PASS', ''));
define('DB_CHARSET', 'utf8mb4'); define('DB_CHARSET', 'utf8mb4');
// Bepaal de dynamische of geconfigureerde applicatie URL // Bepaal de dynamische of geconfigureerde applicatie URL
@@ -137,7 +138,7 @@ date_default_timezone_set('Europe/Amsterdam');
define('SMTP_HOST', fociz_env('SMTP_HOST', 'mail.fociz.nl')); define('SMTP_HOST', fociz_env('SMTP_HOST', 'mail.fociz.nl'));
define('SMTP_PORT', (int)fociz_env('SMTP_PORT', 587)); define('SMTP_PORT', (int)fociz_env('SMTP_PORT', 587));
define('SMTP_USER', fociz_env('SMTP_USER', 'info@fociz.nl')); define('SMTP_USER', fociz_env('SMTP_USER', 'info@fociz.nl'));
define('SMTP_PASS', fociz_env('SMTP_PASS', '6swpQSBg5uWFmjar7F6a')); define('SMTP_PASS', fociz_env('SMTP_PASS', ''));
define('SMTP_SECURE', fociz_env('SMTP_SECURE', 'tls')); // 'tls' (STARTTLS port 587) of 'ssl' (port 465) define('SMTP_SECURE', fociz_env('SMTP_SECURE', 'tls')); // 'tls' (STARTTLS port 587) of 'ssl' (port 465)
define('SMTP_FROM_EMAIL', fociz_env('SMTP_FROM_EMAIL', 'info@fociz.nl')); define('SMTP_FROM_EMAIL', fociz_env('SMTP_FROM_EMAIL', 'info@fociz.nl'));
define('SMTP_FROM_NAME', fociz_env('SMTP_FROM_NAME', 'Fociz')); define('SMTP_FROM_NAME', fociz_env('SMTP_FROM_NAME', 'Fociz'));
@@ -156,10 +157,7 @@ if (APP_ENV === 'live') {
$error = error_get_last(); $error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE])) { if ($error && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE])) {
error_log("Fociz Fatal Error: [{$error['type']}] {$error['message']} in {$error['file']}:{$error['line']}"); error_log("Fociz Fatal Error: [{$error['type']}] {$error['message']} in {$error['file']}:{$error['line']}");
$showDiagnostics = ( $showDiagnostics = (isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'ADMIN');
(isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'ADMIN') ||
(isset($_GET['debug']) && $_GET['debug'] === 'fociz')
);
if ($showDiagnostics) { if ($showDiagnostics) {
echo "<div style='font-family: monospace; background: #0c0f17; color: #ef4444; padding: 25px; margin: 20px; border-radius: 12px; border: 1px solid #dc2626;'> echo "<div style='font-family: monospace; background: #0c0f17; color: #ef4444; padding: 25px; margin: 20px; border-radius: 12px; border: 1px solid #dc2626;'>
<h3>⚠️ Fociz Systeemfout (Diagnostiek)</h3> <h3>⚠️ Fociz Systeemfout (Diagnostiek)</h3>
+9
View File
@@ -0,0 +1,9 @@
# Beveiliging van de interne includes map (Pen Test Beveiliging)
<IfModule authz_core_module>
Require all denied
</IfModule>
<IfModule !authz_core_module>
Order deny,allow
Deny from all
</IfModule>
+49
View File
@@ -568,6 +568,55 @@ 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);
} }
/**
* Rate limiting helpers via app_cache
*/
function fociz_check_rate_limit(string $actionKey, int $maxAttempts = 5, int $decaySeconds = 900): array {
require_once __DIR__ . '/cache.php';
$cacheKey = 'rl_' . hash('sha256', $actionKey);
$data = fociz_cache_get($cacheKey);
$attempts = 0;
$expiresAt = time() + $decaySeconds;
if (is_array($data)) {
$attempts = (int)($data['attempts'] ?? 0);
$expiresAt = (int)($data['expires_at'] ?? $expiresAt);
}
$allowed = ($attempts < $maxAttempts);
$remaining = max(0, $maxAttempts - $attempts);
$retryAfter = max(0, $expiresAt - time());
return [
'allowed' => $allowed,
'remaining' => $remaining,
'retry_after' => $retryAfter,
'attempts' => $attempts
];
}
function fociz_hit_rate_limit(string $actionKey, int $decaySeconds = 900): void {
require_once __DIR__ . '/cache.php';
$cacheKey = 'rl_' . hash('sha256', $actionKey);
$data = fociz_cache_get($cacheKey);
$attempts = 1;
$expiresAt = time() + $decaySeconds;
if (is_array($data)) {
$attempts = (int)($data['attempts'] ?? 0) + 1;
$expiresAt = (int)($data['expires_at'] ?? $expiresAt);
}
$ttl = max(1, $expiresAt - time());
fociz_cache_set($cacheKey, ['attempts' => $attempts, 'expires_at' => $expiresAt], $ttl);
}
function fociz_clear_rate_limit(string $actionKey): void {
require_once __DIR__ . '/cache.php';
$cacheKey = 'rl_' . hash('sha256', $actionKey);
fociz_cache_delete($cacheKey);
}
/** /**
* Genereert een cryptografisch ondertekende OAuth state parameter waarin de user_id en timestamp veilig zijn opgeslagen. * 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. * Dit voorkomt CSRF en maakt sessieherstel mogelijk bij cross-site OAuth redirects.
+29 -2
View File
@@ -9,14 +9,41 @@ require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../cache.php'; require_once __DIR__ . '/../cache.php';
/** /**
* Normaliseert het Jira domein naar een schone URL zonder slash aan het einde * Normaliseert het Jira domein naar een schone URL zonder slash aan het einde.
* Bevat SSRF-bescherming: weigert loopbacks, localhost en private netwerkadressen.
*/ */
function jira_normalize_domain(string $domain): string { function jira_normalize_domain(string $domain): string {
$domain = trim($domain); $domain = trim($domain);
if (empty($domain)) {
return '';
}
if (!preg_match('#^https?://#i', $domain)) { if (!preg_match('#^https?://#i', $domain)) {
$domain = 'https://' . $domain; $domain = 'https://' . $domain;
} }
return rtrim($domain, '/'); $domain = rtrim($domain, '/');
// Parseer en valideer de host ter voorkoming van SSRF (Server-Side Request Forgery)
$parsed = parse_url($domain);
$host = $parsed['host'] ?? '';
if (empty($host)) {
return '';
}
$lowerHost = strtolower($host);
// Weiger localhost en local domains
if ($lowerHost === 'localhost' || str_ends_with($lowerHost, '.localhost') || str_ends_with($lowerHost, '.local') || str_ends_with($lowerHost, '.internal')) {
return '';
}
// Weiger private IP-ranges en loopback adressen (bijv. 127.0.0.1, 192.168.x, 10.x, 172.16.x, 169.254.x)
if (filter_var($host, FILTER_VALIDATE_IP)) {
if (!filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return '';
}
}
return $domain;
} }
/** /**
+14 -3
View File
@@ -35,15 +35,26 @@ class FocizMailer {
$logs[] = date('H:i:s') . ' ' . $msg; $logs[] = date('H:i:s') . ' ' . $msg;
}; };
// Saniteer e-mailadressen en headers tegen CRLF en SMTP command injection
$toEmail = str_replace(["\r", "\n", "\0"], '', trim($toEmail));
$toName = str_replace(["\r", "\n", "\0"], '', trim($toName));
$subject = str_replace(["\r", "\n", "\0"], '', trim($subject));
if (!filter_var($toEmail, FILTER_VALIDATE_EMAIL)) {
$err = "Ongeldig e-mailadres voor verzending: {$toEmail}";
$log("FOUT: {$err}");
return ['success' => false, 'error' => $err, 'logs' => $logs];
}
// 1. Configuratieparameters ophalen // 1. Configuratieparameters ophalen
$host = $options['host'] ?? (defined('SMTP_HOST') ? SMTP_HOST : 'mail.fociz.nl'); $host = $options['host'] ?? (defined('SMTP_HOST') ? SMTP_HOST : 'mail.fociz.nl');
$port = (int)($options['port'] ?? (defined('SMTP_PORT') ? SMTP_PORT : 587)); $port = (int)($options['port'] ?? (defined('SMTP_PORT') ? SMTP_PORT : 587));
$username = $options['username'] ?? (defined('SMTP_USER') ? SMTP_USER : 'info@fociz.nl'); $username = $options['username'] ?? (defined('SMTP_USER') ? SMTP_USER : 'info@fociz.nl');
$password = $options['password'] ?? (defined('SMTP_PASS') ? SMTP_PASS : ''); $password = $options['password'] ?? (defined('SMTP_PASS') ? SMTP_PASS : '');
$secure = strtolower($options['secure'] ?? (defined('SMTP_SECURE') ? SMTP_SECURE : 'tls')); $secure = strtolower($options['secure'] ?? (defined('SMTP_SECURE') ? SMTP_SECURE : 'tls'));
$fromEmail = $options['from_email'] ?? (defined('SMTP_FROM_EMAIL') ? SMTP_FROM_EMAIL : 'no-reply@fociz.nl'); $fromEmail = str_replace(["\r", "\n", "\0"], '', trim($options['from_email'] ?? (defined('SMTP_FROM_EMAIL') ? SMTP_FROM_EMAIL : 'no-reply@fociz.nl')));
$fromName = $options['from_name'] ?? (defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'Fociz'); $fromName = str_replace(["\r", "\n", "\0"], '', trim($options['from_name'] ?? (defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'Fociz')));
$replyTo = $options['reply_to'] ?? (defined('SMTP_REPLY_TO') ? SMTP_REPLY_TO : 'info@fociz.nl'); $replyTo = str_replace(["\r", "\n", "\0"], '', trim($options['reply_to'] ?? (defined('SMTP_REPLY_TO') ? SMTP_REPLY_TO : 'info@fociz.nl')));
$timeout = (int)($options['timeout'] ?? 15); $timeout = (int)($options['timeout'] ?? 15);
// Technische afzender voor de SMTP envelope (moet doorgaans overeenkomen met het geauthenticeerde account) // Technische afzender voor de SMTP envelope (moet doorgaans overeenkomen met het geauthenticeerde account)
+8 -1
View File
@@ -110,7 +110,14 @@ function get_workday_settings(): array {
*/ */
function get_ai_settings(): array { function get_ai_settings(): array {
$envKey = fociz_env('OPENAI_API_KEY', ''); $envKey = fociz_env('OPENAI_API_KEY', '');
$savedKey = get_system_setting('ai_api_key', ''); $savedKeyRaw = get_system_setting('ai_api_key', '');
$savedKey = '';
if (!empty($savedKeyRaw)) {
if (!function_exists('fociz_decrypt')) {
require_once __DIR__ . '/auth.php';
}
$savedKey = fociz_decrypt($savedKeyRaw);
}
$apiKey = !empty($savedKey) ? $savedKey : $envKey; $apiKey = !empty($savedKey) ? $savedKey : $envKey;
return [ return [
+4 -6
View File
@@ -107,14 +107,12 @@ class TOTP {
} }
/** /**
* Genereert een QR code afbeelding URL (via QuickChart HTTPS) * Genereert een veilige client-side QR code (geen externe API calls meer ter bescherming van TOTP secrets)
*/ */
public static function getQrCodeUrl(string $otpAuthUrl, int $size = 220): string { public static function getQrCodeUrl(string $otpAuthUrl, int $size = 220): string {
return sprintf( // Externe API (quickchart.io) verwijderd om te voorkomen dat 2FA secrets uitlekken naar derden.
'https://quickchart.io/qr?text=%s&size=%d&margin=1&ecLevel=M', // QR rendering gebeurt nu 100% lokaal via /assets/js/qrcode.min.js in de browser.
urlencode($otpAuthUrl), return '';
$size
);
} }
/** /**
+57 -51
View File
@@ -25,6 +25,10 @@ if (!empty($_GET['success'])) {
// POST Verwerking VÓÓR headers zodat redirects en mutaties soepel verlopen // POST Verwerking VÓÓR headers zodat redirects en mutaties soepel verlopen
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf_token($_POST['csrf_token'] ?? null)) {
$message = "Beveiligingsfout: CSRF-token ongeldig of verlopen. Ververs de pagina en probeer opnieuw.";
$messageType = 'error';
} else {
$action = $_POST['action'] ?? ''; $action = $_POST['action'] ?? '';
// 1. Outlook koppelen via OAuth // 1. Outlook koppelen via OAuth
@@ -79,7 +83,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: []; $existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: [];
if (($jiraToken === '' || $jiraToken === '••••••••••••••••') && !empty($existingSettings['jira_api_token']) && $existingSettings['jira_api_token'] !== '••••••••••••••••') { if (($jiraToken === '' || $jiraToken === '••••••••••••••••') && !empty($existingSettings['jira_api_token']) && $existingSettings['jira_api_token'] !== '••••••••••••••••') {
$jiraToken = $existingSettings['jira_api_token']; $jiraToken = fociz_decrypt($existingSettings['jira_api_token']);
} }
$accountName = $existingSettings['account_name'] ?? ''; $accountName = $existingSettings['account_name'] ?? '';
@@ -104,7 +108,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$newSettings = [ $newSettings = [
'jira_domain' => $jiraDomain, 'jira_domain' => $jiraDomain,
'jira_email' => $jiraEmail, 'jira_email' => $jiraEmail,
'jira_api_token' => $jiraToken, 'jira_api_token' => fociz_encrypt($jiraToken),
'account_name' => $accountName, 'account_name' => $accountName,
'account_id' => $accountId, 'account_id' => $accountId,
]; ];
@@ -266,6 +270,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$messageType = 'success'; $messageType = 'success';
} }
} }
}
// Actuele gegevens ophalen // Actuele gegevens ophalen
$stmtUser = $db->prepare("SELECT * FROM users WHERE id = ?"); $stmtUser = $db->prepare("SELECT * FROM users WHERE id = ?");
@@ -524,6 +529,7 @@ require_once __DIR__ . '/includes/header.php';
<?php if (!empty($userProfile['avatar_url'])): ?> <?php if (!empty($userProfile['avatar_url'])): ?>
<form method="POST" action="/profile.php" class="self-start sm:self-auto"> <form method="POST" action="/profile.php" class="self-start sm:self-auto">
<input type="hidden" name="action" value="remove_avatar"> <input type="hidden" name="action" value="remove_avatar">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<button type="submit" onclick="return confirm('Avatar herstellen naar standaard initiaal-badge?')" class="inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-950/30 border border-rose-200 dark:border-rose-800/50 hover:bg-rose-100 dark:hover:bg-rose-900/50 transition cursor-pointer"> <button type="submit" onclick="return confirm('Avatar herstellen naar standaard initiaal-badge?')" class="inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-950/30 border border-rose-200 dark:border-rose-800/50 hover:bg-rose-100 dark:hover:bg-rose-900/50 transition cursor-pointer">
<i data-lucide="rotate-ccw" class="w-3.5 h-3.5"></i> <i data-lucide="rotate-ccw" class="w-3.5 h-3.5"></i>
<span>Standaard Herstellen</span> <span>Standaard Herstellen</span>
@@ -532,61 +538,51 @@ require_once __DIR__ . '/includes/header.php';
<?php endif; ?> <?php endif; ?>
</div> </div>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6"> <!-- Avatar Selector Grid & Upload -->
<!-- Linker kolom: 24 Standaard Preset Avatars (8 cols) --> <div class="mt-6 grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
<div class="lg:col-span-8 space-y-3"> <!-- Linker kolom: 24 mini karakters in grid (8 cols) -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2"> <div class="lg:col-span-8 space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-xs font-bold text-slate-800 dark:text-slate-200 flex items-center space-x-1.5"> <h3 class="text-xs font-bold text-slate-800 dark:text-slate-200 flex items-center space-x-1.5">
<i data-lucide="sparkles" class="w-3.5 h-3.5 text-indigo-500"></i> <i data-lucide="sparkles" class="w-3.5 h-3.5 text-indigo-500"></i>
<span>Kies een Karakter (24 Flitsende Avatars)</span> <span>Kies een Karakter (24 Stijlen)</span>
</h3> </h3>
<span class="text-[11px] text-slate-400 dark:text-slate-500">Klik om direct toe te passen</span>
<!-- Categorie Filters -->
<div class="flex items-center space-x-1 bg-slate-100 dark:bg-[#0c0f17] p-1 rounded-xl border border-slate-200 dark:border-slate-800 text-[11px]" id="avatarFilterBar">
<button type="button" onclick="filterAvatars('all')" class="avatar-filter-btn active px-2.5 py-1 rounded-lg font-medium transition cursor-pointer" data-cat="all">Alles</button>
<button type="button" onclick="filterAvatars('pro')" class="avatar-filter-btn px-2.5 py-1 rounded-lg font-medium transition text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white cursor-pointer" data-cat="pro">Pro</button>
<button type="button" onclick="filterAvatars('creative')" class="avatar-filter-btn px-2.5 py-1 rounded-lg font-medium transition text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white cursor-pointer" data-cat="creative">Creatief</button>
<button type="button" onclick="filterAvatars('tech')" class="avatar-filter-btn px-2.5 py-1 rounded-lg font-medium transition text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white cursor-pointer" data-cat="tech">Tech</button>
<button type="button" onclick="filterAvatars('heroes')" class="avatar-filter-btn px-2.5 py-1 rounded-lg font-medium transition text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white cursor-pointer" data-cat="heroes">Heroes</button>
</div>
</div> </div>
<!-- Categorie Filter Pills --> <div class="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2.5 p-3 rounded-2xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200 dark:border-slate-800" id="avatarGrid">
<div class="flex flex-wrap items-center gap-1.5 pt-1">
<button type="button" onclick="filterAvatarCategory('all')" id="avCatBtn-all" class="av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-bold transition cursor-pointer bg-indigo-600 text-white shadow-xs">
Alle (24)
</button>
<button type="button" onclick="filterAvatarCategory('emoji')" id="avCatBtn-emoji" class="av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-semibold transition cursor-pointer bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300">
⚡ Fun & Emojis (6)
</button>
<button type="button" onclick="filterAvatarCategory('bots')" id="avCatBtn-bots" class="av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-semibold transition cursor-pointer bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300">
🤖 Cyber & Bots (6)
</button>
<button type="button" onclick="filterAvatarCategory('personas')" id="avCatBtn-personas" class="av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-semibold transition cursor-pointer bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300">
✨ Moderne Personas (6)
</button>
<button type="button" onclick="filterAvatarCategory('heroes')" id="avCatBtn-heroes" class="av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-semibold transition cursor-pointer bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300">
🎯 Adventurer & Heroes (6)
</button>
</div>
<div class="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-3 p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800">
<?php <?php
$avatarPresets = [ $avatarPresets = [
['file' => 'avatar-01.svg', 'cat' => 'emoji', 'name' => 'Cool Star (Zonnebril)'], ['file' => 'avatar-01.svg', 'cat' => 'pro', 'name' => 'Alex (Business)'],
['file' => 'avatar-02.svg', 'cat' => 'emoji', 'name' => 'Party Champion'], ['file' => 'avatar-02.svg', 'cat' => 'pro', 'name' => 'Sophie (Corporate)'],
['file' => 'avatar-03.svg', 'cat' => 'emoji', 'name' => 'Celebration Vibe'], ['file' => 'avatar-03.svg', 'cat' => 'pro', 'name' => 'Liam (Consultant)'],
['file' => 'avatar-04.svg', 'cat' => 'emoji', 'name' => 'Laser Eyes Energy'], ['file' => 'avatar-04.svg', 'cat' => 'pro', 'name' => 'Emma (Manager)'],
['file' => 'avatar-05.svg', 'cat' => 'emoji', 'name' => 'Sunny Smiles'], ['file' => 'avatar-05.svg', 'cat' => 'pro', 'name' => 'Daan (Director)'],
['file' => 'avatar-06.svg', 'cat' => 'emoji', 'name' => 'Fire Heart'], ['file' => 'avatar-06.svg', 'cat' => 'pro', 'name' => 'Noa (Executive)'],
['file' => 'avatar-07.svg', 'cat' => 'bots', 'name' => 'Cyber Tron'], ['file' => 'avatar-07.svg', 'cat' => 'creative', 'name' => 'Mila (Designer)'],
['file' => 'avatar-08.svg', 'cat' => 'bots', 'name' => 'Aero Bot'], ['file' => 'avatar-08.svg', 'cat' => 'creative', 'name' => 'Lucas (Creator)'],
['file' => 'avatar-09.svg', 'cat' => 'bots', 'name' => 'Neon Volt'], ['file' => 'avatar-09.svg', 'cat' => 'creative', 'name' => 'Zara (Artist)'],
['file' => 'avatar-10.svg', 'cat' => 'bots', 'name' => 'Pixel Pulse'], ['file' => 'avatar-10.svg', 'cat' => 'creative', 'name' => 'Finn (Illustrator)'],
['file' => 'avatar-11.svg', 'cat' => 'bots', 'name' => 'Quantum AI'], ['file' => 'avatar-11.svg', 'cat' => 'creative', 'name' => 'Lotte (Writer)'],
['file' => 'avatar-12.svg', 'cat' => 'bots', 'name' => 'Robo Racer'], ['file' => 'avatar-12.svg', 'cat' => 'creative', 'name' => 'Sem (Architect)'],
['file' => 'avatar-13.svg', 'cat' => 'personas', 'name' => 'Alex (Hipster)'], ['file' => 'avatar-13.svg', 'cat' => 'tech', 'name' => 'Bram (Developer)'],
['file' => 'avatar-14.svg', 'cat' => 'personas', 'name' => 'Maya (Tech Lead)'], ['file' => 'avatar-14.svg', 'cat' => 'tech', 'name' => 'Julia (Engineer)'],
['file' => 'avatar-15.svg', 'cat' => 'personas', 'name' => 'Lucas (Designer)'], ['file' => 'avatar-15.svg', 'cat' => 'tech', 'name' => 'Jesse (DevOps)'],
['file' => 'avatar-16.svg', 'cat' => 'personas', 'name' => 'Elena (Developer)'], ['file' => 'avatar-16.svg', 'cat' => 'tech', 'name' => 'Tess (Data Scientist)'],
['file' => 'avatar-17.svg', 'cat' => 'personas', 'name' => 'Sam (Creative)'], ['file' => 'avatar-17.svg', 'cat' => 'tech', 'name' => 'Milan (Cyber Sec)'],
['file' => 'avatar-18.svg', 'cat' => 'personas', 'name' => 'Zoe (Founder)'], ['file' => 'avatar-18.svg', 'cat' => 'tech', 'name' => 'Liv (AI Engineer)'],
['file' => 'avatar-19.svg', 'cat' => 'heroes', 'name' => 'Jack (Adventurer)'], ['file' => 'avatar-19.svg', 'cat' => 'heroes', 'name' => 'Max (Ninja)'],
['file' => 'avatar-20.svg', 'cat' => 'heroes', 'name' => 'Luna (Heroine)'], ['file' => 'avatar-20.svg', 'cat' => 'heroes', 'name' => 'Sara (Astronaut)'],
['file' => 'avatar-21.svg', 'cat' => 'heroes', 'name' => 'Oliver (Explorer)'], ['file' => 'avatar-21.svg', 'cat' => 'heroes', 'name' => 'Robin (Pilot)'],
['file' => 'avatar-22.svg', 'cat' => 'heroes', 'name' => 'Mia (Rebel)'], ['file' => 'avatar-22.svg', 'cat' => 'heroes', 'name' => 'Eva (Explorer)'],
['file' => 'avatar-23.svg', 'cat' => 'heroes', 'name' => 'Leo (Cosmic)'], ['file' => 'avatar-23.svg', 'cat' => 'heroes', 'name' => 'Leo (Cosmic)'],
['file' => 'avatar-24.svg', 'cat' => 'heroes', 'name' => 'Nova (Cyberpunk)'], ['file' => 'avatar-24.svg', 'cat' => 'heroes', 'name' => 'Nova (Cyberpunk)'],
]; ];
@@ -597,6 +593,7 @@ require_once __DIR__ . '/includes/header.php';
?> ?>
<form method="POST" action="/profile.php" class="relative group avatar-item" data-category="<?= $av['cat'] ?>"> <form method="POST" action="/profile.php" class="relative group avatar-item" data-category="<?= $av['cat'] ?>">
<input type="hidden" name="action" value="select_preset_avatar"> <input type="hidden" name="action" value="select_preset_avatar">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<input type="hidden" name="avatar_preset" value="<?= $avatarPath ?>"> <input type="hidden" name="avatar_preset" value="<?= $avatarPath ?>">
<button type="submit" class="w-full aspect-square rounded-2xl p-1 transition-all duration-200 transform hover:scale-110 active:scale-95 hover:shadow-lg hover:shadow-indigo-500/25 focus:outline-none cursor-pointer <?= $isSelected ? 'ring-2 ring-indigo-600 dark:ring-indigo-400 bg-indigo-50 dark:bg-indigo-950/40 shadow-sm' : 'hover:bg-white dark:hover:bg-slate-800 border border-transparent' ?>" title="<?= htmlspecialchars($av['name']) ?>"> <button type="submit" class="w-full aspect-square rounded-2xl p-1 transition-all duration-200 transform hover:scale-110 active:scale-95 hover:shadow-lg hover:shadow-indigo-500/25 focus:outline-none cursor-pointer <?= $isSelected ? 'ring-2 ring-indigo-600 dark:ring-indigo-400 bg-indigo-50 dark:bg-indigo-950/40 shadow-sm' : 'hover:bg-white dark:hover:bg-slate-800 border border-transparent' ?>" title="<?= htmlspecialchars($av['name']) ?>">
<img src="<?= $avatarPath ?>" alt="<?= htmlspecialchars($av['name']) ?>" class="w-full h-full rounded-xl object-contain pointer-events-none drop-shadow-xs"> <img src="<?= $avatarPath ?>" alt="<?= htmlspecialchars($av['name']) ?>" class="w-full h-full rounded-xl object-contain pointer-events-none drop-shadow-xs">
@@ -620,6 +617,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" action="/profile.php" enctype="multipart/form-data" class="p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 space-y-4"> <form method="POST" action="/profile.php" enctype="multipart/form-data" class="p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 space-y-4">
<input type="hidden" name="action" value="upload_custom_avatar"> <input type="hidden" name="action" value="upload_custom_avatar">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="text-center p-5 border-2 border-dashed border-slate-300 dark:border-slate-700 rounded-xl hover:border-indigo-500 dark:hover:border-indigo-400 transition bg-white dark:bg-[#111622]"> <div class="text-center p-5 border-2 border-dashed border-slate-300 dark:border-slate-700 rounded-xl hover:border-indigo-500 dark:hover:border-indigo-400 transition bg-white dark:bg-[#111622]">
<div class="w-10 h-10 mx-auto mb-2 rounded-xl bg-indigo-50 dark:bg-indigo-950/40 text-indigo-600 dark:text-indigo-400 flex items-center justify-center"> <div class="w-10 h-10 mx-auto mb-2 rounded-xl bg-indigo-50 dark:bg-indigo-950/40 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
@@ -674,6 +672,7 @@ require_once __DIR__ . '/includes/header.php';
</a> </a>
<form method="POST" onsubmit="return confirm('Weet u zeker dat u uw Microsoft Outlook agenda wilt ontkoppelen?');"> <form method="POST" onsubmit="return confirm('Weet u zeker dat u uw Microsoft Outlook agenda wilt ontkoppelen?');">
<input type="hidden" name="action" value="disconnect_outlook"> <input type="hidden" name="action" value="disconnect_outlook">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<button type="submit" class="px-3 py-1.5 rounded-xl bg-rose-50 hover:bg-rose-100 dark:bg-rose-500/10 dark:hover:bg-rose-500/20 text-rose-600 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20 text-xs font-medium transition cursor-pointer"> <button type="submit" class="px-3 py-1.5 rounded-xl bg-rose-50 hover:bg-rose-100 dark:bg-rose-500/10 dark:hover:bg-rose-500/20 text-rose-600 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20 text-xs font-medium transition cursor-pointer">
Ontkoppelen Ontkoppelen
</button> </button>
@@ -703,6 +702,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" class="mt-4 p-4 rounded-xl bg-slate-50/70 dark:bg-slate-900/60 border border-slate-200/80 dark:border-slate-800 flex flex-col sm:flex-row sm:items-center justify-between gap-4"> <form method="POST" class="mt-4 p-4 rounded-xl bg-slate-50/70 dark:bg-slate-900/60 border border-slate-200/80 dark:border-slate-800 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<input type="hidden" name="action" value="save_outlook_settings"> <input type="hidden" name="action" value="save_outlook_settings">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="space-y-1"> <div class="space-y-1">
<label for="sync_interval_seconds" class="text-xs font-bold text-slate-900 dark:text-white flex items-center space-x-1.5"> <label for="sync_interval_seconds" class="text-xs font-bold text-slate-900 dark:text-white flex items-center space-x-1.5">
<i data-lucide="refresh-cw" class="w-3.5 h-3.5 text-sky-500"></i> <i data-lucide="refresh-cw" class="w-3.5 h-3.5 text-sky-500"></i>
@@ -752,6 +752,7 @@ require_once __DIR__ . '/includes/header.php';
<div> <div>
<form method="POST"> <form method="POST">
<input type="hidden" name="action" value="connect_outlook"> <input type="hidden" name="action" value="connect_outlook">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<button type="submit" class="inline-flex items-center space-x-2 px-6 py-3 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-semibold text-xs transition shadow-xs cursor-pointer"> <button type="submit" class="inline-flex items-center space-x-2 px-6 py-3 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-semibold text-xs transition shadow-xs cursor-pointer">
<i data-lucide="log-in" class="w-4 h-4"></i> <i data-lucide="log-in" class="w-4 h-4"></i>
<span>Koppel Mijn Outlook Agenda</span> <span>Koppel Mijn Outlook Agenda</span>
@@ -802,6 +803,7 @@ require_once __DIR__ . '/includes/header.php';
<?php if ($isJiraConnected): ?> <?php if ($isJiraConnected): ?>
<form method="POST" onsubmit="return confirm('Weet u zeker dat u Jira wilt ontkoppelen?');"> <form method="POST" onsubmit="return confirm('Weet u zeker dat u Jira wilt ontkoppelen?');">
<input type="hidden" name="action" value="disconnect_jira"> <input type="hidden" name="action" value="disconnect_jira">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<button type="submit" class="px-3 py-1.5 rounded-xl bg-rose-50 hover:bg-rose-100 dark:bg-rose-500/10 dark:hover:bg-rose-500/20 text-rose-600 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20 text-xs font-medium transition cursor-pointer"> <button type="submit" class="px-3 py-1.5 rounded-xl bg-rose-50 hover:bg-rose-100 dark:bg-rose-500/10 dark:hover:bg-rose-500/20 text-rose-600 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20 text-xs font-medium transition cursor-pointer">
Jira Ontkoppelen Jira Ontkoppelen
</button> </button>
@@ -811,6 +813,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" class="mt-6 space-y-4"> <form method="POST" class="mt-6 space-y-4">
<input type="hidden" name="action" value="save_jira"> <input type="hidden" name="action" value="save_jira">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
@@ -840,7 +843,7 @@ require_once __DIR__ . '/includes/header.php';
</a> </a>
</div> </div>
<div class="relative"> <div class="relative">
<input type="password" id="jiraApiTokenInput" name="jira_api_token" value="<?= !empty($jiraSettings['jira_api_token']) ? htmlspecialchars($jiraSettings['jira_api_token']) : '' ?>" placeholder="Plak hier uw Atlassian API token..." class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl pl-3.5 pr-10 py-2.5 text-xs text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-indigo-500"> <input type="password" id="jiraApiTokenInput" name="jira_api_token" value="<?= !empty($jiraSettings['jira_api_token']) ? '••••••••••••••••' : '' ?>" placeholder="Plak hier uw Atlassian API token..." class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl pl-3.5 pr-10 py-2.5 text-xs text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<button type="button" onclick="toggleJiraToken()" id="toggleJiraTokenBtn" title="Toon / verberg token" class="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 cursor-pointer"> <button type="button" onclick="toggleJiraToken()" id="toggleJiraTokenBtn" title="Toon / verberg token" class="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 cursor-pointer">
<i data-lucide="eye" class="w-4 h-4" id="eyeJiraIcon"></i> <i data-lucide="eye" class="w-4 h-4" id="eyeJiraIcon"></i>
</button> </button>
@@ -912,6 +915,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" class="space-y-4"> <form method="POST" class="space-y-4">
<input type="hidden" name="action" value="save_notifications"> <input type="hidden" name="action" value="save_notifications">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="flex items-center justify-between p-4 rounded-xl bg-slate-50/70 dark:bg-slate-900/60 border border-slate-200/80 dark:border-slate-800"> <div class="flex items-center justify-between p-4 rounded-xl bg-slate-50/70 dark:bg-slate-900/60 border border-slate-200/80 dark:border-slate-800">
<div> <div>
@@ -1021,6 +1025,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" class="space-y-4"> <form method="POST" class="space-y-4">
<input type="hidden" name="action" value="update_profile"> <input type="hidden" name="action" value="update_profile">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div> <div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5"> <label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
@@ -1054,6 +1059,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" class="space-y-3"> <form method="POST" class="space-y-3">
<input type="hidden" name="action" value="change_password"> <input type="hidden" name="action" value="change_password">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div> <div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1"> <label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
+37 -23
View File
@@ -13,8 +13,14 @@ $message = null;
$generatedLink = null; $generatedLink = null;
$error = null; $error = null;
// Uitnodiging aanmaken // POST acties afhandelen met CSRF-bescherming
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_invite') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
$csrfToken = $_POST['csrf_token'] ?? null;
if (!verify_csrf_token($csrfToken)) {
$error = "Beveiligingsfout: CSRF-token ongeldig of verlopen. Ververs de pagina en probeer opnieuw.";
} elseif ($action === 'create_invite') {
$email = strtolower(trim($_POST['email'] ?? '')); $email = strtolower(trim($_POST['email'] ?? ''));
$role = in_array($_POST['role'] ?? '', ['ADMIN', 'MEMBER']) ? $_POST['role'] : 'MEMBER'; $role = in_array($_POST['role'] ?? '', ['ADMIN', 'MEMBER']) ? $_POST['role'] : 'MEMBER';
@@ -40,11 +46,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
} }
} }
} }
} } elseif ($action === 'resend_invite') {
$inviteId = (int)($_POST['invite_id'] ?? 0);
// Uitnodiging opnieuw verzenden per e-mail
if (isset($_GET['resend_invite'])) {
$inviteId = (int)$_GET['resend_invite'];
$stmtInv = $db->prepare("SELECT * FROM invites WHERE id = ? AND used_at IS NULL AND expires_at > NOW()"); $stmtInv = $db->prepare("SELECT * FROM invites WHERE id = ? AND used_at IS NULL AND expires_at > NOW()");
$stmtInv->execute([$inviteId]); $stmtInv->execute([$inviteId]);
$inv = $stmtInv->fetch(); $inv = $stmtInv->fetch();
@@ -60,20 +63,14 @@ if (isset($_GET['resend_invite'])) {
} else { } else {
$error = "Uitnodiging niet gevonden of reeds verlopen/geactiveerd."; $error = "Uitnodiging niet gevonden of reeds verlopen/geactiveerd.";
} }
} } elseif ($action === 'delete_invite') {
$inviteId = (int)($_POST['invite_id'] ?? 0);
// Uitnodiging intrekken
if (isset($_GET['delete_invite'])) {
$inviteId = (int)$_GET['delete_invite'];
$stmtDel = $db->prepare("DELETE FROM invites WHERE id = ?"); $stmtDel = $db->prepare("DELETE FROM invites WHERE id = ?");
$stmtDel->execute([$inviteId]); $stmtDel->execute([$inviteId]);
header('Location: /users.php'); header('Location: /users.php');
exit; exit;
} } elseif ($action === 'delete_user') {
$delUserId = (int)($_POST['user_id'] ?? 0);
// Gebruiker verwijderen
if (isset($_GET['delete_user'])) {
$delUserId = (int)$_GET['delete_user'];
if ($delUserId !== (int)$adminUser['id']) { if ($delUserId !== (int)$adminUser['id']) {
$stmtDelUser = $db->prepare("DELETE FROM users WHERE id = ?"); $stmtDelUser = $db->prepare("DELETE FROM users WHERE id = ?");
$stmtDelUser->execute([$delUserId]); $stmtDelUser->execute([$delUserId]);
@@ -81,6 +78,7 @@ if (isset($_GET['delete_user'])) {
header('Location: /users.php'); header('Location: /users.php');
exit; exit;
} }
}
// Haal alle gebruikers op // Haal alle gebruikers op
$users = []; $users = [];
@@ -158,6 +156,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" action="/users.php" class="grid grid-cols-1 md:grid-cols-12 gap-3 items-end"> <form method="POST" action="/users.php" class="grid grid-cols-1 md:grid-cols-12 gap-3 items-end">
<input type="hidden" name="action" value="create_invite"> <input type="hidden" name="action" value="create_invite">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="md:col-span-6"> <div class="md:col-span-6">
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">E-mailadres van collega</label> <label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">E-mailadres van collega</label>
@@ -237,9 +236,14 @@ require_once __DIR__ . '/includes/header.php';
</td> </td>
<td class="p-3 text-right"> <td class="p-3 text-right">
<?php if ($u['id'] !== $adminUser['id']): ?> <?php if ($u['id'] !== $adminUser['id']): ?>
<a href="/users.php?delete_user=<?= $u['id'] ?>" onclick="return confirm('Gebruiker verwijderen?')" class="text-slate-400 hover:text-red-600 dark:text-slate-500 dark:hover:text-red-400 transition p-1 cursor-pointer"> <form method="POST" action="/users.php" class="inline" onsubmit="return confirm('Weet u zeker dat u deze gebruiker wilt verwijderen?')">
<input type="hidden" name="action" value="delete_user">
<input type="hidden" name="user_id" value="<?= (int)$u['id'] ?>">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<button type="submit" title="Gebruiker verwijderen" class="text-slate-400 hover:text-red-600 dark:text-slate-500 dark:hover:text-red-400 transition p-1 cursor-pointer bg-transparent border-0">
<i data-lucide="trash-2" class="w-4 h-4 inline"></i> <i data-lucide="trash-2" class="w-4 h-4 inline"></i>
</a> </button>
</form>
<?php else: ?> <?php else: ?>
<span class="text-[10px] text-slate-400 dark:text-slate-600 font-medium">Uzelf</span> <span class="text-[10px] text-slate-400 dark:text-slate-600 font-medium">Uzelf</span>
<?php endif; ?> <?php endif; ?>
@@ -295,14 +299,24 @@ require_once __DIR__ . '/includes/header.php';
<td class="p-3 text-right"> <td class="p-3 text-right">
<div class="flex items-center justify-end space-x-2"> <div class="flex items-center justify-end space-x-2">
<?php if (!$isUsed && !$isExpired): ?> <?php if (!$isUsed && !$isExpired): ?>
<a href="/users.php?resend_invite=<?= $inv['id'] ?>" title="Uitnodigingsmail opnieuw verzenden" class="inline-flex items-center space-x-1 px-2 py-1 bg-indigo-50 hover:bg-indigo-100 dark:bg-indigo-950/40 dark:hover:bg-indigo-900/60 text-indigo-600 dark:text-indigo-400 rounded-lg text-[11px] font-medium transition cursor-pointer"> <form method="POST" action="/users.php" class="inline">
<input type="hidden" name="action" value="resend_invite">
<input type="hidden" name="invite_id" value="<?= (int)$inv['id'] ?>">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<button type="submit" title="Uitnodigingsmail opnieuw verzenden" class="inline-flex items-center space-x-1 px-2 py-1 bg-indigo-50 hover:bg-indigo-100 dark:bg-indigo-950/40 dark:hover:bg-indigo-900/60 text-indigo-600 dark:text-indigo-400 rounded-lg text-[11px] font-medium transition cursor-pointer border-0">
<i data-lucide="mail" class="w-3.5 h-3.5"></i> <i data-lucide="mail" class="w-3.5 h-3.5"></i>
<span>Opnieuw mailen</span> <span>Opnieuw mailen</span>
</a> </button>
</form>
<?php endif; ?> <?php endif; ?>
<a href="/users.php?delete_invite=<?= $inv['id'] ?>" onclick="return confirm('Uitnodiging intrekken?')" title="Uitnodiging intrekken" class="text-slate-400 hover:text-red-600 dark:text-slate-500 dark:hover:text-red-400 transition p-1 cursor-pointer"> <form method="POST" action="/users.php" class="inline" onsubmit="return confirm('Weet u zeker dat u deze uitnodiging wilt intrekken?')">
<input type="hidden" name="action" value="delete_invite">
<input type="hidden" name="invite_id" value="<?= (int)$inv['id'] ?>">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<button type="submit" title="Uitnodiging intrekken" class="text-slate-400 hover:text-red-600 dark:text-slate-500 dark:hover:text-red-400 transition p-1 cursor-pointer bg-transparent border-0">
<i data-lucide="trash-2" class="w-4 h-4 inline"></i> <i data-lucide="trash-2" class="w-4 h-4 inline"></i>
</a> </button>
</form>
</div> </div>
</td> </td>
</tr> </tr>