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
|
## [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
|
### Light Mode & Design
|
||||||
- **Browser-Icon:** Das Neural-Emblem wird unter `/favicon.ico` ausgeliefert und
|
- **Browser-Icon:** Das Neural-Emblem wird unter `/favicon.ico` ausgeliefert und
|
||||||
direkt auf der Startseite verlinkt. Die Versionierung der Bild-URL ersetzt auch
|
direkt auf der Startseite verlinkt. Die Versionierung der Bild-URL ersetzt auch
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ public class SecurityConfig {
|
|||||||
.withUsername(user.getUsername())
|
.withUsername(user.getUsername())
|
||||||
.password(user.getPassword())
|
.password(user.getPassword())
|
||||||
.roles(user.getRole()) // Achtung: Nur ein String, z.B. "USER" oder "ADMIN"
|
.roles(user.getRole()) // Achtung: Nur ein String, z.B. "USER" oder "ADMIN"
|
||||||
.disabled(!user.isEmailVerified())
|
|
||||||
.build();
|
.build();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
package com.example.mindflux.controller;
|
package com.example.mindflux.controller;
|
||||||
|
|
||||||
import com.example.mindflux.service.RegistrationResult;
|
import com.example.mindflux.service.RegistrationResult;
|
||||||
import com.example.mindflux.service.AccountMailService;
|
|
||||||
import com.example.mindflux.service.UserService;
|
import com.example.mindflux.service.UserService;
|
||||||
import org.springframework.stereotype.Controller;
|
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.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
@@ -13,14 +10,10 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
@Controller
|
@Controller
|
||||||
public class RegisterController {
|
public class RegisterController {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(RegisterController.class);
|
|
||||||
|
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final AccountMailService accountMailService;
|
|
||||||
|
|
||||||
public RegisterController(UserService userService, AccountMailService accountMailService) {
|
public RegisterController(UserService userService) {
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.accountMailService = accountMailService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/register")
|
@GetMapping("/register")
|
||||||
@@ -36,18 +29,9 @@ public class RegisterController {
|
|||||||
@RequestParam(required = false) String confirmPassword) {
|
@RequestParam(required = false) String confirmPassword) {
|
||||||
RegistrationResult result = userService.register(username, email, password, confirmPassword);
|
RegistrationResult result = userService.register(username, email, password, confirmPassword);
|
||||||
return switch (result) {
|
return switch (result) {
|
||||||
case SUCCESS -> {
|
// Ein SMTP-Problem darf die Nutzung eines neu angelegten Accounts nicht blockieren.
|
||||||
try {
|
// Die E-Mail-Adresse wird weiterhin für den Passwort-Reset gespeichert.
|
||||||
var user = userService.findByUsernameIgnoreCase(username.trim());
|
case SUCCESS -> "redirect:/?registerSuccess";
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case PASSWORD_MISMATCH -> "redirect:/?passwordMismatch";
|
case PASSWORD_MISMATCH -> "redirect:/?passwordMismatch";
|
||||||
case PASSWORD_TOO_SHORT -> "redirect:/?passwordPolicy";
|
case PASSWORD_TOO_SHORT -> "redirect:/?passwordPolicy";
|
||||||
case EMAIL_INVALID -> "redirect:/?emailInvalid";
|
case EMAIL_INVALID -> "redirect:/?emailInvalid";
|
||||||
|
|||||||
@@ -88,7 +88,9 @@ public class UserServiceImpl implements UserService {
|
|||||||
return RegistrationResult.EMAIL_TAKEN;
|
return RegistrationResult.EMAIL_TAKEN;
|
||||||
}
|
}
|
||||||
User user = new User(name, passwordEncoder.encode(password), email.trim(), "USER");
|
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 {
|
try {
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
} catch (DataIntegrityViolationException e) {
|
} 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.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.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.registerSuccess}" class="mf-alert ok">Dein Account wurde erstellt. Du kannst dich jetzt anmelden.</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.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.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>
|
<div th:if="${param.resetInvalid}" class="mf-alert err">Dieser Passwort-Link ist ungültig oder abgelaufen.</div>
|
||||||
|
|
||||||
<form th:action="@{/login}" method="post">
|
<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.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.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.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">Account erstellt. Du kannst dich sofort anmelden.</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>
|
|
||||||
|
|
||||||
<form th:action="@{/register}" method="post">
|
<form th:action="@{/register}" method="post">
|
||||||
<div class="mf-field">
|
<div class="mf-field">
|
||||||
@@ -106,20 +102,6 @@
|
|||||||
<button class="neon-btn" type="submit">Account erstellen</button>
|
<button class="neon-btn" type="submit">Account erstellen</button>
|
||||||
</form>
|
</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>
|
<p class="mobile-switch">Schon dabei? <a data-goto="login">Zum Login</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,17 +141,6 @@
|
|||||||
if (toSignup) toSignup.addEventListener('click', function () { c.classList.add('active'); });
|
if (toSignup) toSignup.addEventListener('click', function () { c.classList.add('active'); });
|
||||||
if (toLogin) toLogin.addEventListener('click', function () { c.classList.remove('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) {
|
document.querySelectorAll('[data-goto="signup"]').forEach(function (el) {
|
||||||
el.addEventListener('click', function (e) { e.preventDefault(); c.classList.add('active'); });
|
el.addEventListener('click', function (e) { e.preventDefault(); c.classList.add('active'); });
|
||||||
});
|
});
|
||||||
@@ -179,7 +150,7 @@
|
|||||||
|
|
||||||
// Bei Registrierungs-/Passwortfehler direkt das Signup-Panel öffnen
|
// Bei Registrierungs-/Passwortfehler direkt das Signup-Panel öffnen
|
||||||
var qp = new URLSearchParams(window.location.search);
|
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');
|
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>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>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>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>
|
<tr><td>Anmeldeschutz</td><td>Fehlgeschlagene Anmeldeversuche (Benutzername, Zeitpunkt) zum Schutz vor automatisierten Anmeldeangriffen (Brute Force)</td></tr>
|
||||||
</table>
|
</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>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>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>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>
|
</ul>
|
||||||
|
|
||||||
<h2>5. Cookies</h2>
|
<h2>5. Cookies</h2>
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><strong>Kontodaten, Spielstände, Erfolge:</strong> solange das Nutzerkonto besteht; werden bei Kontolöschung entfernt.</li>
|
<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>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>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>
|
<li><strong>Sitzungs-Cookie:</strong> verliert mit Ende der Sitzung bzw. der Abmeldung seine Gültigkeit.</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
<h2>7. Empfänger und Hosting</h2>
|
<h2>7. Empfänger und Hosting</h2>
|
||||||
<p>
|
<p>
|
||||||
Die Daten werden auf einer vom Anbieter betriebenen Server-Infrastruktur verarbeitet und nicht zu
|
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
|
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
|
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
|
von der eigenen Infrastruktur ausgeliefert – es werden keine externen Content-Delivery-Networks oder
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ class UserServiceImplTest {
|
|||||||
assertEquals("alice", saved.getUsername());
|
assertEquals("alice", saved.getUsername());
|
||||||
assertEquals("a@b.de", saved.getEmail());
|
assertEquals("a@b.de", saved.getEmail());
|
||||||
assertEquals("USER", saved.getRole());
|
assertEquals("USER", saved.getRole());
|
||||||
|
assertTrue(saved.isEmailVerified());
|
||||||
assertTrue(encoder.matches("passwort123", saved.getPassword()));
|
assertTrue(encoder.matches("passwort123", saved.getPassword()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user