Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6060f39566 | |||
| cf0ceeca82 |
@@ -27,6 +27,10 @@ if (!empty($_GET['success'])) {
|
||||
|
||||
// POST Verwerking VÓÓR header output zodat redirects en opslaan vlekkeloos werken
|
||||
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'] ?? '';
|
||||
|
||||
// 1. Centrale Azure App Registratie opslaan
|
||||
@@ -194,7 +198,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
$currentAi = get_ai_settings();
|
||||
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);
|
||||
@@ -223,6 +227,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Haal actuele configuratie op
|
||||
@@ -425,6 +430,7 @@ require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
<form method="POST" class="mt-6 space-y-5">
|
||||
<input type="hidden" name="action" value="save_azure">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
||||
|
||||
<!-- 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">
|
||||
@@ -563,6 +569,7 @@ require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
<form method="POST" class="mt-6 space-y-4">
|
||||
<input type="hidden" name="action" value="save_jira_domain">
|
||||
<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">
|
||||
@@ -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">
|
||||
<input type="hidden" name="action" value="delete_jira_queue_filter">
|
||||
<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">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
<i data-lucide="trash-2" class="w-4 h-4 inline"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -724,6 +732,7 @@ require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
<form method="POST" action="/admin.php" class="space-y-4">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<!-- Werkdag uren -->
|
||||
@@ -993,6 +1004,7 @@ require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
<form method="POST" action="/admin.php" class="space-y-4">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<input
|
||||
type="email"
|
||||
|
||||
+12
-1
@@ -431,7 +431,9 @@ try {
|
||||
$targetLaneId = (int)($input['target_lane_id'] ?? 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) {
|
||||
echo json_encode(['success' => false, 'error' => 'Taak niet gevonden.']);
|
||||
exit;
|
||||
@@ -443,6 +445,15 @@ try {
|
||||
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->execute([$targetLaneId, $targetPosition, $cardId]);
|
||||
|
||||
|
||||
+40
-2
@@ -47,11 +47,25 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Stemmen / Upvote op kaart
|
||||
// 2. Stemmen / Upvote op kaart (met bescherming tegen oneindig stemmen)
|
||||
if ($action === 'vote_card') {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$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->execute([$cardId]);
|
||||
|
||||
@@ -63,11 +77,35 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. Kaart verwijderen
|
||||
// 3. Kaart verwijderen (met autorisatiecontrole: auteur, sessie-eigenaar of beheerder)
|
||||
if ($action === 'delete_card') {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$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->execute([$cardId]);
|
||||
|
||||
|
||||
@@ -9,15 +9,19 @@ require_once __DIR__ . '/../../config/database.php';
|
||||
|
||||
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', '');
|
||||
if (!empty($configuredSecret)) {
|
||||
$incomingSecret = $_GET['secret'] ?? ($_SERVER['HTTP_X_ATLASSIAN_WEBHOOK_SECRET'] ?? '');
|
||||
if (!hash_equals($configuredSecret, $incomingSecret)) {
|
||||
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'] ?? '');
|
||||
if (empty($incomingSecret) || !hash_equals($configuredSecret, $incomingSecret)) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Onbevoegde webhook aanroep']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$rawPayload = file_get_contents('php://input');
|
||||
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -18,11 +18,24 @@ $error = null;
|
||||
$submitted = false;
|
||||
|
||||
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.";
|
||||
} else {
|
||||
fociz_hit_rate_limit($ipRateKey, 900);
|
||||
fociz_hit_rate_limit($emailRateKey, 900);
|
||||
try {
|
||||
$db = get_db_connection();
|
||||
|
||||
@@ -176,6 +189,7 @@ $pageTitle = "Fociz — Wachtwoord Vergeten";
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<form method="POST" action="/auth/forgot-password.php" class="space-y-4">
|
||||
<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">Uw e-mailadres</label>
|
||||
<div class="relative">
|
||||
|
||||
+13
-1
@@ -191,7 +191,7 @@ $pageTitle = "Fociz — Uitnodiging Accepteren";
|
||||
</p>
|
||||
|
||||
<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 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>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/qrcode.min.js"></script>
|
||||
<script>
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+31
-5
@@ -42,13 +42,24 @@ if (isset($_GET['action']) && $_GET['action'] === 'restart') {
|
||||
// POST Verwerking
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$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
|
||||
if ($step === 1) {
|
||||
$email = strtolower(trim($_POST['email'] ?? ''));
|
||||
$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.";
|
||||
} else {
|
||||
$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();
|
||||
|
||||
if ($user && password_verify($password, $user['password_hash'])) {
|
||||
fociz_clear_rate_limit($emailRateKey);
|
||||
// Controleer of de inloglocatie/IP bekend is
|
||||
$clientIp = fociz_get_client_ip();
|
||||
$isIpKnown = fociz_is_ip_known_for_user((int)$user['id'], $clientIp);
|
||||
|
||||
if (!$isIpKnown) {
|
||||
@@ -72,6 +83,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$step = 2;
|
||||
}
|
||||
} else {
|
||||
fociz_hit_rate_limit($ipRateKey, 900);
|
||||
if (!empty($email)) {
|
||||
fociz_hit_rate_limit($emailRateKey, 900);
|
||||
}
|
||||
$error = "Onjuist e-mailadres of wachtwoord.";
|
||||
}
|
||||
}
|
||||
@@ -86,7 +101,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
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.";
|
||||
} else {
|
||||
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
|
||||
@@ -120,8 +140,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
if ($isValid) {
|
||||
fociz_clear_rate_limit($totpRateKey);
|
||||
fociz_clear_rate_limit($ipRateKey);
|
||||
|
||||
// Volledige login succesvol (geldig tot 23:59:59 van vandaag)
|
||||
$clientIp = fociz_get_client_ip();
|
||||
fociz_register_user_ip((int)$user['id'], $clientIp, true);
|
||||
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
@@ -142,6 +164,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
header('Location: /planner.php');
|
||||
exit;
|
||||
} else {
|
||||
fociz_hit_rate_limit($totpRateKey, 900);
|
||||
fociz_hit_rate_limit($ipRateKey, 900);
|
||||
$error = "Onjuiste 2FA code. Controleer uw authenticator app en probeer opnieuw.";
|
||||
}
|
||||
}
|
||||
@@ -229,6 +253,7 @@ $pageTitle = "Fociz — Inloggen";
|
||||
<?php if ($step === 1): ?>
|
||||
<!-- STAP 1: E-mail & Wachtwoord -->
|
||||
<form method="POST" action="/auth/login.php" class="space-y-4">
|
||||
<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">E-mailadres</label>
|
||||
<div class="relative">
|
||||
@@ -256,6 +281,7 @@ $pageTitle = "Fociz — Inloggen";
|
||||
|
||||
<!-- STAP 2: 2FA TOTP Code -->
|
||||
<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="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>
|
||||
|
||||
+13
-1
@@ -214,7 +214,7 @@ $pageTitle = "Fociz — Eerste Initialisatie";
|
||||
</p>
|
||||
|
||||
<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 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; ?>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/qrcode.min.js"></script>
|
||||
<script>
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+4
-6
@@ -17,6 +17,7 @@ if (!headers_sent()) {
|
||||
header('X-XSS-Protection: 1; mode=block');
|
||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
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') {
|
||||
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_NAME', fociz_env('DB_NAME', $isDevEnv ? 'focizdev' : 'fociz'));
|
||||
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');
|
||||
|
||||
// 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_PORT', (int)fociz_env('SMTP_PORT', 587));
|
||||
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_FROM_EMAIL', fociz_env('SMTP_FROM_EMAIL', 'info@fociz.nl'));
|
||||
define('SMTP_FROM_NAME', fociz_env('SMTP_FROM_NAME', 'Fociz'));
|
||||
@@ -156,10 +157,7 @@ if (APP_ENV === 'live') {
|
||||
$error = error_get_last();
|
||||
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']}");
|
||||
$showDiagnostics = (
|
||||
(isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'ADMIN') ||
|
||||
(isset($_GET['debug']) && $_GET['debug'] === 'fociz')
|
||||
);
|
||||
$showDiagnostics = (isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'ADMIN');
|
||||
if ($showDiagnostics) {
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -568,6 +568,55 @@ function verify_csrf_token(?string $token): bool {
|
||||
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.
|
||||
* Dit voorkomt CSRF en maakt sessieherstel mogelijk bij cross-site OAuth redirects.
|
||||
|
||||
@@ -9,14 +9,41 @@ require_once __DIR__ . '/../../config/database.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 {
|
||||
$domain = trim($domain);
|
||||
if (empty($domain)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!preg_match('#^https?://#i', $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
@@ -35,15 +35,26 @@ class FocizMailer {
|
||||
$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
|
||||
$host = $options['host'] ?? (defined('SMTP_HOST') ? SMTP_HOST : 'mail.fociz.nl');
|
||||
$port = (int)($options['port'] ?? (defined('SMTP_PORT') ? SMTP_PORT : 587));
|
||||
$username = $options['username'] ?? (defined('SMTP_USER') ? SMTP_USER : 'info@fociz.nl');
|
||||
$password = $options['password'] ?? (defined('SMTP_PASS') ? SMTP_PASS : '');
|
||||
$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');
|
||||
$fromName = $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');
|
||||
$fromEmail = str_replace(["\r", "\n", "\0"], '', trim($options['from_email'] ?? (defined('SMTP_FROM_EMAIL') ? SMTP_FROM_EMAIL : 'no-reply@fociz.nl')));
|
||||
$fromName = str_replace(["\r", "\n", "\0"], '', trim($options['from_name'] ?? (defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'Fociz')));
|
||||
$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);
|
||||
|
||||
// Technische afzender voor de SMTP envelope (moet doorgaans overeenkomen met het geauthenticeerde account)
|
||||
|
||||
@@ -110,7 +110,14 @@ function get_workday_settings(): array {
|
||||
*/
|
||||
function get_ai_settings(): array {
|
||||
$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;
|
||||
|
||||
return [
|
||||
|
||||
+4
-6
@@ -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 {
|
||||
return sprintf(
|
||||
'https://quickchart.io/qr?text=%s&size=%d&margin=1&ecLevel=M',
|
||||
urlencode($otpAuthUrl),
|
||||
$size
|
||||
);
|
||||
// Externe API (quickchart.io) verwijderd om te voorkomen dat 2FA secrets uitlekken naar derden.
|
||||
// QR rendering gebeurt nu 100% lokaal via /assets/js/qrcode.min.js in de browser.
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+57
-51
@@ -25,6 +25,10 @@ if (!empty($_GET['success'])) {
|
||||
|
||||
// POST Verwerking VÓÓR headers zodat redirects en mutaties soepel verlopen
|
||||
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'] ?? '';
|
||||
|
||||
// 1. Outlook koppelen via OAuth
|
||||
@@ -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">
|
||||
|
||||
@@ -13,8 +13,14 @@ $message = null;
|
||||
$generatedLink = null;
|
||||
$error = null;
|
||||
|
||||
// Uitnodiging aanmaken
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_invite') {
|
||||
// POST acties afhandelen met CSRF-bescherming
|
||||
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'] ?? ''));
|
||||
$role = in_array($_POST['role'] ?? '', ['ADMIN', 'MEMBER']) ? $_POST['role'] : 'MEMBER';
|
||||
|
||||
@@ -40,11 +46,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Uitnodiging opnieuw verzenden per e-mail
|
||||
if (isset($_GET['resend_invite'])) {
|
||||
$inviteId = (int)$_GET['resend_invite'];
|
||||
} elseif ($action === 'resend_invite') {
|
||||
$inviteId = (int)($_POST['invite_id'] ?? 0);
|
||||
$stmtInv = $db->prepare("SELECT * FROM invites WHERE id = ? AND used_at IS NULL AND expires_at > NOW()");
|
||||
$stmtInv->execute([$inviteId]);
|
||||
$inv = $stmtInv->fetch();
|
||||
@@ -60,26 +63,21 @@ if (isset($_GET['resend_invite'])) {
|
||||
} else {
|
||||
$error = "Uitnodiging niet gevonden of reeds verlopen/geactiveerd.";
|
||||
}
|
||||
}
|
||||
|
||||
// Uitnodiging intrekken
|
||||
if (isset($_GET['delete_invite'])) {
|
||||
$inviteId = (int)$_GET['delete_invite'];
|
||||
} elseif ($action === 'delete_invite') {
|
||||
$inviteId = (int)($_POST['invite_id'] ?? 0);
|
||||
$stmtDel = $db->prepare("DELETE FROM invites WHERE id = ?");
|
||||
$stmtDel->execute([$inviteId]);
|
||||
header('Location: /users.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Gebruiker verwijderen
|
||||
if (isset($_GET['delete_user'])) {
|
||||
$delUserId = (int)$_GET['delete_user'];
|
||||
} elseif ($action === 'delete_user') {
|
||||
$delUserId = (int)($_POST['user_id'] ?? 0);
|
||||
if ($delUserId !== (int)$adminUser['id']) {
|
||||
$stmtDelUser = $db->prepare("DELETE FROM users WHERE id = ?");
|
||||
$stmtDelUser->execute([$delUserId]);
|
||||
}
|
||||
header('Location: /users.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Haal alle gebruikers op
|
||||
@@ -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">
|
||||
<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">
|
||||
<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 class="p-3 text-right">
|
||||
<?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>
|
||||
</a>
|
||||
</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<span class="text-[10px] text-slate-400 dark:text-slate-600 font-medium">Uzelf</span>
|
||||
<?php endif; ?>
|
||||
@@ -295,14 +299,24 @@ require_once __DIR__ . '/includes/header.php';
|
||||
<td class="p-3 text-right">
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
<?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>
|
||||
<span>Opnieuw mailen</span>
|
||||
</a>
|
||||
</button>
|
||||
</form>
|
||||
<?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>
|
||||
</a>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user