Unifi-Voucher-Tool/admin/users.php
Friederich Loheide 850a04d628 Englische Übersetzungen für die restlichen Admin-Seiten
API-Schlüssel, Integration & Wartung, Voucher-Import, Backup & Restore,
Reporting, Sicherheit (2FA) und Audit-Log waren fest auf Deutsch
verdrahtet, obwohl in der Kopfzeile ein DE/EN-Umschalter sitzt. Diese
Seiten laufen jetzt komplett über lang/de.php bzw. lang/en.php.

- rund 200 neue Sprachschlüssel, beide Dateien deckungsgleich
- Aktionsnamen im Audit-Log werden übersetzt statt fest ausgegeben
- Bestätigungsdialoge und Statusmeldungen in JavaScript ebenfalls
- admin/security.php initialisiert jetzt I18n und setzt <html lang>
  passend zur Auswahl

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 06:34:25 +00:00

429 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../includes/Database.php';
require_once __DIR__ . '/../includes/Auth.php';
require_once __DIR__ . '/../includes/Mailer.php';
require_once __DIR__ . '/../includes/I18n.php';
$auth = new Auth();
$auth->requireAdmin();
$db = Database::getInstance();
$mailer = new Mailer();
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
$smtpEnabled = $db->getSetting('smtp_enabled', '0') === '1';
I18n::init();
$error = '';
$success = '';
// Send password reset link
if (isset($_GET['send_reset']) && isset($_GET['token'])) {
if ($auth->validateCsrfToken($_GET['token'])) {
$targetUser = $db->fetchOne("SELECT * FROM users WHERE id=? AND is_active=1 AND password_hash IS NOT NULL", [(int)$_GET['send_reset']]);
if ($targetUser) {
try {
$db->execute("DELETE FROM password_reset_tokens WHERE user_id=?", [$targetUser['id']]);
$token = bin2hex(random_bytes(32));
$expiresAt = date('Y-m-d H:i:s', strtotime('+1 hour'));
$db->execute("INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES (?,?,?)", [$targetUser['id'], $token, $expiresAt]);
$systemUrl = rtrim($db->getSetting('system_url', ''), '/');
if (empty($systemUrl)) {
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']==='on' ? 'https' : 'http';
$scriptPath = dirname(dirname($_SERVER['SCRIPT_NAME']));
$systemUrl = $protocol . '://' . $_SERVER['HTTP_HOST'] . ($scriptPath==='/'?'':$scriptPath);
}
$resetUrl = $systemUrl . '/reset_password.php?token=' . $token;
$mailer->sendRaw($targetUser['email'], $appTitle . ' Passwort zurücksetzen',
"Hallo {$targetUser['name']},\n\nEin Administrator hat für Sie einen Passwort-Reset-Link erstellt:\n\n{$resetUrl}\n\n(Gültig für 1 Stunde)\n\n{$appTitle}");
$success = 'Passwort-Reset-Link wurde an ' . htmlspecialchars($targetUser['email']) . ' gesendet.';
} catch (Exception $e) {
$error = 'Fehler beim Senden: ' . $e->getMessage();
}
} else {
$error = 'Benutzer nicht gefunden oder kein lokales Passwort.';
}
} else {
$error = __('error_csrf');
}
}
// Edit user
if ($_SERVER['REQUEST_METHOD']==='POST' && isset($_POST['edit_user'])) {
if (!$auth->validateCsrfToken($_POST['csrf_token']??'')) {
$error = __('error_csrf');
} else {
try {
$userId = (int)$_POST['user_id'];
$isAdmin = isset($_POST['is_admin']) ? 1 : 0;
$siteIds = $_POST['site_ids'] ?? [];
$oldUser = $db->fetchOne("SELECT * FROM users WHERE id=?", [$userId]);
$oldSites= $db->fetchAll("SELECT s.name FROM sites s INNER JOIN user_site_access usa ON s.id=usa.site_id WHERE usa.user_id=?", [$userId]);
$db->query("UPDATE users SET is_admin=? WHERE id=?", [$isAdmin, $userId]);
$db->query("DELETE FROM user_site_access WHERE user_id=?", [$userId]);
$newSites = [];
if (!$isAdmin && !empty($siteIds)) {
foreach ($siteIds as $siteId) {
$db->execute("INSERT INTO user_site_access (user_id, site_id) VALUES (?,?)", [$userId, $siteId]);
$site = $db->fetchOne("SELECT name FROM sites WHERE id=?", [$siteId]);
if ($site) $newSites[] = $site['name'];
}
}
$changes = [];
if ($oldUser['is_admin'] != $isAdmin) {
$changes[] = $isAdmin ? 'Sie wurden zum Administrator ernannt' : 'Ihre Administrator-Rechte wurden entfernt';
}
$oldSiteNames = array_column($oldSites,'name');
$addedSites = array_diff($newSites, $oldSiteNames);
$removedSites = array_diff($oldSiteNames, $newSites);
if (!empty($addedSites)) $changes[] = 'Zugriff gewährt auf: ' . implode(', ', $addedSites);
if (!empty($removedSites)) $changes[] = 'Zugriff entfernt von: ' . implode(', ', $removedSites);
if ($isAdmin && !$oldUser['is_admin']) $changes[] = 'Sie haben nun Zugriff auf alle Sites';
if (!empty($changes)) $mailer->sendUserNotification($oldUser['email'], $oldUser['name'], $changes);
$success = __('users_updated') . (!empty($changes) ? ' '.__('users_notified') : '');
$auth->writeAuditLog($_SESSION['user_id'], 'user_edit', 'user', $userId, implode('; ', $changes) ?: 'Keine Änderungen');
} catch (Exception $e) { $error = $e->getMessage(); }
}
}
// Add user
if ($_SERVER['REQUEST_METHOD']==='POST' && isset($_POST['add_user'])) {
if (!$auth->validateCsrfToken($_POST['csrf_token']??'')) {
$error = __('error_csrf');
} else {
try {
$email = trim($_POST['email']);
$name = trim($_POST['name']);
$password= $_POST['password'];
$isAdmin = isset($_POST['is_admin']) ? 1 : 0;
$siteIds = $_POST['site_ids'] ?? [];
if (empty($email)||empty($name)||empty($password)) throw new Exception(__('error_fill_all'));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) throw new Exception(__('error_email_invalid'));
if (strlen($password) < 8) throw new Exception(__('settings_pw_minlength'));
if ($db->fetchOne("SELECT id FROM users WHERE email=?", [$email])) throw new Exception('E-Mail bereits vorhanden');
$userId = $auth->registerUser($email, $name, $password, $isAdmin);
if (!$userId) throw new Exception('Benutzer konnte nicht erstellt werden');
if (!$isAdmin && !empty($siteIds)) {
foreach ($siteIds as $siteId) {
$db->execute("INSERT INTO user_site_access (user_id, site_id) VALUES (?,?)", [$userId, $siteId]);
}
}
$auth->writeAuditLog($_SESSION['user_id'], 'user_create', 'user', $userId, "Benutzer {$name} erstellt");
$success = __('users_added');
} catch (Exception $e) { $error = $e->getMessage(); }
}
}
// Delete user
if (isset($_GET['delete']) && isset($_GET['token'])) {
if ($auth->validateCsrfToken($_GET['token'])) {
$deleteId = (int)$_GET['delete'];
if ($deleteId === (int)$_SESSION['user_id']) {
$error = 'Sie können sich nicht selbst löschen';
} else {
$db->query("DELETE FROM users WHERE id=?", [$deleteId]);
$auth->writeAuditLog($_SESSION['user_id'], 'user_delete', 'user', $deleteId, 'Benutzer gelöscht');
$success = __('users_deleted');
}
} else { $error = __('error_csrf'); }
}
// Toggle user active
if (isset($_GET['toggle']) && isset($_GET['token'])) {
if ($auth->validateCsrfToken($_GET['token'])) {
$toggleId = (int)$_GET['toggle'];
if ($toggleId === (int)$_SESSION['user_id']) {
$error = 'Sie können sich nicht selbst deaktivieren';
} else {
$user = $db->fetchOne("SELECT is_active FROM users WHERE id=?", [$toggleId]);
if ($user) {
$newStatus = $user['is_active'] ? 0 : 1;
$db->query("UPDATE users SET is_active=? WHERE id=?", [$newStatus, $toggleId]);
$success = 'Benutzer-Status aktualisiert!';
}
}
} else { $error = __('error_csrf'); }
}
// 2FA eines Benutzers zurücksetzen (Admin-Hilfe bei verlorenem Authenticator)
if (isset($_GET['reset_2fa']) && isset($_GET['token'])) {
if ($auth->validateCsrfToken($_GET['token'])) {
$auth->disableTotp((int)$_GET['reset_2fa']);
$success = '2FA des Benutzers wurde zurückgesetzt.';
} else { $error = __('error_csrf'); }
}
$users = $db->fetchAll("SELECT * FROM users ORDER BY name");
$sites = $db->fetchAll("SELECT * FROM sites WHERE is_active=1 ORDER BY name");
$userSiteAccess = [];
foreach ($users as $user) {
$userSiteAccess[$user['id']] = $db->fetchAll("SELECT s.id, s.name FROM sites s INNER JOIN user_site_access usa ON s.id=usa.site_id WHERE usa.user_id=?", [$user['id']]);
}
$currentPage = 'users';
?>
<!DOCTYPE html>
<html lang="<?= I18n::getLanguage() ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('users_title') ?> <?= htmlspecialchars($appTitle) ?></title>
<?php include __DIR__ . '/../includes/admin_nav.php'; ?>
<div class="page-header">
<h1 class="page-title"><?= __('users_title') ?></h1>
<button onclick="openModal()" class="btn btn-primary">
<i class="fas fa-plus"></i> <?= __('users_add') ?>
</button>
</div>
<?php if ($error): ?>
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= htmlspecialchars($success) ?></div>
<?php endif; ?>
<div class="card">
<div class="card-header"><h2 class="card-title"><?= __('users_all') ?></h2></div>
<div class="card-body" style="padding:0;">
<?php if (empty($users)): ?>
<div class="empty-state"><i class="fas fa-users"></i><p><?= __('users_none_found') ?></p></div>
<?php else: ?>
<div style="overflow-x:auto;">
<div class="table-container">
<table class="table">
<thead>
<tr>
<th><?= __('label_name') ?></th>
<th><?= __('label_email') ?></th>
<th><?= __('label_role') ?></th>
<th><?= __('label_status') ?></th>
<th><?= __('users_site_access') ?></th>
<th><?= __('users_last_login') ?></th>
<th><?= __('label_actions') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<?php $userSiteIds = array_column($userSiteAccess[$user['id']]??[], 'id'); ?>
<tr>
<td>
<strong><?= htmlspecialchars($user['name']) ?></strong>
<?php if ($user['id'] == $_SESSION['user_id']): ?>
<span class="badge badge-info"><?= __('users_you') ?></span>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($user['email']) ?></td>
<td>
<?php if ($user['is_admin']): ?>
<span class="badge badge-danger"><i class="fas fa-crown"></i> <?= __('status_admin') ?></span>
<?php else: ?>
<span class="badge badge-info"><?= __('status_user') ?></span>
<?php endif; ?>
</td>
<td>
<?php if ($user['is_active']): ?>
<span class="badge badge-success"><i class="fas fa-check"></i> <?= __('status_active') ?></span>
<?php else: ?>
<span class="badge badge-warning"><i class="fas fa-pause"></i> <?= __('status_inactive') ?></span>
<?php endif; ?>
</td>
<td>
<?php if ($user['is_admin']): ?>
<em style="color:var(--text-muted);"><?= __('users_all_sites') ?></em>
<?php elseif (!empty($userSiteAccess[$user['id']])): ?>
<?php foreach ($userSiteAccess[$user['id']] as $s): ?>
<span class="badge badge-info"><?= htmlspecialchars($s['name']) ?></span>
<?php endforeach; ?>
<?php else: ?>
<em style="color:var(--text-muted);"><?= __('users_none') ?></em>
<?php endif; ?>
</td>
<td style="font-size:13px;">
<?php if ($user['last_login']): ?>
<?= date('d.m.Y H:i', strtotime($user['last_login'])) ?>
<?php else: ?>
<em style="color:var(--text-muted);"><?= __('users_never') ?></em>
<?php endif; ?>
</td>
<td>
<div class="action-btns">
<button onclick="openEditModal(<?= $user['id'] ?>, '<?= htmlspecialchars($user['name'], ENT_QUOTES) ?>', <?= $user['is_admin'] ?>, [<?= implode(',', array_map('intval', $userSiteIds)) ?>])"
class="btn btn-secondary btn-sm" title="<?= __('btn_edit') ?>">
<i class="fas fa-edit"></i>
</button>
<?php if ($user['id'] != $_SESSION['user_id']): ?>
<a href="?toggle=<?= $user['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
class="btn btn-secondary btn-sm" title="<?= $user['is_active'] ? __('sites_deactivate') : __('sites_activate') ?>">
<i class="fas fa-<?= $user['is_active'] ? 'pause' : 'play' ?>"></i>
</a>
<?php if ($smtpEnabled && !empty($user['password_hash'])): ?>
<a href="?send_reset=<?= $user['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
class="btn btn-warning btn-sm" title="<?= __('users_reset_pw') ?>"
onclick="return confirm('Passwort-Reset-Link senden an <?= htmlspecialchars($user['email'], ENT_QUOTES) ?>?')">
<i class="fas fa-key"></i>
</a>
<?php endif; ?>
<?php if (!empty($user['totp_enabled'])): ?>
<a href="?reset_2fa=<?= $user['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
class="btn btn-secondary btn-sm" title="2FA zurücksetzen"
onclick="return confirm('2FA für <?= htmlspecialchars($user['email'], ENT_QUOTES) ?> zurücksetzen?')">
<i class="fas fa-user-shield"></i>
</a>
<?php endif; ?>
<a href="?delete=<?= $user['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
class="btn btn-danger-soft btn-sm" title="<?= __('btn_delete') ?>"
onclick="return confirm('<?= __('js_confirm_delete_user') ?>')">
<i class="fas fa-trash"></i>
</a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
</div>
</div>
</main>
<!-- Add User Modal -->
<div id="addUserModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title"><?= __('users_add_title') ?></h2>
<button class="modal-close" onclick="closeModal('addUserModal')">&times;</button>
</div>
<div class="modal-body">
<form method="post" id="addUserForm">
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<div class="form-group">
<label><?= __('label_name') ?> *</label>
<input type="text" name="name" required>
</div>
<div class="form-group">
<label><?= __('label_email') ?> *</label>
<input type="email" name="email" required>
</div>
<div class="form-group">
<label><?= __('label_password') ?> *</label>
<input type="password" name="password" required minlength="8">
<small style="color:var(--text-muted);font-size:12px;"><?= __('users_password_hint') ?></small>
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="add_is_admin" name="is_admin" onchange="toggleSiteSelection('add')">
<label for="add_is_admin" style="margin:0;"><?= __('users_admin_check') ?></label>
</div>
<small style="color:var(--text-muted);font-size:12px;"><?= __('users_admin_hint') ?></small>
</div>
<div class="form-group" id="siteSelectionGroup">
<label><?= __('users_site_access') ?></label>
<div class="site-selection">
<?php if (empty($sites)): ?>
<em style="color:var(--text-muted);"><?= __('users_no_sites') ?></em>
<?php else: ?>
<?php foreach ($sites as $site): ?>
<div class="checkbox-group">
<input type="checkbox" name="site_ids[]" value="<?= $site['id'] ?>" id="add_site_<?= $site['id'] ?>">
<label for="add_site_<?= $site['id'] ?>" style="margin:0;"><?= htmlspecialchars($site['name']) ?></label>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<small style="color:var(--text-muted);font-size:12px;"><?= __('users_site_hint') ?></small>
</div>
<div style="display:flex;gap:10px;margin-top:20px;">
<button type="submit" name="add_user" class="btn btn-primary" style="flex:1;">
<i class="fas fa-save"></i> <?= __('users_save') ?>
</button>
<button type="button" onclick="closeModal('addUserModal')" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit User Modal -->
<div id="editUserModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title"><?= __('users_edit_title') ?></h2>
<button class="modal-close" onclick="closeModal('editUserModal')">&times;</button>
</div>
<div class="modal-body">
<form method="post" id="editUserForm">
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<input type="hidden" name="user_id" id="edit_user_id">
<div class="form-group">
<label><?= __('label_name') ?></label>
<input type="text" id="edit_name" readonly style="background:var(--bg-hover);">
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="edit_is_admin" name="is_admin" onchange="toggleSiteSelection('edit')">
<label for="edit_is_admin" style="margin:0;"><?= __('users_admin_check') ?></label>
</div>
<small style="color:var(--text-muted);font-size:12px;"><?= __('users_admin_hint') ?></small>
</div>
<div class="form-group" id="editSiteSelectionGroup">
<label><?= __('users_site_access') ?></label>
<div class="site-selection" id="editSitesList">
<?php foreach ($sites as $site): ?>
<div class="checkbox-group">
<input type="checkbox" name="site_ids[]" value="<?= $site['id'] ?>" id="edit_site_<?= $site['id'] ?>">
<label for="edit_site_<?= $site['id'] ?>" style="margin:0;"><?= htmlspecialchars($site['name']) ?></label>
</div>
<?php endforeach; ?>
</div>
</div>
<div style="display:flex;gap:10px;margin-top:20px;">
<button type="submit" name="edit_user" class="btn btn-primary" style="flex:1;">
<i class="fas fa-save"></i> <?= __('users_save_edit') ?>
</button>
<button type="button" onclick="closeModal('editUserModal')" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
</div>
</form>
</div>
</div>
</div>
<div id="toast-container"></div>
<script src="../assets/global.js"></script>
<script>
function openModal() { document.getElementById('addUserModal').classList.add('active'); }
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
function openEditModal(userId, userName, isAdmin, siteIds) {
document.getElementById('edit_user_id').value = userId;
document.getElementById('edit_name').value = userName;
document.getElementById('edit_is_admin').checked = isAdmin == 1;
document.querySelectorAll('#editSitesList input[type="checkbox"]').forEach(cb => cb.checked = false);
siteIds.forEach(id => { const cb = document.getElementById('edit_site_' + id); if (cb) cb.checked = true; });
toggleSiteSelection('edit');
document.getElementById('editUserModal').classList.add('active');
}
function toggleSiteSelection(mode) {
const isAdmin = document.getElementById(mode + '_is_admin').checked;
const group = document.getElementById(mode === 'add' ? 'siteSelectionGroup' : 'editSiteSelectionGroup');
if (group) group.style.display = isAdmin ? 'none' : 'block';
}
toggleSiteSelection('add');
['addUserModal','editUserModal'].forEach(id => {
document.getElementById(id).addEventListener('click', function(e) {
if (e.target === this) closeModal(id);
});
});
</script>
</body>
</html>