Added Password Self-Service

This commit is contained in:
root
2026-08-23 19:36:19 +02:00
parent a772317b72
commit 8643f45370
3 changed files with 79 additions and 11 deletions
+23 -1
View File
@@ -33,6 +33,29 @@ if ($action === 'logout') {
if (!isset($_SESSION['user_id'])) { echo json_encode(['success' => false, 'error' => 'Zugriff verweigert.']); exit; } if (!isset($_SESSION['user_id'])) { echo json_encode(['success' => false, 'error' => 'Zugriff verweigert.']); exit; }
// === NEU: PASSWORT ÄNDERN ===
if ($action === 'change_password') {
$oldPass = $_POST['old_password'] ?? '';
$newPass = $_POST['new_password'] ?? '';
$userId = $_SESSION['user_id'];
$stmt = $pdo->prepare("SELECT password_hash FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($oldPass, $user['password_hash'])) {
if(strlen($newPass) < 4) { echo json_encode(['success' => false, 'error' => 'Passwort muss mindestens 4 Zeichen lang sein.']); exit; }
$newHash = password_hash($newPass, PASSWORD_DEFAULT);
$uStmt = $pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?");
$uStmt->execute([$newHash, $userId]);
logAudit('Passwort geändert', 'Self-Service');
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Das alte Passwort ist falsch.']);
}
exit;
}
$isAdmin = ($_SESSION['role'] ?? '') === 'admin'; $isAdmin = ($_SESSION['role'] ?? '') === 'admin';
session_write_close(); session_write_close();
@@ -62,7 +85,6 @@ function checkVmPermission($pdo, $vmid) {
$stmt = $pdo->prepare("SELECT permissions FROM users WHERE id = ?"); $stmt->execute([$userId]); return in_array($vmid, json_decode($stmt->fetchColumn(), true)['allowed_vms'] ?? []); $stmt = $pdo->prepare("SELECT permissions FROM users WHERE id = ?"); $stmt->execute([$userId]); return in_array($vmid, json_decode($stmt->fetchColumn(), true)['allowed_vms'] ?? []);
} }
// === AUDIT LOG ENDPUNKT ===
if ($action === 'get_audit_logs') { if ($action === 'get_audit_logs') {
if (!$isAdmin) exit; if (!$isAdmin) exit;
$stmt = $pdo->query("SELECT * FROM audit_logs ORDER BY id DESC LIMIT 200"); $stmt = $pdo->query("SELECT * FROM audit_logs ORDER BY id DESC LIMIT 200");
+30 -10
View File
@@ -24,6 +24,34 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
} }
} }
// === NEU: PASSWORT ÄNDERN LOGIK ===
const pwdModal = document.getElementById('passwordModal');
window.openPasswordModal = function() { if(pwdModal) { pwdModal.classList.remove('hidden'); document.getElementById('changePasswordForm').reset(); } }
window.closePasswordModal = function() { if(pwdModal) pwdModal.classList.add('hidden'); }
const pwdForm = document.getElementById('changePasswordForm');
if(pwdForm) {
pwdForm.addEventListener('submit', async function(e) {
e.preventDefault();
const oldP = document.getElementById('oldPassword').value;
const newP = document.getElementById('newPassword').value;
const confirmP = document.getElementById('newPasswordConfirm').value;
if (newP !== confirmP) { alert('Die neuen Passwörter stimmen nicht überein!'); return; }
const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Speichere...';
const fd = new FormData(); fd.append('old_password', oldP); fd.append('new_password', newP);
try {
const res = await (await fetch('api.php?action=change_password', {method: 'POST', body: fd})).json();
if(res.success) {
alert('Passwort erfolgreich geändert! Bitte neu anmelden.');
closePasswordModal(); logout();
} else { alert(res.error || 'Fehler beim Ändern des Passworts.'); }
} catch(err) { alert('Netzwerkfehler.'); } finally { btn.innerText = oTxt; }
});
}
window.switchTab = function(tab) { window.switchTab = function(tab) {
['pve', 'pbs', 'pmg'].forEach(t => { const el = document.getElementById('tab-' + t); const nav = document.getElementById('nav-tab-' + t); if (t === tab) { el.classList.remove('hidden'); setTimeout(() => el.classList.remove('opacity-0'), 50); nav.classList.add('tab-active' + (t === 'pve' ? '' : '-' + t)); } else { el.classList.add('hidden', 'opacity-0'); nav.classList.remove('tab-active', 'tab-active-pbs', 'tab-active-pmg'); } }); ['pve', 'pbs', 'pmg'].forEach(t => { const el = document.getElementById('tab-' + t); const nav = document.getElementById('nav-tab-' + t); if (t === tab) { el.classList.remove('hidden'); setTimeout(() => el.classList.remove('opacity-0'), 50); nav.classList.add('tab-active' + (t === 'pve' ? '' : '-' + t)); } else { el.classList.add('hidden', 'opacity-0'); nav.classList.remove('tab-active', 'tab-active-pbs', 'tab-active-pmg'); } });
if(tab === 'pbs') fetchPbsStats(); if(tab === 'pmg') fetchPmgStats(); if(tab === 'pbs') fetchPbsStats(); if(tab === 'pmg') fetchPmgStats();
@@ -537,7 +565,7 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
window.toggleCronJob = async function(id, newState) { const fd = new FormData(); fd.append('id', id); fd.append('is_active', newState); await fetch('api.php?action=toggle_cron_job', {method: 'POST', body: fd}); loadCronJobs(); } window.toggleCronJob = async function(id, newState) { const fd = new FormData(); fd.append('id', id); fd.append('is_active', newState); await fetch('api.php?action=toggle_cron_job', {method: 'POST', body: fd}); loadCronJobs(); }
window.deleteCronJob = async function(id) { if(!confirm('Diesen geplanten Job wirklich löschen?')) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_cron_job', {method: 'POST', body: fd}); loadCronJobs(); } window.deleteCronJob = async function(id) { if(!confirm('Diesen geplanten Job wirklich löschen?')) return; const fd = new FormData(); fd.append('id', id); await fetch('api.php?action=delete_cron_job', {method: 'POST', body: fd}); loadCronJobs(); }
// === NEU: AUDIT LOG === // === AUDIT LOG ===
const auditModal = document.getElementById('auditLogModal'); const auditModal = document.getElementById('auditLogModal');
window.openAuditLog = async function() { window.openAuditLog = async function() {
auditModal.classList.remove('hidden'); auditModal.classList.remove('hidden');
@@ -549,18 +577,10 @@ if (window.APP.isLoggedIn && window.APP.nodeCount > 0) {
tbody.innerHTML = ''; tbody.innerHTML = '';
if(res.data.length === 0) { tbody.innerHTML = '<tr><td colspan="4" class="text-center text-gray-500 py-4">Noch keine Einträge.</td></tr>'; return; } if(res.data.length === 0) { tbody.innerHTML = '<tr><td colspan="4" class="text-center text-gray-500 py-4">Noch keine Einträge.</td></tr>'; return; }
res.data.forEach(log => { res.data.forEach(log => {
// Konvertiere SQLite DATETIME in lokales deutsches Format
const d = new Date(log.timestamp + 'Z'); const d = new Date(log.timestamp + 'Z');
const timeStr = d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE'); const timeStr = d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE');
const isSystem = log.username === 'System'; const isSystem = log.username === 'System';
tbody.innerHTML += `<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50"><td class="px-4 py-3 text-gray-400 whitespace-nowrap text-xs">${timeStr}</td><td class="px-4 py-3"><span class="${isSystem ? 'text-gray-500' : 'text-blue-400 font-bold'}">${log.username}</span></td><td class="px-4 py-3 font-bold text-white">${log.action}</td><td class="px-4 py-3 text-gray-300 text-xs">${log.target || '-'}</td></tr>`;
tbody.innerHTML += `
<tr class="hover:bg-darkcard/50 transition-colors border-b border-darkborder/50">
<td class="px-4 py-3 text-gray-400 whitespace-nowrap text-xs">${timeStr}</td>
<td class="px-4 py-3"><span class="${isSystem ? 'text-gray-500' : 'text-blue-400 font-bold'}">${log.username}</span></td>
<td class="px-4 py-3 font-bold text-white">${log.action}</td>
<td class="px-4 py-3 text-gray-300 text-xs">${log.target || '-'}</td>
</tr>`;
}); });
} }
} catch(e) { tbody.innerHTML = '<tr><td colspan="4" class="text-center text-red-500 py-4">Fehler beim Laden.</td></tr>'; } } catch(e) { tbody.innerHTML = '<tr><td colspan="4" class="text-center text-red-500 py-4">Fehler beim Laden.</td></tr>'; }
+26
View File
@@ -94,4 +94,30 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- NEU: PASSWORT ÄNDERN MODAL -->
<div id="passwordModal" class="fixed inset-0 bg-black/80 hidden z-[100] flex items-center justify-center backdrop-blur-sm">
<div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-sm overflow-hidden flex flex-col">
<div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg">
<h2 class="text-lg font-bold text-white flex items-center gap-2">🔑 Passwort ändern</h2>
<button onclick="closePasswordModal()" class="text-gray-400 hover:text-white transition-colors text-xl"></button>
</div>
<div class="p-6">
<form id="changePasswordForm" class="space-y-4">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Aktuelles Passwort</label>
<input type="password" id="oldPassword" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm focus:border-proxmox" required>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Neues Passwort</label>
<input type="password" id="newPassword" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm focus:border-proxmox" required>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Neues Passwort bestätigen</label>
<input type="password" id="newPasswordConfirm" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white text-sm focus:border-proxmox" required>
</div>
<button type="submit" class="w-full bg-proxmox hover:bg-orange-600 text-white font-bold py-2.5 rounded mt-2 transition-colors">Passwort aktualisieren</button>
</form>
</div>
</div>
</div> </div>