diff --git a/CHANGELOG.md b/CHANGELOG.md index d72d87b..facb7ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ Versionierung [semantisch](https://semver.org/lang/de/). ## [Unreleased] – 2026-07-14 · SysnaptiK Neural Rebrand +### Konto & Sicherheit +- **E-Mail-Bestätigung:** Neue Accounts erhalten einen einmaligen, 24 Stunden + gültigen Bestätigungslink und werden erst nach erfolgreicher Verifikation zum + Login freigeschaltet. Bestehende Accounts bleiben ohne Unterbrechung bestätigt. +- **Passwort-Reset per E-Mail:** Über „Passwort vergessen?“ können bestätigte + Nutzerinnen und Nutzer einen zeitlich begrenzten Reset-Link anfordern und ihr + Passwort selbst neu vergeben. Token werden ausschließlich gehasht gespeichert. +- **Admin-Reset vereinheitlicht:** Manuelle Passwort-Resets im Adminbereich + verwenden jetzt verdeckte Eingabefelder und dieselbe Mindestlänge von acht Zeichen. + ### Marke & Plattform - **Neural Lightmode & Kontrast:** Die helle Plattformvariante ist wieder ein vollständiges Mint-/Blau-Neural-Theme, inklusive Sidebar, Karten und Login. diff --git a/pom.xml b/pom.xml index 6ee0ae7..7229a9c 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,12 @@ spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-mail + + org.springframework.boot diff --git a/src/main/java/com/example/mindflux/config/SecurityConfig.java b/src/main/java/com/example/mindflux/config/SecurityConfig.java index abffaf3..7438707 100644 --- a/src/main/java/com/example/mindflux/config/SecurityConfig.java +++ b/src/main/java/com/example/mindflux/config/SecurityConfig.java @@ -20,7 +20,7 @@ public class SecurityConfig { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth - .requestMatchers("/", "/login", "/register", "/impressum", "/nutzungsbedingungen", "/datenschutz", "/richtlinien", "/css/**", "/js/**", "/img/**").permitAll() + .requestMatchers("/", "/login", "/register", "/forgot-password", "/reset-password", "/verify-email", "/verification/resend", "/impressum", "/nutzungsbedingungen", "/datenschutz", "/richtlinien", "/css/**", "/js/**", "/img/**").permitAll() .requestMatchers("/actuator/health").permitAll() // Container-Healthcheck .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/api/users/**").hasRole("ADMIN") @@ -63,6 +63,7 @@ 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(); }; } diff --git a/src/main/java/com/example/mindflux/controller/AccountRecoveryController.java b/src/main/java/com/example/mindflux/controller/AccountRecoveryController.java new file mode 100644 index 0000000..2e09e31 --- /dev/null +++ b/src/main/java/com/example/mindflux/controller/AccountRecoveryController.java @@ -0,0 +1,75 @@ +package com.example.mindflux.controller; + +import com.example.mindflux.service.AccountMailService; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +public class AccountRecoveryController { + + private final AccountMailService accountMailService; + + public AccountRecoveryController(AccountMailService accountMailService) { + this.accountMailService = accountMailService; + } + + @GetMapping("/forgot-password") + public String forgotPassword() { + return "forgot-password"; + } + + @PostMapping("/forgot-password") + public String requestPasswordReset(@RequestParam String email) { + try { + accountMailService.requestPasswordReset(email); + } catch (RuntimeException ignored) { + // Die Antwort bleibt absichtlich identisch: keine E-Mail- oder Account-Informationen preisgeben. + } + // Einheitliche Antwort, damit nicht erkennbar ist, welche Adressen registriert sind. + return "redirect:/forgot-password?sent"; + } + + @PostMapping("/verification/resend") + public String resendVerification(@RequestParam String email) { + try { + accountMailService.resendVerification(email); + } catch (RuntimeException ignored) { + // Auch bei einem SMTP-Fehler keine Information über einen möglichen Account verraten. + } + return "redirect:/?verificationSent"; + } + + @GetMapping("/verify-email") + public String verifyEmail(@RequestParam(required = false) String token) { + return accountMailService.verifyEmail(token) + ? "redirect:/?emailVerified" + : "redirect:/?verificationInvalid"; + } + + @GetMapping("/reset-password") + public String showResetPassword(@RequestParam(required = false) String token, Model model) { + if (!accountMailService.isResetTokenValid(token)) { + return "redirect:/?resetInvalid"; + } + model.addAttribute("token", token); + return "reset-password"; + } + + @PostMapping("/reset-password") + public String resetPassword(@RequestParam String token, + @RequestParam String password, + @RequestParam String confirmPassword) { + if (password.length() < 8) { + return "redirect:/reset-password?token=" + token + "&error=policy"; + } + if (!password.equals(confirmPassword)) { + return "redirect:/reset-password?token=" + token + "&error=mismatch"; + } + return accountMailService.resetPassword(token, password, confirmPassword) + ? "redirect:/?passwordReset" + : "redirect:/?resetInvalid"; + } +} diff --git a/src/main/java/com/example/mindflux/controller/AdminUserController.java b/src/main/java/com/example/mindflux/controller/AdminUserController.java index 4c95f54..1787b68 100644 --- a/src/main/java/com/example/mindflux/controller/AdminUserController.java +++ b/src/main/java/com/example/mindflux/controller/AdminUserController.java @@ -64,12 +64,13 @@ public class AdminUserController { /** Direktes Zurücksetzen des Passworts eines Nutzers (Admin-Recovery, ohne E-Mail). */ @PostMapping("/users/{id}/reset-password") public String resetPassword(@PathVariable Long id, @RequestParam String newPassword) { - if (newPassword != null && !newPassword.isBlank()) { - User user = userService.findById(id) - .orElseThrow(() -> new IllegalArgumentException("Ungültige User-ID: " + id)); - user.setPassword(passwordEncoder.encode(newPassword.trim())); - userService.save(user); + if (newPassword == null || newPassword.trim().length() < 8) { + return "redirect:/admin/users?passwordPolicy"; } + User user = userService.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Ungültige User-ID: " + id)); + user.setPassword(passwordEncoder.encode(newPassword.trim())); + userService.save(user); return "redirect:/admin/users"; } diff --git a/src/main/java/com/example/mindflux/controller/RegisterController.java b/src/main/java/com/example/mindflux/controller/RegisterController.java index 1d8ca26..86def8c 100644 --- a/src/main/java/com/example/mindflux/controller/RegisterController.java +++ b/src/main/java/com/example/mindflux/controller/RegisterController.java @@ -1,6 +1,7 @@ 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.springframework.web.bind.annotation.GetMapping; @@ -11,9 +12,11 @@ import org.springframework.web.bind.annotation.RequestParam; public class RegisterController { private final UserService userService; + private final AccountMailService accountMailService; - public RegisterController(UserService userService) { + public RegisterController(UserService userService, AccountMailService accountMailService) { this.userService = userService; + this.accountMailService = accountMailService; } @GetMapping("/register") @@ -29,10 +32,18 @@ public class RegisterController { @RequestParam(required = false) String confirmPassword) { RegistrationResult result = userService.register(username, email, password, confirmPassword); return switch (result) { - case SUCCESS -> "redirect:/?registerSuccess"; + case SUCCESS -> { + try { + accountMailService.sendVerificationFor(userService.findByUsernameIgnoreCase(username.trim())); + yield "redirect:/?registerSuccess"; + } catch (RuntimeException e) { + yield "redirect:/?registerMailError"; + } + } case PASSWORD_MISMATCH -> "redirect:/?passwordMismatch"; case PASSWORD_TOO_SHORT -> "redirect:/?passwordPolicy"; case EMAIL_INVALID -> "redirect:/?emailInvalid"; + case EMAIL_TAKEN -> "redirect:/?emailTaken"; case USERNAME_INVALID, USERNAME_TAKEN -> "redirect:/?registerError"; }; } diff --git a/src/main/java/com/example/mindflux/model/AccountEmailToken.java b/src/main/java/com/example/mindflux/model/AccountEmailToken.java new file mode 100644 index 0000000..eaf4cd7 --- /dev/null +++ b/src/main/java/com/example/mindflux/model/AccountEmailToken.java @@ -0,0 +1,43 @@ +package com.example.mindflux.model; + +import jakarta.persistence.*; +import java.time.Instant; + +@Entity +@Table(name = "account_email_tokens") +public class AccountEmailToken { + public enum Purpose { EMAIL_VERIFICATION, PASSWORD_RESET } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "token_hash", nullable = false, unique = true, length = 64) + private String tokenHash; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 32) + private Purpose purpose; + + @Column(name = "expires_at", nullable = false) + private Instant expiresAt; + + @Column(name = "consumed_at") + private Instant consumedAt; + + public Long getId() { return id; } + public User getUser() { return user; } + public void setUser(User user) { this.user = user; } + public String getTokenHash() { return tokenHash; } + public void setTokenHash(String tokenHash) { this.tokenHash = tokenHash; } + public Purpose getPurpose() { return purpose; } + public void setPurpose(Purpose purpose) { this.purpose = purpose; } + public Instant getExpiresAt() { return expiresAt; } + public void setExpiresAt(Instant expiresAt) { this.expiresAt = expiresAt; } + public Instant getConsumedAt() { return consumedAt; } + public void setConsumedAt(Instant consumedAt) { this.consumedAt = consumedAt; } +} diff --git a/src/main/java/com/example/mindflux/model/User.java b/src/main/java/com/example/mindflux/model/User.java index 33b848f..4fa2496 100644 --- a/src/main/java/com/example/mindflux/model/User.java +++ b/src/main/java/com/example/mindflux/model/User.java @@ -16,6 +16,9 @@ public class User { private String email; private String role; + @Column(name = "email_verified", nullable = false) + private boolean emailVerified = true; + @Column(name = "profile_picture") private String profilePicture; @@ -47,6 +50,9 @@ public class User { public String getRole() { return role; } public void setRole(String r) { this.role = r; } + public boolean isEmailVerified() { return emailVerified; } + public void setEmailVerified(boolean emailVerified) { this.emailVerified = emailVerified; } + public String getProfilePicture() { return profilePicture; } public void setProfilePicture(String pictureUrl) { this.profilePicture = pictureUrl; } } diff --git a/src/main/java/com/example/mindflux/repository/AccountEmailTokenRepository.java b/src/main/java/com/example/mindflux/repository/AccountEmailTokenRepository.java new file mode 100644 index 0000000..25c78e9 --- /dev/null +++ b/src/main/java/com/example/mindflux/repository/AccountEmailTokenRepository.java @@ -0,0 +1,14 @@ +package com.example.mindflux.repository; + +import com.example.mindflux.model.AccountEmailToken; +import com.example.mindflux.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface AccountEmailTokenRepository extends JpaRepository { + Optional findByTokenHashAndPurposeAndConsumedAtIsNull( + String tokenHash, AccountEmailToken.Purpose purpose); + + void deleteByUserAndPurpose(User user, AccountEmailToken.Purpose purpose); +} diff --git a/src/main/java/com/example/mindflux/repository/UserRepository.java b/src/main/java/com/example/mindflux/repository/UserRepository.java index ad251e7..2edb71e 100644 --- a/src/main/java/com/example/mindflux/repository/UserRepository.java +++ b/src/main/java/com/example/mindflux/repository/UserRepository.java @@ -2,6 +2,7 @@ package com.example.mindflux.repository; import com.example.mindflux.model.User; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface UserRepository extends JpaRepository { @@ -12,6 +13,8 @@ public interface UserRepository extends JpaRepository { // Case-insensitive Suche: Login und Registrierung sollen unabhängig von Groß-/Kleinschreibung greifen Optional findByUsernameIgnoreCase(String username); + List findAllByEmailIgnoreCase(String email); + // Benutzer anhand des Benutzernamens löschen void deleteByUsername(String username); -} \ No newline at end of file +} diff --git a/src/main/java/com/example/mindflux/service/AccountMailService.java b/src/main/java/com/example/mindflux/service/AccountMailService.java new file mode 100644 index 0000000..f1bfdd6 --- /dev/null +++ b/src/main/java/com/example/mindflux/service/AccountMailService.java @@ -0,0 +1,167 @@ +package com.example.mindflux.service; + +import com.example.mindflux.model.AccountEmailToken; +import com.example.mindflux.model.User; +import com.example.mindflux.repository.AccountEmailTokenRepository; +import com.example.mindflux.repository.UserRepository; +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.List; +import java.util.Optional; + +@Service +public class AccountMailService { + + private static final Duration TOKEN_LIFETIME = Duration.ofHours(24); + private final AccountEmailTokenRepository tokenRepository; + private final UserRepository userRepository; + private final JavaMailSender mailSender; + private final BCryptPasswordEncoder passwordEncoder; + private final SecureRandom secureRandom = new SecureRandom(); + private final String publicUrl; + private final String fromAddress; + + public AccountMailService(AccountEmailTokenRepository tokenRepository, + UserRepository userRepository, + JavaMailSender mailSender, + BCryptPasswordEncoder passwordEncoder, + @Value("${mindflux.public-url}") String publicUrl, + @Value("${mindflux.mail.from}") String fromAddress) { + this.tokenRepository = tokenRepository; + this.userRepository = userRepository; + this.mailSender = mailSender; + this.passwordEncoder = passwordEncoder; + this.publicUrl = publicUrl.replaceAll("/+$", ""); + this.fromAddress = fromAddress; + } + + @Transactional + public void sendVerificationFor(User user) { + if (user == null || user.isEmailVerified()) { + return; + } + String token = issueToken(user, AccountEmailToken.Purpose.EMAIL_VERIFICATION); + send(user.getEmail(), "Bestätige deinen Mindflux-Account", + "Hallo " + user.getUsername() + ",\n\n" + + "bitte bestätige deine E-Mail-Adresse, um deinen Account freizuschalten:\n" + + publicUrl + "/verify-email?token=" + token + "\n\n" + + "Der Link ist 24 Stunden gültig und kann nur einmal verwendet werden.\n\n" + + "Falls du dich nicht registriert hast, kannst du diese E-Mail ignorieren."); + } + + @Transactional + public void resendVerification(String email) { + uniqueUserForEmail(email).filter(user -> !user.isEmailVerified()).ifPresent(this::sendVerificationFor); + } + + @Transactional + public void requestPasswordReset(String email) { + uniqueUserForEmail(email).filter(User::isEmailVerified).ifPresent(user -> { + String token = issueToken(user, AccountEmailToken.Purpose.PASSWORD_RESET); + send(user.getEmail(), "Passwort für deinen Mindflux-Account zurücksetzen", + "Hallo " + user.getUsername() + ",\n\n" + + "über diesen Link kannst du ein neues Passwort vergeben:\n" + + publicUrl + "/reset-password?token=" + token + "\n\n" + + "Der Link ist 24 Stunden gültig und kann nur einmal verwendet werden.\n\n" + + "Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren."); + }); + } + + @Transactional + public boolean verifyEmail(String rawToken) { + Optional token = validToken(rawToken, AccountEmailToken.Purpose.EMAIL_VERIFICATION); + if (token.isEmpty()) { + return false; + } + AccountEmailToken value = token.get(); + User user = value.getUser(); + user.setEmailVerified(true); + value.setConsumedAt(Instant.now()); + userRepository.save(user); + tokenRepository.save(value); + return true; + } + + @Transactional + public boolean resetPassword(String rawToken, String password, String confirmPassword) { + if (password == null || password.length() < 8 || !password.equals(confirmPassword)) { + return false; + } + Optional token = validToken(rawToken, AccountEmailToken.Purpose.PASSWORD_RESET); + if (token.isEmpty()) { + return false; + } + AccountEmailToken value = token.get(); + User user = value.getUser(); + user.setPassword(passwordEncoder.encode(password)); + value.setConsumedAt(Instant.now()); + userRepository.save(user); + tokenRepository.save(value); + return true; + } + + public boolean isResetTokenValid(String rawToken) { + return validToken(rawToken, AccountEmailToken.Purpose.PASSWORD_RESET).isPresent(); + } + + private Optional uniqueUserForEmail(String email) { + if (email == null || email.isBlank()) { + return Optional.empty(); + } + List users = userRepository.findAllByEmailIgnoreCase(email.trim()); + return users.size() == 1 ? Optional.of(users.getFirst()) : Optional.empty(); + } + + private String issueToken(User user, AccountEmailToken.Purpose purpose) { + tokenRepository.deleteByUserAndPurpose(user, purpose); + byte[] bytes = new byte[32]; + secureRandom.nextBytes(bytes); + String rawToken = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); + AccountEmailToken token = new AccountEmailToken(); + token.setUser(user); + token.setPurpose(purpose); + token.setTokenHash(hash(rawToken)); + token.setExpiresAt(Instant.now().plus(TOKEN_LIFETIME)); + tokenRepository.save(token); + return rawToken; + } + + private Optional validToken(String rawToken, AccountEmailToken.Purpose purpose) { + if (rawToken == null || rawToken.isBlank()) { + return Optional.empty(); + } + return tokenRepository.findByTokenHashAndPurposeAndConsumedAtIsNull(hash(rawToken), purpose) + .filter(token -> token.getExpiresAt().isAfter(Instant.now())); + } + + private void send(String recipient, String subject, String body) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(fromAddress); + message.setTo(recipient); + message.setSubject(subject); + message.setText(body); + mailSender.send(message); + } + + private String hash(String value) { + try { + byte[] digest = MessageDigest.getInstance("SHA-256") + .digest(value.getBytes(StandardCharsets.UTF_8)); + return java.util.HexFormat.of().formatHex(digest); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 ist nicht verfügbar", e); + } + } +} diff --git a/src/main/java/com/example/mindflux/service/RegistrationResult.java b/src/main/java/com/example/mindflux/service/RegistrationResult.java index ad56a3b..6e37f9d 100644 --- a/src/main/java/com/example/mindflux/service/RegistrationResult.java +++ b/src/main/java/com/example/mindflux/service/RegistrationResult.java @@ -6,6 +6,7 @@ public enum RegistrationResult { USERNAME_INVALID, USERNAME_TAKEN, EMAIL_INVALID, + EMAIL_TAKEN, PASSWORD_MISMATCH, PASSWORD_TOO_SHORT } diff --git a/src/main/java/com/example/mindflux/service/UserServiceImpl.java b/src/main/java/com/example/mindflux/service/UserServiceImpl.java index e7133ff..23555bd 100644 --- a/src/main/java/com/example/mindflux/service/UserServiceImpl.java +++ b/src/main/java/com/example/mindflux/service/UserServiceImpl.java @@ -84,7 +84,11 @@ public class UserServiceImpl implements UserService { if (userRepository.findByUsernameIgnoreCase(name).isPresent()) { return RegistrationResult.USERNAME_TAKEN; } + if (!userRepository.findAllByEmailIgnoreCase(email.trim()).isEmpty()) { + return RegistrationResult.EMAIL_TAKEN; + } User user = new User(name, passwordEncoder.encode(password), email.trim(), "USER"); + user.setEmailVerified(false); try { userRepository.save(user); } catch (DataIntegrityViolationException e) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f45cee3..e029885 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -41,6 +41,21 @@ spring.output.ansi.enabled=ALWAYS # Öffentliche Basis-URL, z. B. für künftig erzeugte absolute Links. mindflux.public-url=${MINDFLUX_PUBLIC_URL:http://localhost:8282} +mindflux.mail.from=${MAIL_USERNAME:noreply@sysnaptik.de} + +# ======================= +# E-Mail-Versand +# ======================= +spring.mail.host=${MAIL_HOST:localhost} +spring.mail.port=${MAIL_PORT:587} +spring.mail.username=${MAIL_USERNAME:} +spring.mail.password=${MAIL_PASSWORD:} +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=true +spring.mail.properties.mail.smtp.starttls.required=true +spring.mail.properties.mail.smtp.connectiontimeout=5000 +spring.mail.properties.mail.smtp.timeout=5000 +spring.mail.properties.mail.smtp.writetimeout=5000 # ======================= # Datei-Uploads (Profilbilder) diff --git a/src/main/resources/db/migration/V18__account_email_tokens.sql b/src/main/resources/db/migration/V18__account_email_tokens.sql new file mode 100644 index 0000000..53b42ac --- /dev/null +++ b/src/main/resources/db/migration/V18__account_email_tokens.sql @@ -0,0 +1,17 @@ +ALTER TABLE users + ADD COLUMN email_verified BOOLEAN NOT NULL DEFAULT TRUE; + +CREATE TABLE account_email_tokens ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash CHAR(64) NOT NULL UNIQUE, + purpose VARCHAR(32) NOT NULL, + expires_at TIMESTAMP NOT NULL, + consumed_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT account_email_tokens_purpose_check + CHECK (purpose IN ('EMAIL_VERIFICATION', 'PASSWORD_RESET')) +); + +CREATE INDEX idx_account_email_tokens_lookup + ON account_email_tokens (token_hash, purpose); diff --git a/src/main/resources/templates/admin/users/list.html b/src/main/resources/templates/admin/users/list.html index c6f551f..d9abc3f 100644 --- a/src/main/resources/templates/admin/users/list.html +++ b/src/main/resources/templates/admin/users/list.html @@ -28,6 +28,7 @@ + Das neue Passwort muss mindestens 8 Zeichen lang sein. @@ -48,8 +49,8 @@ - + PW zurücksetzen + + + + + Passwort zurücksetzen · Mindflux + + + + + + + + Account-Zugang + Passwort zurücksetzen + Gib deine E-Mail-Adresse ein. Falls ein bestätigter Account dazugehört, senden wir dir einen Link zum Setzen eines neuen Passworts. + Falls ein passender Account existiert, wurde eine E-Mail versendet. + + + E-Mail-Adresse + + + Reset-Link senden + + Zur Anmeldung + + + + diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index d85281d..240b552 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -29,7 +29,12 @@ Benutzername oder Passwort sind falsch. Du hast dich erfolgreich abgemeldet. - Registrierung erfolgreich! Du kannst dich jetzt einloggen. + Fast geschafft: Wir haben dir einen Bestätigungslink per E-Mail gesendet. + Deine E-Mail-Adresse wurde bestätigt. Du kannst dich jetzt anmelden. + Dein Passwort wurde geändert. Du kannst dich jetzt anmelden. + Falls ein unbestätigter Account zu dieser Adresse existiert, wurde ein neuer Link versendet. + Dieser Bestätigungslink ist ungültig oder abgelaufen. + Dieser Passwort-Link ist ungültig oder abgelaufen. @@ -43,6 +48,8 @@ Login + Passwort vergessen? + Noch keinen Account? Jetzt registrieren @@ -61,6 +68,8 @@ Die Passwörter stimmen nicht überein. Bitte erneut eingeben. Das Passwort muss mindestens 8 Zeichen lang sein. Bitte gib eine gültige E-Mail-Adresse an. + Diese E-Mail-Adresse wird bereits verwendet. + Der Account wurde angelegt, aber die Bestätigungs-E-Mail konnte nicht gesendet werden. Fordere unten einen neuen Link an. @@ -83,6 +92,14 @@ Account erstellen + + Bestätigungslink erneut senden + + + Link senden + + + Schon dabei? Zum Login @@ -138,7 +155,7 @@ // Bei Registrierungs-/Passwortfehler direkt das Signup-Panel öffnen var qp = new URLSearchParams(window.location.search); - if (qp.has('registerError') || qp.has('passwordMismatch')) { + if (qp.has('registerError') || qp.has('passwordMismatch') || qp.has('passwordPolicy') || qp.has('emailInvalid') || qp.has('emailTaken') || qp.has('registerMailError')) { c.classList.add('active'); } diff --git a/src/main/resources/templates/reset-password.html b/src/main/resources/templates/reset-password.html new file mode 100644 index 0000000..1bc1880 --- /dev/null +++ b/src/main/resources/templates/reset-password.html @@ -0,0 +1,33 @@ + + + + + + Neues Passwort · Mindflux + + + + + + + + Account-Zugang + Neues Passwort vergeben + Das Passwort muss mindestens 8 Zeichen lang sein. + Die Passwörter stimmen nicht überein. + + + + Neues Passwort + + + + Passwort wiederholen + + + Passwort speichern + + + + +
Gib deine E-Mail-Adresse ein. Falls ein bestätigter Account dazugehört, senden wir dir einen Link zum Setzen eines neuen Passworts.
Zur Anmeldung
Passwort vergessen?
Noch keinen Account? Jetzt registrieren
Schon dabei? Zum Login