Implement CSRF protection for user invitation and management actions; add QR code generation script; secure internal includes directory with .htaccess

This commit is contained in:
2026-09-11 14:46:08 +02:00
parent 55618ffaed
commit cf0ceeca82
18 changed files with 413 additions and 163 deletions
+58 -52
View File
@@ -25,7 +25,11 @@ if (!empty($_GET['success'])) {
// POST Verwerking VÓÓR headers zodat redirects en mutaties soepel verlopen
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
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'] ?? '';
// 1. Outlook koppelen via OAuth
if ($action === 'connect_outlook') {
@@ -79,7 +83,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: [];
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'] ?? '';
@@ -104,7 +108,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$newSettings = [
'jira_domain' => $jiraDomain,
'jira_email' => $jiraEmail,
'jira_api_token' => $jiraToken,
'jira_api_token' => fociz_encrypt($jiraToken),
'account_name' => $accountName,
'account_id' => $accountId,
];
@@ -265,6 +269,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message = "Uw avatar is teruggezet naar de standaard weergave met uw initialen.";
$messageType = 'success';
}
}
}
// Actuele gegevens ophalen
@@ -524,6 +529,7 @@ require_once __DIR__ . '/includes/header.php';
<?php if (!empty($userProfile['avatar_url'])): ?>
<form method="POST" action="/profile.php" class="self-start sm:self-auto">
<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">
<i data-lucide="rotate-ccw" class="w-3.5 h-3.5"></i>
<span>Standaard Herstellen</span>
@@ -532,61 +538,51 @@ require_once __DIR__ . '/includes/header.php';
<?php endif; ?>
</div>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- Linker kolom: 24 Standaard Preset Avatars (8 cols) -->
<div class="lg:col-span-8 space-y-3">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<!-- Avatar Selector Grid & Upload -->
<div class="mt-6 grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
<!-- Linker kolom: 24 mini karakters in grid (8 cols) -->
<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">
<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>
<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>
<!-- Categorie Filter Pills -->
<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">
<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">
<?php
$avatarPresets = [
['file' => 'avatar-01.svg', 'cat' => 'emoji', 'name' => 'Cool Star (Zonnebril)'],
['file' => 'avatar-02.svg', 'cat' => 'emoji', 'name' => 'Party Champion'],
['file' => 'avatar-03.svg', 'cat' => 'emoji', 'name' => 'Celebration Vibe'],
['file' => 'avatar-04.svg', 'cat' => 'emoji', 'name' => 'Laser Eyes Energy'],
['file' => 'avatar-05.svg', 'cat' => 'emoji', 'name' => 'Sunny Smiles'],
['file' => 'avatar-06.svg', 'cat' => 'emoji', 'name' => 'Fire Heart'],
['file' => 'avatar-07.svg', 'cat' => 'bots', 'name' => 'Cyber Tron'],
['file' => 'avatar-08.svg', 'cat' => 'bots', 'name' => 'Aero Bot'],
['file' => 'avatar-09.svg', 'cat' => 'bots', 'name' => 'Neon Volt'],
['file' => 'avatar-10.svg', 'cat' => 'bots', 'name' => 'Pixel Pulse'],
['file' => 'avatar-11.svg', 'cat' => 'bots', 'name' => 'Quantum AI'],
['file' => 'avatar-12.svg', 'cat' => 'bots', 'name' => 'Robo Racer'],
['file' => 'avatar-13.svg', 'cat' => 'personas', 'name' => 'Alex (Hipster)'],
['file' => 'avatar-14.svg', 'cat' => 'personas', 'name' => 'Maya (Tech Lead)'],
['file' => 'avatar-15.svg', 'cat' => 'personas', 'name' => 'Lucas (Designer)'],
['file' => 'avatar-16.svg', 'cat' => 'personas', 'name' => 'Elena (Developer)'],
['file' => 'avatar-17.svg', 'cat' => 'personas', 'name' => 'Sam (Creative)'],
['file' => 'avatar-18.svg', 'cat' => 'personas', 'name' => 'Zoe (Founder)'],
['file' => 'avatar-19.svg', 'cat' => 'heroes', 'name' => 'Jack (Adventurer)'],
['file' => 'avatar-20.svg', 'cat' => 'heroes', 'name' => 'Luna (Heroine)'],
['file' => 'avatar-21.svg', 'cat' => 'heroes', 'name' => 'Oliver (Explorer)'],
['file' => 'avatar-22.svg', 'cat' => 'heroes', 'name' => 'Mia (Rebel)'],
['file' => 'avatar-01.svg', 'cat' => 'pro', 'name' => 'Alex (Business)'],
['file' => 'avatar-02.svg', 'cat' => 'pro', 'name' => 'Sophie (Corporate)'],
['file' => 'avatar-03.svg', 'cat' => 'pro', 'name' => 'Liam (Consultant)'],
['file' => 'avatar-04.svg', 'cat' => 'pro', 'name' => 'Emma (Manager)'],
['file' => 'avatar-05.svg', 'cat' => 'pro', 'name' => 'Daan (Director)'],
['file' => 'avatar-06.svg', 'cat' => 'pro', 'name' => 'Noa (Executive)'],
['file' => 'avatar-07.svg', 'cat' => 'creative', 'name' => 'Mila (Designer)'],
['file' => 'avatar-08.svg', 'cat' => 'creative', 'name' => 'Lucas (Creator)'],
['file' => 'avatar-09.svg', 'cat' => 'creative', 'name' => 'Zara (Artist)'],
['file' => 'avatar-10.svg', 'cat' => 'creative', 'name' => 'Finn (Illustrator)'],
['file' => 'avatar-11.svg', 'cat' => 'creative', 'name' => 'Lotte (Writer)'],
['file' => 'avatar-12.svg', 'cat' => 'creative', 'name' => 'Sem (Architect)'],
['file' => 'avatar-13.svg', 'cat' => 'tech', 'name' => 'Bram (Developer)'],
['file' => 'avatar-14.svg', 'cat' => 'tech', 'name' => 'Julia (Engineer)'],
['file' => 'avatar-15.svg', 'cat' => 'tech', 'name' => 'Jesse (DevOps)'],
['file' => 'avatar-16.svg', 'cat' => 'tech', 'name' => 'Tess (Data Scientist)'],
['file' => 'avatar-17.svg', 'cat' => 'tech', 'name' => 'Milan (Cyber Sec)'],
['file' => 'avatar-18.svg', 'cat' => 'tech', 'name' => 'Liv (AI Engineer)'],
['file' => 'avatar-19.svg', 'cat' => 'heroes', 'name' => 'Max (Ninja)'],
['file' => 'avatar-20.svg', 'cat' => 'heroes', 'name' => 'Sara (Astronaut)'],
['file' => 'avatar-21.svg', 'cat' => 'heroes', 'name' => 'Robin (Pilot)'],
['file' => 'avatar-22.svg', 'cat' => 'heroes', 'name' => 'Eva (Explorer)'],
['file' => 'avatar-23.svg', 'cat' => 'heroes', 'name' => 'Leo (Cosmic)'],
['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'] ?>">
<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 ?>">
<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">
@@ -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">
<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="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>
<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="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">
Ontkoppelen
</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">
<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">
<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>
@@ -752,6 +752,7 @@ require_once __DIR__ . '/includes/header.php';
<div>
<form method="POST">
<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">
<i data-lucide="log-in" class="w-4 h-4"></i>
<span>Koppel Mijn Outlook Agenda</span>
@@ -802,6 +803,7 @@ require_once __DIR__ . '/includes/header.php';
<?php if ($isJiraConnected): ?>
<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="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">
Jira Ontkoppelen
</button>
@@ -811,6 +813,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" class="mt-6 space-y-4">
<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>
@@ -840,7 +843,7 @@ require_once __DIR__ . '/includes/header.php';
</a>
</div>
<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">
<i data-lucide="eye" class="w-4 h-4" id="eyeJiraIcon"></i>
</button>
@@ -912,6 +915,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" class="space-y-4">
<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>
@@ -1021,6 +1025,7 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" class="space-y-4">
<input type="hidden" name="action" value="update_profile">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div>
<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">
<input type="hidden" name="action" value="change_password">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">