9 Commits

Author SHA1 Message Date
sysnaptik-main 063cbf944f CI: dangling images und alten build cache aufraeumen 2026-08-04 09:03:57 +00:00
AliceDarkest 0e66aff47d Allow immediate login after registration
mindflux-ci / build (push) Successful in 34s
2026-07-24 15:32:01 +02:00
AliceDarkest 6a7e90116a Link favicon explicitly on landing page
mindflux-ci / build (push) Successful in 35s
2026-07-22 14:23:21 +02:00
AliceDarkest 4a9d1f4ada Add Neural browser favicon
mindflux-ci / build (push) Successful in 34s
2026-07-22 14:18:57 +02:00
AliceDarkest dbed1ad79f Formulate public pricing neutrally
mindflux-ci / build (push) Successful in 34s
2026-07-22 14:14:11 +02:00
AliceDarkest 69b15aa4df Remove temporary Klaus account inspection
mindflux-ci / build (push) Successful in 20s
2026-07-22 14:11:41 +02:00
AliceDarkest 0b3e5d39cf Fix temporary account inspection workflow
mindflux-ci / build (push) Successful in 15s
2026-07-22 14:09:36 +02:00
AliceDarkest 1c46eb2421 Temporary: inspect Klaus account 2026-07-22 14:08:18 +02:00
AliceDarkest 955bdb7057 Verbessere Einstieg und TechTacular-Fragen
mindflux-ci / build (push) Successful in 34s
2026-07-22 13:44:45 +02:00
17 changed files with 156 additions and 65 deletions
+8
View File
@@ -34,7 +34,15 @@ jobs:
ALTER USER mindflux WITH PASSWORD :'new_db_password';
SQL
docker compose -f docker-compose.yml pull
<<<<<<< HEAD
docker compose -f docker-compose.yml up -d --wait --wait-timeout 90 || {
docker logs --tail 200 mindflux >&2
exit 1
}
=======
docker compose -f docker-compose.yml up -d
- name: Cleanup
run: |
docker image prune -f
docker builder prune -f --filter "until=168h"
>>>>>>> f79df17 (CI: dangling images und alten build cache aufraeumen)
+18
View File
@@ -6,7 +6,25 @@ 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
bereits gecachte Standard-Globus-Symbole im Browser-Tab.
- **Preismodell offen formuliert:** Die öffentliche Einstiegsseite verspricht keine
kostenlose Nutzung mehr; Beschreibung, Registrierungs-Teaser und Account-Hinweis
bleiben bewusst preisneutral.
- **Produktzweck klarer:** Die Login-Seite erklärt nun Zielgruppe, Lernformen und
den Nutzen eines Accounts; im Dashboard fasst ein kompakter Einstieg den Lernzweck
zusammen.
- **TechTacular-Fragen überspringbar:** Eine noch nicht beantwortete Frage kann per
×, Klick außerhalb oder Escape folgenlos geschlossen und später wieder geöffnet
werden.
- **Light-Mode-Fundament:** zentrale UI-Effekte sind jetzt als Theme-Primitives
organisiert. Dark Mode behält Neon-Emissionen; Light Mode nutzt warme Elevation,
entfernt Text-/Icon-Halos und erhält einen sichtbaren Scrollbar-Thumb. Native
+9
View File
@@ -303,3 +303,12 @@ Zusatzfunde (neu):
- Der Registrierungsabschluss meldet keinen Versand-Erfolg mehr, falls der gerade
angelegte Benutzer beim Versand nicht geladen werden kann oder bereits als bestätigt
vorliegt; der Grund erscheint als `VERIFICATION_SKIPPED` im Betriebslog.
## Nachtrag 2026-07-22 — Nutzerfeedback (erledigt)
- **TechTacular:** Unbeantwortete Fragen lassen sich per ×, Klick auf den Hintergrund
oder Escape schließen und später erneut öffnen; sie werden dabei weder gewertet noch
als beantwortet markiert.
- **Produktverständnis:** Vor dem Login erklärt eine sichtbare Einordnung Zielgruppe,
Lernformen und Nutzen der Plattform. Das Dashboard wiederholt den Zweck mit einem
kompakten „Was du hier machst“-Abschnitt.
@@ -21,7 +21,7 @@ public class SecurityConfig {
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/register", "/forgot-password", "/reset-password", "/verify-email", "/verification/resend", "/impressum", "/nutzungsbedingungen", "/datenschutz", "/richtlinien", "/css/**", "/js/**", "/img/**").permitAll()
.requestMatchers("/", "/login", "/register", "/forgot-password", "/reset-password", "/verify-email", "/verification/resend", "/impressum", "/nutzungsbedingungen", "/datenschutz", "/richtlinien", "/favicon.ico", "/css/**", "/js/**", "/img/**").permitAll()
.requestMatchers("/actuator/health").permitAll() // Container-Healthcheck
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/api/users/**").hasRole("ADMIN")
@@ -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();
};
}
@@ -0,0 +1,23 @@
package com.example.mindflux.controller;
import java.time.Duration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FaviconController {
@GetMapping(value = "/favicon.ico", produces = MediaType.IMAGE_PNG_VALUE)
public ResponseEntity<Resource> favicon() {
Resource emblem = new ClassPathResource("static/img/mindflux-emblem.png");
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofDays(30)).cachePublic())
.body(emblem);
}
}
@@ -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';
+8
View File
@@ -6,6 +6,12 @@
@property --mf-ang { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
.auth-stage { width:100%; display:flex; flex-direction:column; align-items:center; gap:1.4rem; }
.auth-intro { width:100%; max-width:840px; padding:1.25rem 1.4rem; border:1px solid var(--border); border-radius:16px; background:var(--surface); box-shadow:var(--g-soft); }
.auth-intro h1 { color:var(--heading); font-size:clamp(1.35rem,3vw,1.85rem); line-height:1.18; margin:.35rem 0 .6rem; }
.auth-intro p { color:var(--muted); line-height:1.6; max-width:72ch; }
.auth-intro ul { display:grid; grid-template-columns:repeat(3,1fr); gap:.7rem 1rem; list-style:none; margin-top:.9rem; padding:0; }
.auth-intro li { color:var(--muted); font-size:.82rem; line-height:1.45; }
.auth-intro li b { color:var(--accent); }
/* ---- Dual-Panel-Container (Bild 2) ---- */
.auth-container {
@@ -149,6 +155,8 @@
/* ---- Responsiv: gestapelt, ohne Overlay ---- */
@media (max-width:760px) {
.auth-intro { padding:1rem; }
.auth-intro ul { grid-template-columns:1fr; gap:.45rem; }
.auth-container { max-width:420px; min-height:auto; }
.overlay-container { display:none; }
.form-box {
@@ -216,6 +216,9 @@ body.tt-body {
}
.tt-q-cat { color: var(--muted); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.85rem; }
.tt-q-points { color: var(--accent); font-weight: 800; font-size: 1.3rem; }
.tt-q-close { margin-left:.35rem; width:32px; height:32px; border:1px solid var(--border); border-radius:8px; background:transparent; color:var(--muted); font:400 1.5rem/1 inherit; cursor:pointer; }
.tt-q-close:hover { color:var(--accent); border-color:var(--border-accent); background:var(--surface-2); }
.tt-q-close:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.tt-q-instruction { color: var(--muted); font-size: 0.85rem; font-weight: 700; margin-bottom: 0.35rem; }
.tt-q-text {
@@ -21,6 +21,7 @@
const qAnswers = document.getElementById('tt-q-answers');
const qFeedback = document.getElementById('tt-q-feedback');
const qNext = document.getElementById('tt-q-next');
const qClose = document.getElementById('tt-q-close');
const jokerBtn = document.getElementById('tt-fifty-fifty');
const jokerNote = document.getElementById('tt-joker-note');
@@ -212,6 +213,12 @@
}
}
function dismissQuestion() {
if (!current || current.answered) return;
current = null;
qOverlay.hidden = true;
}
async function finishGame() {
if (!running) return;
running = false;
@@ -301,7 +308,16 @@
finishBtn.addEventListener('click', finishGame);
cancelBtn.addEventListener('click', cancelGame);
qNext.addEventListener('click', closeQuestion);
qClose.addEventListener('click', dismissQuestion);
jokerBtn.addEventListener('click', useFiftyFifty);
qOverlay.addEventListener('click', (event) => {
if (event.target === qOverlay) dismissQuestion();
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !qOverlay.hidden) {
dismissQuestion();
}
});
loadStats();
newGame();
+18 -1
View File
@@ -46,6 +46,11 @@
.challenge .bolt { color:var(--accent); flex:0 0 auto; filter:drop-shadow(0 0 6px var(--glow-s)); }
.challenge h5 { color:var(--accent); font-size:.82rem; letter-spacing:.04em; text-transform:uppercase; margin-bottom:.3rem; }
.challenge p { color:var(--muted); font-size:.78rem; line-height:1.45; }
.platform-intro { margin-bottom:1.1rem; }
.platform-intro h2 { color:var(--heading); font-size:1.15rem; margin:.25rem 0 .45rem; }
.platform-intro p { color:var(--muted); line-height:1.55; font-size:.88rem; }
.platform-intro ul { display:flex; flex-wrap:wrap; gap:.45rem .75rem; list-style:none; margin-top:.8rem; padding:0; }
.platform-intro li { color:var(--text); font-size:.76rem; padding:.32rem .55rem; border:1px solid var(--border); border-radius:99px; background:var(--surface-2); }
</style>
</head>
<body>
@@ -79,7 +84,19 @@
</header>
<!-- ===== Spielkarten ===== -->
<main class="mf-main"><div class="mf-grid" id="grid"></div></main>
<main class="mf-main">
<section class="mf-panel platform-intro" aria-labelledby="platform-intro-title">
<div class="mf-eyebrow">Dein Lernbereich</div>
<h2 id="platform-intro-title">Was du mit SysnaptiK Neural machst</h2>
<p>Du bereitest dich praxisnah auf AP1 und AP2 der Fachinformatiker-Ausbildung vor: Wissen kurz einordnen, in Lernspielen anwenden und deinen Fortschritt gezielt weiterentwickeln.</p>
<ul aria-label="Lernangebote">
<li>Interaktive IT-Szenarien</li>
<li>Prüfungsthemen &amp; Übungen</li>
<li>Gespeicherter Lernfortschritt</li>
</ul>
</section>
<div class="mf-grid" id="grid"></div>
</main>
<!-- ===== Rechte Spalte ===== -->
<aside class="mf-rightbar">
+19 -35
View File
@@ -4,6 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SysnaptiK Neural – Deine Zukunft beginnt hier</title>
<link rel="icon" type="image/png" href="/img/mindflux-emblem.png?v=2">
<link rel="apple-touch-icon" href="/img/mindflux-emblem.png?v=2">
<link rel="stylesheet" href="/css/theme.css?v=4">
<script src="/js/theme.js?v=5"></script>
<link rel="stylesheet" href="/css/auth.css">
@@ -15,6 +17,17 @@
<div class="mf-center">
<div class="auth-stage">
<section class="auth-intro" aria-labelledby="auth-intro-title">
<p class="mf-eyebrow">IT-Ausbildung · AP1 &amp; AP2</p>
<h1 id="auth-intro-title">Lerne Fachinformatik praktisch statt nur zu lesen.</h1>
<p>SysnaptiK Neural ist eine Lernplattform für angehende Fachinformatiker:innen. Sie verbindet kurze Erklärungen mit interaktiven Spielen, Prüfungstraining und sichtbarem Lernfortschritt.</p>
<ul>
<li><b>Trainieren:</b> Netzwerke, Programmierung, Security und IT-Service anwenden.</li>
<li><b>Verstehen:</b> FinalProtocol bündelt AP1- und AP2-Themen mit Übungen.</li>
<li><b>Dranbleiben:</b> Ein Account speichert Fortschritt, Bestwerte und Lernziele.</li>
</ul>
</section>
<div class="auth-container" id="authContainer">
<!-- ===== Sign In ===== -->
@@ -29,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">
@@ -60,7 +71,7 @@
<span class="mark"></span>
<b>SYSNAPTIK NEURAL</b>
<h2>Account erstellen</h2>
<span class="sub">Kostenlos – und in 30 Sekunden startklar.</span>
<span class="sub">In 30 Sekunden startklar.</span>
</div>
<div th:if="${param.registerError}" class="mf-alert err">Benutzername bereits vergeben.</div>
@@ -68,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">
@@ -93,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>
@@ -125,7 +120,7 @@
<div class="overlay-panel overlay-right">
<span class="o-emblem"></span>
<h3>Neu bei SysnaptiK Neural?</h3>
<p>Erstelle deinen kostenlosen Account und schalte die volle Lernplattform frei.</p>
<p>Erstelle deinen Account und schalte die volle Lernplattform frei.</p>
<button type="button" class="ghost-btn" id="toSignup">Account erstellen</button>
</div>
</div>
@@ -146,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'); });
});
@@ -166,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
@@ -47,6 +47,7 @@
<div class="tt-q-head">
<span id="tt-q-cat" class="tt-q-cat">Kategorie</span>
<span id="tt-q-points" class="tt-q-points">100</span>
<button type="button" id="tt-q-close" class="tt-q-close" aria-label="Frage ohne Antwort schließen" title="Frage ohne Antwort schließen">×</button>
</div>
<p class="tt-q-instruction">Antwort – finde die passende Frage:</p>
<h2 id="tt-q-text" class="tt-q-text"></h2>
@@ -76,7 +77,7 @@
</div>
</div>
<script src="/js/techtacular.js?v=4" defer></script>
<script src="/js/techtacular.js?v=5" defer></script>
<footer th:replace="~{fragments/layout :: footer}"></footer>
</body>
</html>
@@ -3,6 +3,7 @@ package com.example.mindflux;
import com.example.mindflux.config.SecurityConfig;
import com.example.mindflux.controller.AdminUserController;
import com.example.mindflux.controller.DashboardController;
import com.example.mindflux.controller.FaviconController;
import com.example.mindflux.controller.ProgressController;
import com.example.mindflux.controller.PlatformStatusController;
import com.example.mindflux.dto.ProgressData;
@@ -28,9 +29,10 @@ import static org.springframework.security.test.web.servlet.request.SecurityMock
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
/** Absichert die zentralen Security-Regeln: Login-Zwang, Admin-Schutz, gelöschter Legacy-Endpunkt. */
@WebMvcTest(controllers = {DashboardController.class, ProgressController.class, PlatformStatusController.class, AdminUserController.class})
@WebMvcTest(controllers = {DashboardController.class, ProgressController.class, PlatformStatusController.class, AdminUserController.class, FaviconController.class})
@Import(SecurityConfig.class)
class SecurityRulesTest {
@@ -58,6 +60,13 @@ class SecurityRulesTest {
.andExpect(status().is3xxRedirection());
}
@Test
void faviconIstOhneAnmeldungErreichbar() throws Exception {
mockMvc.perform(get("/favicon.ico"))
.andExpect(status().isOk())
.andExpect(content().contentType("image/png"));
}
@Test
@WithMockUser(username = "alice")
void eingeloggteNutzerSehenDasDashboard() throws Exception {
@@ -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()));
}