Allow immediate login after registration
mindflux-ci / build (push) Successful in 34s

This commit is contained in:
AliceDarkest
2026-07-24 15:32:01 +02:00
parent 6a7e90116a
commit 0e66aff47d
8 changed files with 31 additions and 59 deletions
+6
View File
@@ -6,6 +6,12 @@ Versionierung [semantisch](https://semver.org/lang/de/).
## [Unreleased] – 2026-07-21 · Gesamt-Audit: Light Mode, Barrierefreiheit, AP2-Inhalte, DSGVO
### Registrierung & Kontozugang
- **Sofort nutzbare Accounts:** Die Registrierung hängt nicht mehr an der Zustellung
einer Bestätigungs-E-Mail. Neue Accounts können sich direkt anmelden; bereits
wartende Konten werden mit Migration V23 aktiviert. Nicht mehr benötigte
Bestätigungs-Token werden gelöscht. E-Mail bleibt für den Passwort-Reset erhalten.
### Light Mode & Design
- **Browser-Icon:** Das Neural-Emblem wird unter `/favicon.ico` ausgeliefert und
direkt auf der Startseite verlinkt. Die Versionierung der Bild-URL ersetzt auch
@@ -75,7 +75,6 @@ public class SecurityConfig {
.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRole()) // Achtung: Nur ein String, z.B. "USER" oder "ADMIN"
.disabled(!user.isEmailVerified())
.build();
};
}
@@ -1,11 +1,8 @@
package com.example.mindflux.controller;
import com.example.mindflux.service.RegistrationResult;
import com.example.mindflux.service.AccountMailService;
import com.example.mindflux.service.UserService;
import org.springframework.stereotype.Controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -13,14 +10,10 @@ import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class RegisterController {
private static final Logger log = LoggerFactory.getLogger(RegisterController.class);
private final UserService userService;
private final AccountMailService accountMailService;
public RegisterController(UserService userService, AccountMailService accountMailService) {
public RegisterController(UserService userService) {
this.userService = userService;
this.accountMailService = accountMailService;
}
@GetMapping("/register")
@@ -36,18 +29,9 @@ public class RegisterController {
@RequestParam(required = false) String confirmPassword) {
RegistrationResult result = userService.register(username, email, password, confirmPassword);
return switch (result) {
case SUCCESS -> {
try {
var user = userService.findByUsernameIgnoreCase(username.trim());
yield accountMailService.sendVerificationFor(user)
? "redirect:/?registerSuccess"
: "redirect:/?registerMailError";
} catch (RuntimeException e) {
log.warn("mail_event=REGISTRATION_VERIFICATION_FAILED errorType={}",
e.getClass().getSimpleName());
yield "redirect:/?registerMailError";
}
}
// Ein SMTP-Problem darf die Nutzung eines neu angelegten Accounts nicht blockieren.
// Die E-Mail-Adresse wird weiterhin für den Passwort-Reset gespeichert.
case SUCCESS -> "redirect:/?registerSuccess";
case PASSWORD_MISMATCH -> "redirect:/?passwordMismatch";
case PASSWORD_TOO_SHORT -> "redirect:/?passwordPolicy";
case EMAIL_INVALID -> "redirect:/?emailInvalid";
@@ -88,7 +88,9 @@ public class UserServiceImpl implements UserService {
return RegistrationResult.EMAIL_TAKEN;
}
User user = new User(name, passwordEncoder.encode(password), email.trim(), "USER");
user.setEmailVerified(false);
// Die Registrierung ist bewusst nicht vom Versand oder Empfang einer E-Mail abhängig.
// Damit bleibt der Login auch bei verzögerter bzw. abgewiesener SMTP-Zustellung möglich.
user.setEmailVerified(true);
try {
userRepository.save(user);
} catch (DataIntegrityViolationException e) {
@@ -0,0 +1,9 @@
-- Die Anmeldung ist nicht mehr an die Zustellung einer E-Mail gebunden.
-- Bereits registrierte, bisher noch wartende Accounts werden dadurch nutzbar.
UPDATE users
SET email_verified = TRUE
WHERE email_verified = FALSE;
-- Verifikations-Links werden nicht mehr benötigt; gespeicherte Hashes entfernen.
DELETE FROM account_email_tokens
WHERE purpose = 'EMAIL_VERIFICATION';
+4 -33
View File
@@ -42,11 +42,9 @@
<div th:if="${param.error}" class="mf-alert err">Benutzername oder Passwort sind falsch.</div>
<div th:if="${param.logout}" class="mf-alert ok">Du hast dich erfolgreich abgemeldet.</div>
<div th:if="${param.registerSuccess}" class="mf-alert ok">Fast geschafft: Wir haben dir einen Bestätigungslink per E-Mail gesendet.</div>
<div th:if="${param.emailVerified}" class="mf-alert ok">Deine E-Mail-Adresse wurde bestätigt. Du kannst dich jetzt anmelden.</div>
<div th:if="${param.registerSuccess}" class="mf-alert ok">Dein Account wurde erstellt. Du kannst dich jetzt anmelden.</div>
<div th:if="${param.emailVerified}" class="mf-alert ok">Dein Account ist aktiv. Du kannst dich jetzt anmelden.</div>
<div th:if="${param.passwordReset}" class="mf-alert ok">Dein Passwort wurde geändert. Du kannst dich jetzt anmelden.</div>
<div th:if="${param.verificationSent}" class="mf-alert ok">Falls ein unbestätigter Account zu dieser Adresse existiert, wurde ein neuer Link versendet.</div>
<div th:if="${param.verificationInvalid}" class="mf-alert err">Dieser Bestätigungslink ist ungültig oder abgelaufen.</div>
<div th:if="${param.resetInvalid}" class="mf-alert err">Dieser Passwort-Link ist ungültig oder abgelaufen.</div>
<form th:action="@{/login}" method="post">
@@ -81,9 +79,7 @@
<div th:if="${param.passwordPolicy}" class="mf-alert err">Das Passwort muss mindestens 8 Zeichen lang sein.</div>
<div th:if="${param.emailInvalid}" class="mf-alert err">Bitte gib eine gültige E-Mail-Adresse an.</div>
<div th:if="${param.emailTaken}" class="mf-alert err">Diese E-Mail-Adresse wird bereits verwendet.</div>
<div th:if="${param.registerMailError}" class="mf-alert err">Der Account wurde angelegt, aber die Bestätigungs-E-Mail konnte nicht gesendet werden. Fordere unten einen neuen Link an.</div>
<div th:if="${param.registerSuccess}" class="mf-alert ok">Fast geschafft: Wir haben dir einen Bestätigungslink per E-Mail gesendet.</div>
<div th:if="${param.verificationSent}" class="mf-alert ok">Falls ein unbestätigter Account zu dieser Adresse existiert, wurde ein neuer Link versendet.</div>
<div th:if="${param.registerSuccess}" class="mf-alert ok">Account erstellt. Du kannst dich sofort anmelden.</div>
<form th:action="@{/register}" method="post">
<div class="mf-field">
@@ -106,20 +102,6 @@
<button class="neon-btn" type="submit">Account erstellen</button>
</form>
<div class="auth-resend">
<button id="resend-toggle" class="auth-resend-toggle" type="button"
aria-expanded="false" aria-controls="resend-form">
Bestätigungslink erneut senden
</button>
<form id="resend-form" th:action="@{/verification/resend}" method="post" hidden>
<label class="mf-label" for="resend-email">E-Mail-Adresse</label>
<div class="auth-resend-fields">
<input id="resend-email" class="mf-input" type="email" name="email" placeholder="E-Mail-Adresse" required autocomplete="email">
<button class="mf-btn outline" type="submit">Link senden</button>
</div>
</form>
</div>
<p class="mobile-switch">Schon dabei? <a data-goto="login">Zum Login</a></p>
</div>
</div>
@@ -159,17 +141,6 @@
if (toSignup) toSignup.addEventListener('click', function () { c.classList.add('active'); });
if (toLogin) toLogin.addEventListener('click', function () { c.classList.remove('active'); });
var resendToggle = document.getElementById('resend-toggle');
var resendForm = document.getElementById('resend-form');
if (resendToggle && resendForm) {
resendToggle.addEventListener('click', function () {
var expand = resendForm.hidden;
resendForm.hidden = !expand;
resendToggle.setAttribute('aria-expanded', String(expand));
if (expand) resendForm.querySelector('input').focus();
});
}
document.querySelectorAll('[data-goto="signup"]').forEach(function (el) {
el.addEventListener('click', function (e) { e.preventDefault(); c.classList.add('active'); });
});
@@ -179,7 +150,7 @@
// Bei Registrierungs-/Passwortfehler direkt das Signup-Panel öffnen
var qp = new URLSearchParams(window.location.search);
if (qp.has('signup') || qp.has('registerError') || qp.has('passwordMismatch') || qp.has('passwordPolicy') || qp.has('emailInvalid') || qp.has('emailTaken') || qp.has('registerMailError') || qp.has('registerSuccess') || qp.has('verificationSent')) {
if (qp.has('signup') || qp.has('registerError') || qp.has('passwordMismatch') || qp.has('passwordPolicy') || qp.has('emailInvalid') || qp.has('emailTaken') || qp.has('registerSuccess')) {
c.classList.add('active');
}
@@ -63,7 +63,7 @@
<tr><td>Nutzungs-/Lerndaten</td><td>Spielstände (Spiel, Deck, Punktzahl, Züge, Dauer, Zeitpunkt) sowie erreichte Erfolge/Achievements</td></tr>
<tr><td>Technische Daten</td><td>Sitzungs-Cookie zur Anmeldung, CSRF-Token; serverseitige Protokolldaten (z. B. IP-Adresse, Zeitpunkt, abgerufene Ressource) im Rahmen des technischen Betriebs</td></tr>
<tr><td>NeuralNet-Nachrichten (nur bei Aktivierung)</td><td>Verbindungsanfragen (anfragende und empfangende Person, Status, Zeitpunkt) sowie Nachrichten (Absender, Empfänger, Nachrichteninhalt, Sende- und Lesezeitpunkt/Lesebestätigung)</td></tr>
<tr><td>E-Mail-Bestätigung / Passwort-Reset</td><td>Bestätigungs- bzw. Reset-Token, ausschließlich als SHA-256-Hash gespeichert und 24 Stunden gültig, zur Verifizierung der E-Mail-Adresse bzw. zum Zurücksetzen des Passworts</td></tr>
<tr><td>Passwort-Reset</td><td>Reset-Token, ausschließlich als SHA-256-Hash gespeichert und 24 Stunden gültig, zum Zurücksetzen des Passworts</td></tr>
<tr><td>Anmeldeschutz</td><td>Fehlgeschlagene Anmeldeversuche (Benutzername, Zeitpunkt) zum Schutz vor automatisierten Anmeldeangriffen (Brute Force)</td></tr>
</table>
@@ -73,7 +73,7 @@
<li><strong>Technischer Betrieb, Sicherheit und Stabilität</strong> (Protokolldaten, Schutz vor Missbrauch) – Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse).</li>
<li><strong>Profilbild</strong> – auf Grundlage der freiwilligen Bereitstellung durch die Nutzer:in.</li>
<li><strong>NeuralNet-Community-Funktion</strong> (Verbindungsanfragen und Nachrichten) – nur nach ausdrücklicher Aktivierung (Opt-in, standardmäßig deaktiviert), Art. 6 Abs. 1 lit. a und b DSGVO. Die Funktion lässt sich jederzeit deaktivieren; beim Deaktivieren bzw. beim Löschen des Kontos werden die zugehörigen Nachrichten und Verbindungen per Datenbank-Kaskade mitgelöscht.</li>
<li><strong>E-Mail-Verifizierung und Passwort-Zurücksetzung</strong> (Versand von Bestätigungs- und Reset-Links) – Art. 6 Abs. 1 lit. b und f DSGVO.</li>
<li><strong>Passwort-Zurücksetzung</strong> (Versand von Reset-Links) – Art. 6 Abs. 1 lit. b und f DSGVO.</li>
</ul>
<h2>5. Cookies</h2>
@@ -88,7 +88,7 @@
<ul>
<li><strong>Kontodaten, Spielstände, Erfolge:</strong> solange das Nutzerkonto besteht; werden bei Kontolöschung entfernt.</li>
<li><strong>NeuralNet-Nachrichten und -Verbindungen:</strong> bis zur Löschung durch die Nutzer:in bzw. bis zur Kontolöschung (dann per Kaskade automatisch).</li>
<li><strong>Bestätigungs- und Passwort-Reset-Token:</strong> 24 Stunden; danach automatisch ungültig und nicht mehr verwendbar.</li>
<li><strong>Passwort-Reset-Token:</strong> 24 Stunden; danach automatisch ungültig und nicht mehr verwendbar.</li>
<li><strong>Fehlgeschlagene Anmeldeversuche:</strong> nur kurzfristig für den Brute-Force-Schutz, anschließend verworfen.</li>
<li><strong>Sitzungs-Cookie:</strong> verliert mit Ende der Sitzung bzw. der Abmeldung seine Gültigkeit.</li>
</ul>
@@ -112,7 +112,7 @@
<h2>7. Empfänger und Hosting</h2>
<p>
Die Daten werden auf einer vom Anbieter betriebenen Server-Infrastruktur verarbeitet und nicht zu
werblichen Zwecken an Dritte weitergegeben. Für den Versand von Bestätigungs- und Passwort-Reset-E-Mails
werblichen Zwecken an Dritte weitergegeben. Für den Versand von Passwort-Reset-E-Mails
wird der Mailserver <code>mail.sysnaptik.de</code> genutzt; dabei werden E-Mail-Adresse und der jeweilige
Link übermittelt. Alle statischen Inhalte (einschließlich der Diagramm-Bibliothek) werden ausschließlich
von der eigenen Infrastruktur ausgeliefert – es werden keine externen Content-Delivery-Networks oder
@@ -74,6 +74,7 @@ class UserServiceImplTest {
assertEquals("alice", saved.getUsername());
assertEquals("a@b.de", saved.getEmail());
assertEquals("USER", saved.getRole());
assertTrue(saved.isEmailVerified());
assertTrue(encoder.matches("passwort123", saved.getPassword()));
}