Compare commits
9 Commits
3bc6c8f75c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 063cbf944f | |||
| 0e66aff47d | |||
| 6a7e90116a | |||
| 4a9d1f4ada | |||
| dbed1ad79f | |||
| 69b15aa4df | |||
| 0b3e5d39cf | |||
| 1c46eb2421 | |||
| 955bdb7057 |
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
|
||||
@@ -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 & Übungen</li>
|
||||
<li>Gespeicherter Lernfortschritt</li>
|
||||
</ul>
|
||||
</section>
|
||||
<div class="mf-grid" id="grid"></div>
|
||||
</main>
|
||||
|
||||
<!-- ===== Rechte Spalte ===== -->
|
||||
<aside class="mf-rightbar">
|
||||
|
||||
@@ -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 & 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()));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user