Fix #46: Reiter 'Profil' zum Ändern des eigenen Passworts

This commit is contained in:
fds2610-bot 2026-08-20 18:48:27 +02:00
parent 7e4ef4751f
commit 606bbb3b35
6 changed files with 121 additions and 0 deletions

View file

@ -1,5 +1,19 @@
# Änderungen # Änderungen
## Einstellungen: Reiter "Profil" zum Ändern des eigenen Passworts (20.08.2026)
Bislang ließ sich das eigene Passwort nur direkt über den Datenbestand ändern (Feature #46).
Der Einstellungsdialog bekommt dafür einen neuen Reiter `Profil`, der den angemeldeten
Benutzernamen anzeigt und ein eigenes Formular für die Passwortänderung enthält – getrennt
vom generischen Speichern-Formular der übrigen Reiter, weil hier kein Einstellungswert,
sondern der Anmeldedatensatz geschrieben wird.
`EinstellungenController::passwortAendern()` (Route `POST /einstellungen/passwort`) verlangt
das aktuelle Passwort (geprüft über `Benutzer::pruefeAnmeldung()`), ein neues mit mindestens
10 Zeichen – dieselbe Grenze wie bei der Ersteinrichtung – und dessen Wiederholung. Bei Erfolg
schreibt `Benutzer::passwortSetzen()` den neuen Hash; ein Eintrag landet wie bei An- und
Abmeldung im Debug-Log.
## CONTRIBUTE.md und Setup-Skript für Mitwirkende (19.08.2026) ## CONTRIBUTE.md und Setup-Skript für Mitwirkende (19.08.2026)
`CONTRIBUTE.md` beschreibt, wie Beiträge ankommen (Issue, PR, E-Mail), welche `CONTRIBUTE.md` beschreibt, wie Beiträge ankommen (Issue, PR, E-Mail), welche

View file

@ -68,6 +68,7 @@ $router->post('/cockpit/vermoegen/konten', fn() => (new CockpitController())->ko
$router->get('/einstellungen', fn() => (new EinstellungenController())->index()); $router->get('/einstellungen', fn() => (new EinstellungenController())->index());
$router->post('/einstellungen', fn() => (new EinstellungenController())->speichern()); $router->post('/einstellungen', fn() => (new EinstellungenController())->speichern());
$router->post('/einstellungen/log-leeren', fn() => (new EinstellungenController())->logLeeren()); $router->post('/einstellungen/log-leeren', fn() => (new EinstellungenController())->logLeeren());
$router->post('/einstellungen/passwort', fn() => (new EinstellungenController())->passwortAendern());
// Konten. Die festen Pfade stehen vor den Mustern mit {id}, sonst schluckt // Konten. Die festen Pfade stehen vor den Mustern mit {id}, sonst schluckt
// "/konten/{id}/…" die Route "/konten/neu". // "/konten/{id}/…" die Route "/konten/neu".

View file

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Finanzmanager\Controllers; namespace Finanzmanager\Controllers;
use Finanzmanager\Auth;
use Finanzmanager\Models\Benutzer;
use Finanzmanager\Models\Einstellung; use Finanzmanager\Models\Einstellung;
use Finanzmanager\Models\Kategorie; use Finanzmanager\Models\Kategorie;
use Finanzmanager\Services\Cockpit; use Finanzmanager\Services\Cockpit;
@ -90,6 +92,39 @@ class EinstellungenController extends BaseController
); );
} }
/** Eigenes Passwort ändern, Reiter "Profil". Passt nicht in felderFuer()/speichern(),
* weil es keinen Einstellungswert schreibt, sondern die Anmeldedaten des Benutzers. */
public function passwortAendern(): void
{
$this->verlangeAnmeldung();
$benutzer = Auth::benutzer();
if ($benutzer === null) {
View::redirect('/login');
}
$altesPasswort = (string)($_POST['passwort_alt'] ?? '');
$neuesPasswort = (string)($_POST['passwort_neu'] ?? '');
$wiederholung = (string)($_POST['passwort_neu_wdh'] ?? '');
$fehler = match (true) {
Benutzer::pruefeAnmeldung($benutzer['benutzername'], $altesPasswort) === null
=> 'Das aktuelle Passwort ist falsch.',
mb_strlen($neuesPasswort) < 10 => 'Das neue Passwort muss mindestens 10 Zeichen lang sein.',
$neuesPasswort !== $wiederholung => 'Die beiden neuen Passwörter stimmen nicht überein.',
default => null,
};
if ($fehler !== null) {
View::redirect('/einstellungen?bereich=profil', $fehler, 'fehler');
}
Benutzer::passwortSetzen((int)$benutzer['id'], $neuesPasswort);
DebugLog::schreibe('auth', 'Passwort geändert', ['benutzer_id' => $benutzer['id']]);
View::redirect('/einstellungen?bereich=profil', 'Passwort geändert.');
}
/** /**
* Welche Felder zu welchem Reiter gehören, inklusive Typ. * Welche Felder zu welchem Reiter gehören, inklusive Typ.
* Bereiche ohne Einträge sind Platzhalter für spätere Phasen. * Bereiche ohne Einträge sind Platzhalter für spätere Phasen.

View file

@ -21,6 +21,7 @@ class Einstellung
'import' => 'Import/Export', 'import' => 'Import/Export',
'wertpapiere' => 'Wertpapiere', 'wertpapiere' => 'Wertpapiere',
'diagnose' => 'Diagnose', 'diagnose' => 'Diagnose',
'profil' => 'Profil',
]; ];
/** /**

View file

@ -13,6 +13,7 @@ use Finanzmanager\View;
/** @var list<string> $kacheln */ /** @var list<string> $kacheln */
/** @var list<array<string,mixed>> $kategorien */ /** @var list<array<string,mixed>> $kategorien */
/** @var string $csrfToken */ /** @var string $csrfToken */
/** @var array|null $benutzer */
$wert = static fn(string $k, string $standard = ''): string => (string)($werte[$k] ?? $standard); $wert = static fn(string $k, string $standard = ''): string => (string)($werte[$k] ?? $standard);
@ -266,6 +267,11 @@ include __DIR__ . '/../layout/kopf.php';
</table> </table>
<?php endif; ?> <?php endif; ?>
<?php elseif ($bereich === 'profil'): ?>
<p class="feldhinweis">
Angemeldet als <strong><?= View::h($benutzer['benutzername'] ?? '') ?></strong>.
</p>
<?php else: ?> <?php else: ?>
<p class="platzhalter"> <p class="platzhalter">
Dieser Bereich füllt sich in einer späteren Phase: Dieser Bereich füllt sich in einer späteren Phase:
@ -280,6 +286,28 @@ include __DIR__ . '/../layout/kopf.php';
<?php endif; ?> <?php endif; ?>
</form> </form>
<?php if ($bereich === 'profil'): ?>
<form method="post" action="/einstellungen/passwort" class="formular breit">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<label for="passwort_alt">Aktuelles Passwort</label>
<input type="password" id="passwort_alt" name="passwort_alt" autocomplete="current-password" required>
<label for="passwort_neu">Neues Passwort</label>
<input type="password" id="passwort_neu" name="passwort_neu" autocomplete="new-password"
minlength="10" required>
<p class="feldhinweis">Mindestens 10 Zeichen.</p>
<label for="passwort_neu_wdh">Neues Passwort wiederholen</label>
<input type="password" id="passwort_neu_wdh" name="passwort_neu_wdh" autocomplete="new-password"
minlength="10" required>
<div class="knopfleiste">
<button type="submit" class="knopf primaer">Passwort ändern</button>
</div>
</form>
<?php endif; ?>
<?php if ($bereich === 'diagnose' && $logDateien !== []): ?> <?php if ($bereich === 'diagnose' && $logDateien !== []): ?>
<form method="post" action="/einstellungen/log-leeren" class="formular breit" <form method="post" action="/einstellungen/log-leeren" class="formular breit"
data-bestaetigen="Alle Logdateien löschen?"> data-bestaetigen="Alle Logdateien löschen?">

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
use Finanzmanager\Models\Benutzer;
class ProfilPasswortTest extends TestCase
{
protected string $testName = 'Test 36: Profil – eigenes Passwort ändern';
public function run(): void
{
$this->passwortWirdGeaendert();
$this->altesPasswortWirdUngueltig();
}
private function passwortWirdGeaendert(): void
{
$name = 'test_' . bin2hex(random_bytes(4));
$id = Benutzer::anlegen($name, 'altespasswort123');
Benutzer::passwortSetzen($id, 'neuespasswort456');
$this->assertNotEmpty(
Benutzer::pruefeAnmeldung($name, 'neuespasswort456'),
'Anmeldung mit dem neuen Passwort gelingt'
);
}
private function altesPasswortWirdUngueltig(): void
{
$name = 'test_' . bin2hex(random_bytes(4));
$id = Benutzer::anlegen($name, 'altespasswort123');
Benutzer::passwortSetzen($id, 'neuespasswort456');
$this->assertNull(
Benutzer::pruefeAnmeldung($name, 'altespasswort123'),
'Anmeldung mit dem alten Passwort schlägt danach fehl'
);
}
}