Passwort-ändern-Funktion für alle Rollen hinzufügen

Neuer Endpoint POST /v1/auth/change-password (verlangt aktuelles Passwort
zur Bestätigung) sowie Modal + Button im Topbar für Vermieter und Mieter.
This commit is contained in:
Giuseppe Lombardo 2026-08-13 12:38:23 +00:00
parent dd24d7217a
commit 78810002b4
2 changed files with 109 additions and 0 deletions

View File

@ -32,6 +32,32 @@ authRouter.post('/auth/login', async (req: Request, res: Response) => {
res.status(200).json({ token, user: publicUser(user) }); res.status(200).json({ token, user: publicUser(user) });
}); });
// POST /v1/auth/change-password { currentPassword, newPassword } -> { ok: true }
// Für alle Rollen (Vermieter/Admin/Mieter/Handwerker) gleichermaßen nutzbar —
// ändert ausschließlich das eigene Passwort, verlangt zur Bestätigung das
// aktuelle Passwort.
authRouter.post('/auth/change-password', requireAuth, async (req: AuthedRequest, res: Response) => {
const { currentPassword, newPassword } = req.body || {};
if (!currentPassword || !newPassword) {
return res.status(400).json({ error: 'Aktuelles und neues Passwort erforderlich' });
}
if (String(newPassword).length < 8) {
return res.status(400).json({ error: 'Das neue Passwort muss mindestens 8 Zeichen lang sein' });
}
const user = await prisma.user.findUnique({ where: { id: req.user!.id } });
if (!user) return res.status(404).json({ error: 'Nutzer nicht gefunden' });
const currentOk = await bcrypt.compare(String(currentPassword), user.passwordHash);
if (!currentOk) {
return res.status(401).json({ error: 'Aktuelles Passwort ist falsch' });
}
const passwordHash = await bcrypt.hash(String(newPassword), 12);
await prisma.user.update({ where: { id: user.id }, data: { passwordHash } });
res.status(200).json({ ok: true });
});
// GET /v1/auth/me -> aktuell eingeloggter Nutzer (zur Session-Wiederherstellung) // GET /v1/auth/me -> aktuell eingeloggter Nutzer (zur Session-Wiederherstellung)
authRouter.get('/auth/me', requireAuth, async (req: AuthedRequest, res: Response) => { authRouter.get('/auth/me', requireAuth, async (req: AuthedRequest, res: Response) => {
const user = await prisma.user.findUnique({ where: { id: req.user!.id } }); const user = await prisma.user.findUnique({ where: { id: req.user!.id } });

View File

@ -590,6 +590,7 @@
<div class="topbar"> <div class="topbar">
<button class="hamburger-btn" onclick="openSidebar()"></button> <button class="hamburger-btn" onclick="openSidebar()"></button>
<span id="whoAmI"></span> <span id="whoAmI"></span>
<button class="btn-secondary" onclick="openChangePassword()">Passwort ändern</button>
<button class="btn-secondary" onclick="logout()">Abmelden</button> <button class="btn-secondary" onclick="logout()">Abmelden</button>
</div> </div>
@ -1081,6 +1082,35 @@
</div> </div>
</div> </div>
<!-- ================= PASSWORT ÄNDERN (Modal) ================= -->
<div id="changePasswordOverlay" class="doc-preview-overlay" style="display:none" onclick="if (event.target === this) closeChangePassword()">
<div class="doc-preview-box" style="max-width:380px;">
<div class="doc-preview-header">
<strong>Passwort ändern</strong>
<button type="button" class="btn-secondary" onclick="closeChangePassword()">Schließen ✕</button>
</div>
<div style="padding:20px;">
<div id="changePasswordError" style="display:none" class="form-error"></div>
<div id="changePasswordSuccess" style="display:none" class="form-success"></div>
<form id="changePasswordForm">
<div class="field">
<label for="currentPassword">Aktuelles Passwort</label>
<input type="password" id="currentPassword" required autocomplete="current-password" />
</div>
<div class="field">
<label for="newPassword">Neues Passwort (min. 8 Zeichen)</label>
<input type="password" id="newPassword" required minlength="8" autocomplete="new-password" />
</div>
<div class="field">
<label for="newPasswordRepeat">Neues Passwort wiederholen</label>
<input type="password" id="newPasswordRepeat" required minlength="8" autocomplete="new-password" />
</div>
<button type="submit" class="btn-primary" id="changePasswordSubmitBtn">Passwort ändern</button>
</form>
</div>
</div>
</div>
<script> <script>
const API_BASE_URL = window.WG_API_BASE_URL || const API_BASE_URL = window.WG_API_BASE_URL ||
(location.protocol === 'file:' ? 'http://localhost:3000/v1' : '/v1'); (location.protocol === 'file:' ? 'http://localhost:3000/v1' : '/v1');
@ -1363,6 +1393,7 @@
if (isLandlord) initUtilityStatementForm(); if (isLandlord) initUtilityStatementForm();
if (isLandlord) initCraftsmanForm(); if (isLandlord) initCraftsmanForm();
initRatingForm(); initRatingForm();
initChangePasswordForm();
loadData(); loadData();
loadCleaningTasks(); loadCleaningTasks();
@ -2960,6 +2991,58 @@
document.getElementById('docPreviewBody').innerHTML = ''; document.getElementById('docPreviewBody').innerHTML = '';
} }
// ---------------------------------------------------------------------
// PASSWORT ÄNDERN (für alle Rollen)
// ---------------------------------------------------------------------
function openChangePassword() {
document.getElementById('changePasswordError').style.display = 'none';
document.getElementById('changePasswordSuccess').style.display = 'none';
document.getElementById('changePasswordForm').reset();
document.getElementById('changePasswordOverlay').style.display = 'flex';
}
function closeChangePassword() {
document.getElementById('changePasswordOverlay').style.display = 'none';
}
function initChangePasswordForm() {
const form = document.getElementById('changePasswordForm');
const errorBox = document.getElementById('changePasswordError');
const successBox = document.getElementById('changePasswordSuccess');
form.onsubmit = async (e) => {
e.preventDefault();
errorBox.style.display = 'none';
successBox.style.display = 'none';
const currentPassword = document.getElementById('currentPassword').value;
const newPassword = document.getElementById('newPassword').value;
const newPasswordRepeat = document.getElementById('newPasswordRepeat').value;
if (newPassword !== newPasswordRepeat) {
errorBox.textContent = 'Die neuen Passwörter stimmen nicht überein.';
errorBox.style.display = 'block';
return;
}
const submitBtn = document.getElementById('changePasswordSubmitBtn');
submitBtn.disabled = true;
submitBtn.textContent = 'Ändert…';
try {
await apiFetch('/auth/change-password', {
method: 'POST',
body: JSON.stringify({ currentPassword, newPassword }),
});
form.reset();
successBox.textContent = 'Passwort erfolgreich geändert.';
successBox.style.display = 'block';
} catch (err) {
errorBox.textContent = err.message;
errorBox.style.display = 'block';
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Passwort ändern';
}
};
}
function buildProfileCompletenessBadge(user) { function buildProfileCompletenessBadge(user) {
const missing = []; const missing = [];
if (!user.phoneNumber) missing.push('Telefonnummer'); if (!user.phoneNumber) missing.push('Telefonnummer');