Files
anjuma-dashboard/users.php
T

458 lines
15 KiB
PHP

<?php
require_once "config/session.php";
require_once "config/config.php";
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
header("location: index.php");
exit;
}
if(($_SESSION["role"] ?? "user") !== "admin"){
header("location: dashboard.php"); exit;
}
// CSRF Token (für öffentliche Seite: MUSS)
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
include('navbar.php');
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Users • ANJUMA Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.8/css/dataTables.bootstrap5.min.css" rel="stylesheet">
</head>
<body>
<div class="container my-4">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
<div>
<h3 class="mb-0">Users</h3>
<div class="text-muted small">Benutzer verwalten (Admin-only)</div>
</div>
<button class="btn btn-primary" id="btnNewUser">
<i class="bi bi-person-plus"></i> Neuer User
</button>
</div>
<div class="card shadow-sm border-0">
<div class="card-body">
<!-- Mobile Cards -->
<div id="mobileCards" class="d-md-none"></div>
<!-- Desktop Table -->
<div class="table-responsive d-none d-md-block">
<table id="tblUsers" class="table table-hover align-middle w-100 mb-0">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Username</th>
<th>Role</th>
<th>First Login</th>
<th>Last Login</th>
<th style="width:260px;">Aktion</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1080;">
<div id="appToast" class="toast align-items-center text-bg-dark border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body" id="toastBody">…</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
</div>
<!-- User Modal -->
<div class="modal fade" id="userModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form class="modal-content" id="userForm">
<div class="modal-header">
<h5 class="modal-title" id="userModalTitle">User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="u_id" value="">
<div class="mb-2">
<label class="form-label mb-1">Name *</label>
<input class="form-control" id="u_name" required maxlength="32" placeholder="z.B. Justin">
</div>
<div class="mb-2">
<label class="form-label mb-1">Username *</label>
<input class="form-control" id="u_username" required maxlength="32" placeholder="z.B. justin">
</div>
<div class="mb-2">
<label class="form-label mb-1">Role</label>
<select class="form-select" id="u_role">
<option value="user">user</option>
<option value="admin">admin</option>
</select>
</div>
<div class="mb-2">
<label class="form-label mb-1">Passwort (optional)</label>
<input class="form-control" id="u_password" type="password" placeholder="leer lassen = First Login setzt User selbst">
<div class="text-muted small mt-1">
Wenn leer: <b>first_login=1</b> und User setzt sein Passwort beim ersten Login.
</div>
</div>
<div class="alert alert-secondary small mb-0">
Tipp: Passwort resetten geht auch über den Button in der Liste.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-success" id="btnSaveUser">
<i class="bi bi-check2"></i> Speichern
</button>
</div>
</form>
</div>
</div>
<!-- Confirm Delete Modal -->
<div class="modal fade" id="confirmDeleteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title">User löschen?</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div>Willst du diesen User wirklich löschen?</div>
<div class="text-muted small mt-2" id="delInfo"></div>
</div>
<div class="modal-footer">
<button class="btn btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button class="btn btn-danger" id="btnConfirmDelete"><i class="bi bi-trash"></i> Löschen</button>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/datatables.net@1.13.8/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.8/js/dataTables.bootstrap5.min.js"></script>
<script>
const API_LIST = '/api/users_list.php';
const API_SAVE = '/api/users_save.php';
const API_DEL = '/api/users_delete.php';
const API_RESET = '/api/users_reset_pw.php';
const API_LOGOUTALL = '/api/users_logout_all.php';
const CSRF = <?= json_encode($_SESSION['csrf']) ?>;
const meId = <?= (int)($_SESSION['id'] ?? 0) ?>;
const userModal = new bootstrap.Modal(document.getElementById('userModal'));
const delModal = new bootstrap.Modal(document.getElementById('confirmDeleteModal'));
const toastEl = document.getElementById('appToast');
const toastBody = document.getElementById('toastBody');
const toast = new bootstrap.Toast(toastEl, { delay: 2600 });
function showToast(msg){
toastBody.textContent = msg;
toast.show();
}
let dt = null;
let deleteId = null;
let deleteLabel = '';
let cacheUsers = []; // ✅ kein reload bei edit click
function fmtTs(ts){
ts = parseInt(ts || 0, 10);
if (!ts) return '—';
const d = new Date(ts * 1000);
const pad = n => String(n).padStart(2,'0');
return `${pad(d.getDate())}.${pad(d.getMonth()+1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
function roleBadge(role){
if (role === 'admin') return '<span class="badge bg-dark">admin</span>';
return '<span class="badge bg-secondary">user</span>';
}
function yesNoBadge(v){
return String(v) === '1'
? '<span class="badge bg-warning text-dark">yes</span>'
: '<span class="badge bg-success">no</span>';
}
function actionsHtml(u){
const disableSelf = (parseInt(u.id,10) === meId);
return `
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-primary actEdit" data-id="${u.id}" title="Bearbeiten">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-outline-warning actReset" data-id="${u.id}" ${disableSelf ? 'disabled title="Nicht für dich selbst"' : 'title="Passwort resetten"'}>
<i class="bi bi-key"></i>
</button>
<button class="btn btn-outline-secondary actLogoutAll" data-id="${u.id}" ${disableSelf ? 'disabled title="Nicht für dich selbst"' : 'title="Alle Geräte abmelden"'}>
<i class="bi bi-box-arrow-right"></i>
</button>
<button class="btn btn-outline-danger actDel" data-id="${u.id}" data-label="${u.name} (${u.username})"
${disableSelf ? 'disabled title="Du kannst dich nicht selbst löschen"' : 'title="Löschen"'}>
<i class="bi bi-trash"></i>
</button>
</div>
`;
}
async function apiPost(url, formData){
formData.append('csrf', CSRF);
const res = await fetch(url, { method:'POST', body: formData });
const json = await res.json().catch(() => ({}));
if (!json || json.ok !== true) throw new Error(json?.error || 'Request failed');
return json;
}
async function loadUsers(){
const res = await fetch(API_LIST);
const json = await res.json();
if (!json.ok) throw new Error(json.error || 'Load failed');
return json.items || [];
}
function renderMobileCards(items){
const wrap = document.getElementById('mobileCards');
if (!wrap) return;
if (!items.length){
wrap.innerHTML = `<div class="text-muted">Keine User gefunden.</div>`;
return;
}
wrap.innerHTML = items.map(u => `
<div class="card mb-2">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="fw-bold">${u.name}</div>
<div class="text-muted small">@${u.username}</div>
</div>
<div>${roleBadge(u.role)}</div>
</div>
<div class="mt-2 small">
<div>First Login: ${yesNoBadge(u.first_login)}</div>
<div>Last Login: <span class="text-muted">${fmtTs(u.last_login)}</span></div>
</div>
<div class="mt-3">
${actionsHtml(u)}
</div>
</div>
</div>
`).join('');
}
function initDataTable(items){
if (dt) {
dt.clear().rows.add(items).draw();
return;
}
dt = $('#tblUsers').DataTable({
data: items,
pageLength: 10,
responsive: false,
dom: '<"row align-items-center mb-2"<"col-md-6"l><"col-md-6 text-md-end"f>>rt<"row align-items-center mt-2"<"col-md-6"i><"col-md-6 text-md-end"p>>',
columns: [
{ data: 'name' },
{ data: 'username', render: (d) => '@' + d },
{ data: 'role', render: (d) => roleBadge(d) },
{ data: 'first_login', render: (d) => yesNoBadge(d) },
{ data: 'last_login', render: (d) => `<span class="text-muted">${fmtTs(d)}</span>` },
{ data: null, orderable: false, render: (row) => actionsHtml(row) },
],
language: {
search: "Suche:",
lengthMenu: "_MENU_ pro Seite",
info: "_START_ bis _END_ von _TOTAL_",
paginate: { previous: "Zurück", next: "Weiter" },
emptyTable: "Keine Daten"
}
});
}
async function refresh(){
cacheUsers = await loadUsers(); // ✅ einmal holen, dann lokal nutzen
renderMobileCards(cacheUsers);
initDataTable(cacheUsers);
}
function openNew(){
document.getElementById('userModalTitle').textContent = 'Neuer User';
document.getElementById('u_id').value = '';
document.getElementById('u_name').value = '';
document.getElementById('u_username').value = '';
document.getElementById('u_role').value = 'user';
document.getElementById('u_password').value = '';
userModal.show();
}
function openEdit(u){
document.getElementById('userModalTitle').textContent = 'User bearbeiten';
document.getElementById('u_id').value = u.id;
document.getElementById('u_name').value = u.name;
document.getElementById('u_username').value = u.username;
document.getElementById('u_role').value = u.role;
document.getElementById('u_password').value = '';
userModal.show();
}
document.getElementById('btnNewUser').addEventListener('click', openNew);
document.getElementById('userForm').addEventListener('submit', async (ev) => {
ev.preventDefault();
const id = document.getElementById('u_id').value.trim();
const name = document.getElementById('u_name').value.trim();
const username = document.getElementById('u_username').value.trim();
const role = document.getElementById('u_role').value;
const password = document.getElementById('u_password').value;
const fd = new FormData();
if (id) fd.append('id', id);
fd.append('name', name);
fd.append('username', username);
fd.append('role', role);
if (password) fd.append('password', password);
const btn = document.getElementById('btnSaveUser');
btn.disabled = true;
try {
await apiPost(API_SAVE, fd);
userModal.hide();
showToast('User gespeichert ✅');
await refresh();
} catch (e) {
showToast('Fehler: ' + e.message);
} finally {
btn.disabled = false;
}
});
document.addEventListener('click', async (ev) => {
const editBtn = ev.target.closest('.actEdit');
if (editBtn){
const id = editBtn.dataset.id;
const u = cacheUsers.find(x => String(x.id) === String(id));
if (u) openEdit(u);
return;
}
const delBtn = ev.target.closest('.actDel');
if (delBtn){
deleteId = delBtn.dataset.id;
deleteLabel = delBtn.dataset.label || '';
document.getElementById('delInfo').textContent = deleteLabel;
delModal.show();
return;
}
const resetBtn = ev.target.closest('.actReset');
if (resetBtn){
const id = resetBtn.dataset.id;
if (!confirm('Passwort zurücksetzen?\nUser muss beim nächsten Login ein neues Passwort setzen.\n(Optional: Wir loggen ihn auch auf allen Geräten aus)')) return;
const fd = new FormData();
fd.append('id', id);
try {
await apiPost(API_RESET, fd);
showToast('Passwort-Reset gesetzt 🔑');
await refresh();
} catch (e) {
showToast('Fehler: ' + e.message);
}
return;
}
const loBtn = ev.target.closest('.actLogoutAll');
if (loBtn){
const id = loBtn.dataset.id;
if (!confirm('Alle Geräte abmelden?\n(Das löscht alle Remember-Me Tokens dieses Users)')) return;
const fd = new FormData();
fd.append('id', id);
try {
await apiPost(API_LOGOUTALL, fd);
showToast('Alle Geräte abgemeldet ✅');
await refresh();
} catch (e) {
showToast('Fehler: ' + e.message);
}
return;
}
});
document.getElementById('btnConfirmDelete').addEventListener('click', async () => {
if (!deleteId) return;
const fd = new FormData();
fd.append('id', deleteId);
try {
await apiPost(API_DEL, fd);
delModal.hide();
deleteId = null;
showToast('User gelöscht 🗑️');
await refresh();
} catch (e) {
showToast('Fehler: ' + e.message);
}
});
refresh().catch(e => showToast('Fehler: ' + e.message));
</script>
<style>
div.dataTables_wrapper .dataTables_length select,
div.dataTables_wrapper .dataTables_filter input {
border-radius: .5rem;
border: 1px solid #dee2e6;
padding: .375rem .5rem;
}
div.dataTables_wrapper .dataTables_filter input { margin-left: .5rem; }
div.dataTables_wrapper .dataTables_length label,
div.dataTables_wrapper .dataTables_filter label {
font-size: .9rem;
color: #6c757d;
}
table.dataTable thead th { border-bottom: 1px solid #e9ecef !important; }
</style>
</body>
</html>