Files
Fociz/users.php
T
2026-09-12 22:28:03 +02:00

363 lines
22 KiB
PHP

<?php
/**
* Fociz Team & Uitnodigingen Beheer (Enkel voor Admin)
*/
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/mailer.php';
$adminUser = require_admin();
$currentUser = $adminUser;
$db = get_db_connection();
$message = null;
$generatedLink = null;
$error = null;
// 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';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error = "Voer een geldig e-mailadres in.";
} else {
// Controleer of e-mail al bestaat
$stmtCheck = $db->prepare("SELECT id FROM users WHERE email = ?");
$stmtCheck->execute([$email]);
if ($stmtCheck->fetch()) {
$error = "Er bestaat al een gebruiker met dit e-mailadres.";
} else {
$token = create_invite($adminUser['id'], $email, $role);
$baseUrl = rtrim(APP_URL, '/');
$generatedLink = "{$baseUrl}/auth/invite.php?token={$token}";
// Verstuur de gestylede uitnodigingsmail direct via SMTP
$mailResult = FocizMailer::sendInvitationEmail($email, $adminUser['name'], $role, $generatedLink);
fociz_log_audit((int)$adminUser['id'], 'USER_INVITED', 'invite', null, ['email' => $email, 'role' => $role], 'SUCCESS', $adminUser['email']);
if ($mailResult['success']) {
$message = "Uitnodigingslink succesvol aangemaakt én per e-mail verzonden naar {$email}!";
} else {
$message = "Uitnodiging aangemaakt! Let op: de e-mail kon niet worden verzonden (" . htmlspecialchars($mailResult['error'] ?? 'serverfout') . "). U kunt de link handmatig kopiëren.";
}
}
}
} 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();
if ($inv) {
$baseUrl = rtrim(APP_URL, '/');
$link = "{$baseUrl}/auth/invite.php?token={$inv['token']}";
$mailResult = FocizMailer::sendInvitationEmail($inv['email'], $adminUser['name'], $inv['role'], $link);
fociz_log_audit((int)$adminUser['id'], 'INVITE_RESENT', 'invite', $inviteId, ['email' => $inv['email'], 'role' => $inv['role']], 'SUCCESS', $adminUser['email']);
if ($mailResult['success']) {
$message = "Uitnodiging succesvol opnieuw per e-mail verzonden naar {$inv['email']}!";
} else {
$error = "Fout bij opnieuw verzenden van uitnodiging: " . ($mailResult['error'] ?? 'Onbekende fout');
}
} else {
$error = "Uitnodiging niet gevonden of reeds verlopen/geactiveerd.";
}
} elseif ($action === 'delete_invite') {
$inviteId = (int)($_POST['invite_id'] ?? 0);
$stmtDel = $db->prepare("DELETE FROM invites WHERE id = ?");
$stmtDel->execute([$inviteId]);
fociz_log_audit((int)$adminUser['id'], 'INVITE_REVOKED', 'invite', $inviteId, [], 'SUCCESS', $adminUser['email']);
header('Location: /users.php');
exit;
} elseif ($action === 'delete_user') {
$delUserId = (int)($_POST['user_id'] ?? 0);
if ($delUserId !== (int)$adminUser['id']) {
$stmtUserToDel = $db->prepare("SELECT email, name FROM users WHERE id = ?");
$stmtUserToDel->execute([$delUserId]);
$delUser = $stmtUserToDel->fetch();
$stmtDelUser = $db->prepare("DELETE FROM users WHERE id = ?");
$stmtDelUser->execute([$delUserId]);
try {
$db->prepare("DELETE FROM user_sessions WHERE user_id = ?")->execute([$delUserId]);
} catch (Throwable $e) {}
fociz_log_audit((int)$adminUser['id'], 'USER_DELETED', 'user', $delUserId, ['deleted_email' => $delUser['email'] ?? null, 'deleted_name' => $delUser['name'] ?? null], 'SUCCESS', $adminUser['email']);
}
header('Location: /users.php');
exit;
}
}
// Haal alle gebruikers op
$users = [];
try {
$users = $db->query("SELECT * FROM users ORDER BY created_at DESC")->fetchAll() ?: [];
} catch (Throwable $e) {
error_log("Fout bij ophalen gebruikers: " . $e->getMessage());
}
// Haal alle uitnodigingen op
$invites = [];
try {
$invites = $db->query("
SELECT i.*, u.name as inviter_name
FROM invites i
JOIN users u ON i.created_by_id = u.id
ORDER BY i.created_at DESC
")->fetchAll() ?: [];
} catch (Throwable $e) {
error_log("Fout bij ophalen uitnodigingen: " . $e->getMessage());
ensure_tables_exist($db);
}
$pageTitle = 'Team & Uitnodigingen — Fociz';
require_once __DIR__ . '/includes/header.php';
?>
<div class="space-y-6 max-w-7xl mx-auto pb-12">
<!-- Header -->
<div class="fociz-card p-6 sm:p-7">
<div class="flex items-center space-x-2 text-indigo-600 dark:text-indigo-400 text-xs font-bold uppercase tracking-wider mb-1">
<i data-lucide="shield" class="w-4 h-4"></i>
<span>Beheerderspaneel</span>
</div>
<h1 class="text-2xl sm:text-3xl font-black text-slate-900 dark:text-white tracking-tight">
Team & Uitnodigingen
</h1>
<p class="text-slate-500 dark:text-slate-400 text-xs sm:text-sm mt-1 max-w-2xl">
Beheer collega's, rollen en uitnodigingslinks voor toegang tot de Fociz omgeving.
</p>
</div>
<?php if ($error): ?>
<div class="p-4 rounded-2xl bg-red-500/10 border border-red-500/30 text-red-700 dark:text-red-400 text-xs font-semibold flex items-center space-x-2">
<i data-lucide="alert-triangle" class="w-4 h-4"></i>
<span><?= htmlspecialchars($error) ?></span>
</div>
<?php endif; ?>
<?php if ($message && $generatedLink): ?>
<div class="p-5 rounded-2xl bg-teal-50/80 dark:bg-teal-950/30 border border-teal-200 dark:border-teal-800/60 text-slate-900 dark:text-white space-y-2 shadow-xs transition">
<div class="flex items-center space-x-2 text-teal-700 dark:text-teal-400 font-bold text-xs">
<i data-lucide="check-circle-2" class="w-4 h-4"></i>
<span><?= htmlspecialchars($message) ?></span>
</div>
<p class="text-xs text-slate-600 dark:text-slate-300">
Kopieer onderstaande beveiligde registratielink en stuur deze naar de collega:
</p>
<div class="flex items-center space-x-2 bg-white dark:bg-[#0c0f17] p-2.5 rounded-xl border border-teal-200 dark:border-slate-800">
<input type="text" readonly id="inviteLinkInput" value="<?= htmlspecialchars($generatedLink) ?>" class="bg-transparent text-teal-700 dark:text-teal-300 font-mono text-xs flex-1 outline-none">
<button onclick="copyInviteLink()" class="px-3 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs rounded-xl transition flex items-center space-x-1 cursor-pointer">
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
<span id="copyBtnText">Kopiëren</span>
</button>
</div>
</div>
<?php endif; ?>
<!-- Uitnodiging Genereren Kaart -->
<div class="fociz-card p-6 sm:p-7">
<h3 class="text-base font-bold text-slate-900 dark:text-white mb-4 flex items-center space-x-2">
<i data-lucide="user-plus" class="w-5 h-5 text-indigo-600 dark:text-indigo-400"></i>
<span>Nieuwe Collega Uitnodigen</span>
</h3>
<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>
<input type="email" name="email" required placeholder="collega@bedrijf.nl" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-4 py-2.5 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-xs shadow-2xs">
</div>
<div class="md:col-span-3">
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Rol</label>
<select name="role" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2.5 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
<option value="MEMBER">Collega (Standaard)</option>
<option value="ADMIN">Mede-beheerder</option>
</select>
</div>
<div class="md:col-span-3">
<button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-2.5 px-4 rounded-xl text-xs transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer">
<i data-lucide="link" class="w-4 h-4"></i>
<span>Genereer Link</span>
</button>
</div>
</form>
</div>
<!-- Actieve Gebruikers Tabel -->
<div class="fociz-card p-6 sm:p-7 space-y-4">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
<h3 class="text-sm font-bold text-slate-900 dark:text-white uppercase tracking-wider flex items-center space-x-2">
<i data-lucide="users" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
<span>Geregistreerde Teamleden (<?= count($users) ?>)</span>
</h3>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left text-xs text-slate-700 dark:text-slate-300">
<thead class="bg-slate-50 dark:bg-[#0c0f17] text-slate-500 dark:text-slate-400 uppercase text-[10px] tracking-wider border-b border-slate-200 dark:border-slate-800">
<tr>
<th class="p-3">Naam & E-mail</th>
<th class="p-3">Rol</th>
<th class="p-3">2FA Beveiliging</th>
<th class="p-3">Lid sinds</th>
<th class="p-3 text-right">Acties</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-800/60">
<?php foreach ($users as $u): ?>
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/30 transition">
<td class="p-3">
<div class="flex items-center space-x-2.5">
<div class="w-8 h-8 rounded-xl bg-gradient-to-tr from-indigo-600 to-sky-500 p-0.5 flex-shrink-0 overflow-hidden shadow-2xs">
<?php if (!empty($u['avatar_url'])): ?>
<img src="<?= htmlspecialchars($u['avatar_url']) ?>" alt="Avatar" class="w-full h-full rounded-[10px] object-cover bg-white dark:bg-slate-900" onerror="this.classList.add('hidden'); if(this.nextElementSibling) this.nextElementSibling.classList.remove('hidden');">
<div class="hidden w-full h-full rounded-[10px] bg-white dark:bg-slate-900 flex items-center justify-center font-bold text-xs text-indigo-600 dark:text-indigo-400 uppercase">
<?= strtoupper(substr($u['name'], 0, 1)) ?>
</div>
<?php else: ?>
<div class="w-full h-full rounded-[10px] bg-white dark:bg-slate-900 flex items-center justify-center font-bold text-xs text-indigo-600 dark:text-indigo-400 uppercase">
<?= strtoupper(substr($u['name'], 0, 1)) ?>
</div>
<?php endif; ?>
</div>
<div>
<div class="font-bold text-slate-900 dark:text-white"><?= htmlspecialchars($u['name']) ?></div>
<div class="text-[11px] text-slate-500 dark:text-slate-400 font-mono"><?= htmlspecialchars($u['email']) ?></div>
</div>
</div>
</td>
<td class="p-3">
<span class="px-2 py-0.5 rounded text-[10px] font-bold <?= $u['role'] === 'ADMIN' ? 'bg-indigo-50 dark:bg-indigo-950/40 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/60' : 'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300' ?>">
<?= $u['role'] === 'ADMIN' ? 'Beheerder' : 'Collega' ?>
</span>
</td>
<td class="p-3">
<span class="px-2 py-0.5 rounded-full bg-teal-50 dark:bg-teal-950/30 text-teal-700 dark:text-teal-400 border border-teal-200 dark:border-teal-800/60 text-[10px] font-medium flex items-center space-x-1 w-max">
<i data-lucide="shield-check" class="w-3 h-3"></i>
<span>2FA Actief</span>
</span>
</td>
<td class="p-3 text-slate-500 dark:text-slate-400">
<?= date('d-m-Y', strtotime($u['created_at'])) ?>
</td>
<td class="p-3 text-right">
<?php if ($u['id'] !== $adminUser['id']): ?>
<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>
</button>
</form>
<?php else: ?>
<span class="text-[10px] text-slate-400 dark:text-slate-600 font-medium">Uzelf</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Uitstaande Uitnodigingen Tabel -->
<div class="fociz-card p-6 sm:p-7 space-y-4">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
<h3 class="text-sm font-bold text-slate-900 dark:text-white uppercase tracking-wider flex items-center space-x-2">
<i data-lucide="mail" class="w-4 h-4 text-amber-500"></i>
<span>Uitnodigingen Status (<?= count($invites) ?>)</span>
</h3>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left text-xs text-slate-700 dark:text-slate-300">
<thead class="bg-slate-50 dark:bg-[#0c0f17] text-slate-500 dark:text-slate-400 uppercase text-[10px] tracking-wider border-b border-slate-200 dark:border-slate-800">
<tr>
<th class="p-3">Uitgenodigd E-mailadres</th>
<th class="p-3">Rol</th>
<th class="p-3">Status</th>
<th class="p-3">Verloopt op</th>
<th class="p-3 text-right">Actie</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-800/60">
<?php if (empty($invites)): ?>
<tr><td colspan="5" class="p-4 text-center text-slate-400 dark:text-slate-500">Nog geen uitnodigingen verstuurd.</td></tr>
<?php else: ?>
<?php foreach ($invites as $inv):
$isUsed = !empty($inv['used_at']);
$isExpired = strtotime($inv['expires_at']) < time();
?>
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/30 transition">
<td class="p-3 font-mono text-slate-900 dark:text-white"><?= htmlspecialchars($inv['email']) ?></td>
<td class="p-3">
<span class="px-2 py-0.5 rounded text-[10px] font-bold <?= $inv['role'] === 'ADMIN' ? 'bg-indigo-50 dark:bg-indigo-950/40 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/60' : 'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300' ?>">
<?= $inv['role'] === 'ADMIN' ? 'Beheerder' : 'Collega' ?>
</span>
</td>
<td class="p-3">
<?php if ($isUsed): ?>
<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-teal-50 dark:bg-teal-950/30 text-teal-700 dark:text-teal-400 border border-teal-200 dark:border-teal-800/60">Geaccepteerd</span>
<?php elseif ($isExpired): ?>
<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-red-500/15 text-red-700 dark:text-red-400 border border-red-500/30">Verlopen</span>
<?php else: ?>
<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-amber-500/15 text-amber-700 dark:text-amber-400 border border-amber-500/30">In afwachting</span>
<?php endif; ?>
</td>
<td class="p-3 text-slate-500 dark:text-slate-400"><?= date('d-m-Y H:i', strtotime($inv['expires_at'])) ?></td>
<td class="p-3 text-right">
<div class="flex items-center justify-end space-x-2">
<?php if (!$isUsed && !$isExpired): ?>
<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>
</button>
</form>
<?php endif; ?>
<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>
</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<script>
function copyInviteLink() {
const input = document.getElementById('inviteLinkInput');
input.select();
navigator.clipboard.writeText(input.value);
document.getElementById('copyBtnText').innerText = 'Gekopieerd!';
setTimeout(() => {
document.getElementById('copyBtnText').innerText = 'Kopiëren';
}, 2000);
}
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>