This commit is contained in:
@@ -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';
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user