Erweiterungen
This commit is contained in:
@@ -40,6 +40,13 @@ public class AppUser {
|
|||||||
@Field("password")
|
@Field("password")
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
|
// Reset-Token und Zeitstempel
|
||||||
|
@Field("password_code")
|
||||||
|
private String passwordCode;
|
||||||
|
|
||||||
|
@Field("password_timestamp")
|
||||||
|
private LocalDateTime passwordTimestamp;
|
||||||
|
|
||||||
@Field("geraet")
|
@Field("geraet")
|
||||||
private String geraet;
|
private String geraet;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package de.assecutor.votianlt.pages.service;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.model.AppUser;
|
||||||
|
import de.assecutor.votianlt.model.User;
|
||||||
|
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||||
|
import de.assecutor.votianlt.repository.UserRepository;
|
||||||
|
import de.assecutor.votianlt.util.MailUtil;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PasswordResetService {
|
||||||
|
|
||||||
|
public enum UserType { USERS, APP_USER }
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final AppUserRepository appUserRepository;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final MailUtil mailUtil;
|
||||||
|
private static final Duration TOKEN_VALIDITY = Duration.ofMinutes(15);
|
||||||
|
|
||||||
|
public PasswordResetService(UserRepository userRepository,
|
||||||
|
AppUserRepository appUserRepository,
|
||||||
|
PasswordEncoder passwordEncoder,
|
||||||
|
MailUtil mailUtil) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.appUserRepository = appUserRepository;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
this.mailUtil = mailUtil;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initiateReset(String email, UserType userType, String baseUrl) {
|
||||||
|
// existing typed initiation
|
||||||
|
initiateResetInternal(email, userType, baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate reset without asking for user type. Looks up the email in both collections
|
||||||
|
* and only proceeds if it exists in exactly one of them. Otherwise, it silently returns
|
||||||
|
* to avoid leaking account existence.
|
||||||
|
*/
|
||||||
|
public void initiateResetAuto(String email, String baseUrl) {
|
||||||
|
if (email == null) return;
|
||||||
|
String normalized = email.trim();
|
||||||
|
if (normalized.isEmpty()) return;
|
||||||
|
var userOpt = userRepository.findByEmail(normalized);
|
||||||
|
var appUser = appUserRepository.findByEmail(normalized);
|
||||||
|
boolean inUsers = userOpt.isPresent();
|
||||||
|
boolean inAppUsers = appUser != null;
|
||||||
|
if (inUsers == inAppUsers) {
|
||||||
|
// Either in both or in none: do nothing to keep behavior indistinguishable
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UserType type = inUsers ? UserType.USERS : UserType.APP_USER;
|
||||||
|
initiateResetInternal(normalized, type, baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initiateResetInternal(String email, UserType userType, String baseUrl) {
|
||||||
|
String token = generateToken64();
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
String typeParam = userType == UserType.USERS ? "users" : "app_user";
|
||||||
|
String link = baseUrl + "/forget-password?token=" + token + "&type=" + typeParam;
|
||||||
|
|
||||||
|
switch (userType) {
|
||||||
|
case USERS -> {
|
||||||
|
Optional<User> optional = userRepository.findByEmail(email);
|
||||||
|
if (optional.isEmpty()) {
|
||||||
|
// Do not leak existence; simply return
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
User user = optional.get();
|
||||||
|
user.setPasswordCode(token);
|
||||||
|
user.setPasswordTimestamp(now);
|
||||||
|
userRepository.save(user);
|
||||||
|
sendMail(email, link);
|
||||||
|
}
|
||||||
|
case APP_USER -> {
|
||||||
|
AppUser appUser = appUserRepository.findByEmail(email);
|
||||||
|
if (appUser == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appUser.setPasswordCode(token);
|
||||||
|
appUser.setPasswordTimestamp(now);
|
||||||
|
appUserRepository.save(appUser);
|
||||||
|
sendMail(email, link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendMail(String to, String link) {
|
||||||
|
String subject = "Passwort zurücksetzen";
|
||||||
|
String body = "Hallo,\n\n" +
|
||||||
|
"Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt. " +
|
||||||
|
"Dieser Link ist 15 Minuten gültig:\n" + link + "\n\n" +
|
||||||
|
"Wenn Sie diese Anfrage nicht gestellt haben, können Sie diese E-Mail ignorieren.";
|
||||||
|
try {
|
||||||
|
mailUtil.sendMail(to, subject, body);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// In this minimal implementation we swallow to avoid leaking details to attackers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTokenValid(String token, UserType userType) {
|
||||||
|
LocalDateTime ts = switch (userType) {
|
||||||
|
case USERS -> userRepository.findByPasswordCode(token).map(User::getPasswordTimestamp).orElse(null);
|
||||||
|
case APP_USER -> Optional.ofNullable(appUserRepository.findByPasswordCode(token)).map(AppUser::getPasswordTimestamp).orElse(null);
|
||||||
|
};
|
||||||
|
if (ts == null) return false;
|
||||||
|
return Duration.between(ts, LocalDateTime.now()).compareTo(TOKEN_VALIDITY) <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean resetPassword(String token, UserType userType, String newPassword) {
|
||||||
|
if (!isTokenValid(token, userType)) return false;
|
||||||
|
switch (userType) {
|
||||||
|
case USERS -> {
|
||||||
|
Optional<User> optional = userRepository.findByPasswordCode(token);
|
||||||
|
if (optional.isEmpty()) return false;
|
||||||
|
User user = optional.get();
|
||||||
|
user.setPassword(passwordEncoder.encode(newPassword));
|
||||||
|
user.setPasswordCode(null);
|
||||||
|
user.setPasswordTimestamp(null);
|
||||||
|
userRepository.save(user);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case APP_USER -> {
|
||||||
|
AppUser appUser = appUserRepository.findByPasswordCode(token);
|
||||||
|
if (appUser == null) return false;
|
||||||
|
appUser.setPassword(passwordEncoder.encode(newPassword));
|
||||||
|
appUser.setPasswordCode(null);
|
||||||
|
appUser.setPasswordTimestamp(null);
|
||||||
|
appUserRepository.save(appUser);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateToken64() {
|
||||||
|
// 32 bytes -> 64 hex characters
|
||||||
|
byte[] bytes = new byte[32];
|
||||||
|
new SecureRandom().nextBytes(bytes);
|
||||||
|
return HexFormat.of().formatHex(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package de.assecutor.votianlt.pages.view;
|
||||||
|
|
||||||
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
|
import com.vaadin.flow.component.html.H1;
|
||||||
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
|
import com.vaadin.flow.component.textfield.PasswordField;
|
||||||
|
import com.vaadin.flow.router.BeforeEnterEvent;
|
||||||
|
import com.vaadin.flow.router.BeforeEnterObserver;
|
||||||
|
import com.vaadin.flow.router.PageTitle;
|
||||||
|
import com.vaadin.flow.router.Route;
|
||||||
|
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
||||||
|
import de.assecutor.votianlt.pages.service.PasswordResetService;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Route("forget-password")
|
||||||
|
@PageTitle("Passwort zurücksetzen")
|
||||||
|
@AnonymousAllowed
|
||||||
|
public class ForgetPasswordView extends VerticalLayout implements BeforeEnterObserver {
|
||||||
|
|
||||||
|
private final PasswordResetService passwordResetService;
|
||||||
|
|
||||||
|
private String token;
|
||||||
|
private PasswordResetService.UserType userType;
|
||||||
|
|
||||||
|
private PasswordField newPassword;
|
||||||
|
private PasswordField confirmPassword;
|
||||||
|
private Button submit;
|
||||||
|
|
||||||
|
public ForgetPasswordView(PasswordResetService passwordResetService) {
|
||||||
|
this.passwordResetService = passwordResetService;
|
||||||
|
|
||||||
|
setSizeFull();
|
||||||
|
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
|
||||||
|
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||||
|
|
||||||
|
VerticalLayout container = new VerticalLayout();
|
||||||
|
container.setWidth("400px");
|
||||||
|
container.setPadding(true);
|
||||||
|
container.setSpacing(true);
|
||||||
|
container.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
|
||||||
|
|
||||||
|
H1 title = new H1("Neues Passwort festlegen");
|
||||||
|
title.getStyle().set("text-align", "center");
|
||||||
|
|
||||||
|
newPassword = new PasswordField("Neues Passwort");
|
||||||
|
confirmPassword = new PasswordField("Passwort bestätigen");
|
||||||
|
|
||||||
|
submit = new Button("Passwort speichern", e -> onSubmit());
|
||||||
|
submit.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
|
||||||
|
container.add(title, newPassword, confirmPassword, submit);
|
||||||
|
add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeEnter(BeforeEnterEvent event) {
|
||||||
|
Map<String, java.util.List<String>> params = event.getLocation().getQueryParameters().getParameters();
|
||||||
|
String tokenParam = params.getOrDefault("token", java.util.List.of("")) .get(0);
|
||||||
|
String typeParam = params.getOrDefault("type", java.util.List.of("")) .get(0);
|
||||||
|
this.token = tokenParam != null ? tokenParam.trim() : "";
|
||||||
|
this.userType = "app_user".equalsIgnoreCase(typeParam) ? PasswordResetService.UserType.APP_USER : PasswordResetService.UserType.USERS;
|
||||||
|
|
||||||
|
if (this.token.isEmpty() || !passwordResetService.isTokenValid(this.token, this.userType)) {
|
||||||
|
// Store a flash message in the VaadinSession so it persists through reroute
|
||||||
|
com.vaadin.flow.server.VaadinSession.getCurrent().setAttribute("flashMessage", "Ungültiger oder abgelaufener Token.");
|
||||||
|
event.rerouteTo(LoginView.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSubmit() {
|
||||||
|
String p1 = newPassword.getValue();
|
||||||
|
String p2 = confirmPassword.getValue();
|
||||||
|
if (p1 == null || p1.isBlank()) {
|
||||||
|
Notification.show("Bitte geben Sie ein neues Passwort ein.", 3000, Notification.Position.MIDDLE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!p1.equals(p2)) {
|
||||||
|
Notification.show("Die Passwörter stimmen nicht überein.", 3000, Notification.Position.MIDDLE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean ok = passwordResetService.resetPassword(token, userType, p1);
|
||||||
|
if (ok) {
|
||||||
|
Notification.show("Passwort wurde erfolgreich geändert.", 3000, Notification.Position.MIDDLE);
|
||||||
|
getUI().ifPresent(ui -> ui.navigate("login"));
|
||||||
|
} else {
|
||||||
|
Notification.show("Token ungültig oder abgelaufen.", 3000, Notification.Position.MIDDLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package de.assecutor.votianlt.pages.view;
|
||||||
|
|
||||||
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
|
import com.vaadin.flow.component.html.H1;
|
||||||
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
|
import com.vaadin.flow.component.textfield.EmailField;
|
||||||
|
import com.vaadin.flow.router.PageTitle;
|
||||||
|
import com.vaadin.flow.router.Route;
|
||||||
|
import com.vaadin.flow.server.VaadinService;
|
||||||
|
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
||||||
|
import de.assecutor.votianlt.pages.service.PasswordResetService;
|
||||||
|
|
||||||
|
@Route("forgot-password-request")
|
||||||
|
@PageTitle("Passwort zurücksetzen – E-Mail angeben")
|
||||||
|
@AnonymousAllowed
|
||||||
|
public class ForgotPasswordRequestView extends VerticalLayout {
|
||||||
|
|
||||||
|
private final PasswordResetService passwordResetService;
|
||||||
|
|
||||||
|
public ForgotPasswordRequestView(PasswordResetService passwordResetService) {
|
||||||
|
this.passwordResetService = passwordResetService;
|
||||||
|
|
||||||
|
setSizeFull();
|
||||||
|
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
|
||||||
|
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||||
|
setPadding(true);
|
||||||
|
|
||||||
|
VerticalLayout container = new VerticalLayout();
|
||||||
|
container.setWidth("400px");
|
||||||
|
container.setPadding(true);
|
||||||
|
container.setSpacing(true);
|
||||||
|
container.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
|
||||||
|
|
||||||
|
H1 title = new H1("Passwort zurücksetzen");
|
||||||
|
title.getStyle().set("text-align", "center");
|
||||||
|
|
||||||
|
EmailField emailField = new EmailField("E-Mail-Adresse");
|
||||||
|
emailField.setWidthFull();
|
||||||
|
emailField.setRequired(true);
|
||||||
|
emailField.setAutofocus(true);
|
||||||
|
|
||||||
|
Button cancel = new Button("Abbrechen", e -> getUI().ifPresent(ui -> ui.navigate("login")));
|
||||||
|
Button submit = new Button("E-Mail senden", e -> {
|
||||||
|
String email = emailField.getValue();
|
||||||
|
if (email == null || email.isBlank()) {
|
||||||
|
Notification.show("Bitte E-Mail eingeben.", 3000, Notification.Position.MIDDLE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String baseUrl = getBaseUrl();
|
||||||
|
passwordResetService.initiateResetAuto(email.trim(), baseUrl);
|
||||||
|
Notification.show("Falls die E-Mail existiert, wurde ein Link versendet.", 4000, Notification.Position.MIDDLE);
|
||||||
|
getUI().ifPresent(ui -> ui.navigate("login"));
|
||||||
|
});
|
||||||
|
submit.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
|
||||||
|
HorizontalLayout actions = new HorizontalLayout(cancel, submit);
|
||||||
|
actions.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
|
||||||
|
|
||||||
|
container.add(title, emailField, actions);
|
||||||
|
add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getBaseUrl() {
|
||||||
|
var vReq = VaadinService.getCurrentRequest();
|
||||||
|
if (vReq instanceof com.vaadin.flow.server.VaadinServletRequest servletRequest) {
|
||||||
|
var req = servletRequest.getHttpServletRequest();
|
||||||
|
String scheme = req.getScheme();
|
||||||
|
String serverName = req.getServerName();
|
||||||
|
int serverPort = req.getServerPort();
|
||||||
|
String contextPath = req.getContextPath();
|
||||||
|
String portPart = ("http".equals(scheme) && serverPort == 80) || ("https".equals(scheme) && serverPort == 443)
|
||||||
|
? "" : ":" + serverPort;
|
||||||
|
return scheme + "://" + serverName + portPart + contextPath;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,13 @@ package de.assecutor.votianlt.pages.view;
|
|||||||
import com.vaadin.flow.component.UI;
|
import com.vaadin.flow.component.UI;
|
||||||
import com.vaadin.flow.component.button.Button;
|
import com.vaadin.flow.component.button.Button;
|
||||||
import com.vaadin.flow.component.button.ButtonVariant;
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
|
import com.vaadin.flow.component.html.Div;
|
||||||
import com.vaadin.flow.component.html.H1;
|
import com.vaadin.flow.component.html.H1;
|
||||||
import com.vaadin.flow.component.login.LoginForm;
|
import com.vaadin.flow.component.login.LoginForm;
|
||||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
|
import com.vaadin.flow.router.AfterNavigationEvent;
|
||||||
|
import com.vaadin.flow.router.AfterNavigationObserver;
|
||||||
import com.vaadin.flow.router.BeforeEnterEvent;
|
import com.vaadin.flow.router.BeforeEnterEvent;
|
||||||
import com.vaadin.flow.router.BeforeEnterObserver;
|
import com.vaadin.flow.router.BeforeEnterObserver;
|
||||||
import com.vaadin.flow.router.PageTitle;
|
import com.vaadin.flow.router.PageTitle;
|
||||||
@@ -16,9 +19,10 @@ import com.vaadin.flow.server.auth.AnonymousAllowed;
|
|||||||
@Route("login")
|
@Route("login")
|
||||||
@PageTitle("Bei VotianLT anmelden")
|
@PageTitle("Bei VotianLT anmelden")
|
||||||
@AnonymousAllowed
|
@AnonymousAllowed
|
||||||
public class LoginView extends VerticalLayout implements BeforeEnterObserver {
|
public class LoginView extends VerticalLayout implements BeforeEnterObserver, AfterNavigationObserver {
|
||||||
|
|
||||||
private final LoginForm loginForm = new LoginForm();
|
private final LoginForm loginForm = new LoginForm();
|
||||||
|
private final Div flashBox = new Div();
|
||||||
|
|
||||||
public LoginView() {
|
public LoginView() {
|
||||||
addClassName("login-view");
|
addClassName("login-view");
|
||||||
@@ -28,6 +32,9 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver {
|
|||||||
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
|
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||||
|
|
||||||
loginForm.setAction("login");
|
loginForm.setAction("login");
|
||||||
|
// Enable built-in forgot password link and navigate to request view
|
||||||
|
loginForm.setForgotPasswordButtonVisible(true);
|
||||||
|
loginForm.addForgotPasswordListener(e -> UI.getCurrent().navigate(ForgotPasswordRequestView.class));
|
||||||
|
|
||||||
H1 title = new H1("VotianLT");
|
H1 title = new H1("VotianLT");
|
||||||
title.getStyle().set("color", "var(--lumo-primary-color)");
|
title.getStyle().set("color", "var(--lumo-primary-color)");
|
||||||
@@ -36,15 +43,27 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver {
|
|||||||
e -> UI.getCurrent().navigate("register"));
|
e -> UI.getCurrent().navigate("register"));
|
||||||
registerButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
registerButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
|
|
||||||
|
// Inline flash message box (hidden by default)
|
||||||
|
flashBox.getStyle()
|
||||||
|
.set("background", "var(--lumo-error-color-10pct)")
|
||||||
|
.set("color", "var(--lumo-error-text-color)")
|
||||||
|
.set("border", "1px solid var(--lumo-error-color)")
|
||||||
|
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||||
|
.set("padding", "var(--lumo-space-m)")
|
||||||
|
.set("width", "100%")
|
||||||
|
.set("display", "none");
|
||||||
|
|
||||||
VerticalLayout loginLayout = new VerticalLayout();
|
VerticalLayout loginLayout = new VerticalLayout();
|
||||||
loginLayout.setAlignItems(FlexComponent.Alignment.CENTER);
|
loginLayout.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||||
loginLayout.add(title, loginForm, registerButton);
|
|
||||||
|
loginLayout.add(flashBox, title, loginForm, registerButton);
|
||||||
loginLayout.setMaxWidth("400px");
|
loginLayout.setMaxWidth("400px");
|
||||||
loginLayout.setPadding(true);
|
loginLayout.setPadding(true);
|
||||||
|
|
||||||
add(loginLayout);
|
add(loginLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
|
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
|
||||||
// Zeige Fehlermeldung bei fehlgeschlagener Anmeldung
|
// Zeige Fehlermeldung bei fehlgeschlagener Anmeldung
|
||||||
@@ -55,4 +74,22 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver {
|
|||||||
loginForm.setError(true);
|
loginForm.setError(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterNavigation(AfterNavigationEvent event) {
|
||||||
|
// Show flash message after the view is attached and rendered
|
||||||
|
var session = com.vaadin.flow.server.VaadinSession.getCurrent();
|
||||||
|
if (session != null) {
|
||||||
|
Object msg = session.getAttribute("flashMessage");
|
||||||
|
if (msg instanceof String && !((String) msg).isBlank()) {
|
||||||
|
session.setAttribute("flashMessage", null); // clear
|
||||||
|
// Inline message for guaranteed visibility
|
||||||
|
flashBox.setText((String) msg);
|
||||||
|
flashBox.getStyle().set("display", "block");
|
||||||
|
} else {
|
||||||
|
flashBox.setText("");
|
||||||
|
flashBox.getStyle().set("display", "none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ public interface AppUserRepository extends MongoRepository<AppUser, ObjectId> {
|
|||||||
// Find AppUser by email for login
|
// Find AppUser by email for login
|
||||||
AppUser findByEmail(String email);
|
AppUser findByEmail(String email);
|
||||||
|
|
||||||
|
AppUser findByPasswordCode(String passwordCode);
|
||||||
|
|
||||||
// Custom query methods can be added here if needed
|
// Custom query methods can be added here if needed
|
||||||
// List<AppUser> findByBezeichnung(String bezeichnung);
|
// List<AppUser> findByBezeichnung(String bezeichnung);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,6 @@ public interface UserRepository extends MongoRepository<User, String> {
|
|||||||
boolean existsByEmail(String email);
|
boolean existsByEmail(String email);
|
||||||
|
|
||||||
void deleteByEmail(String email);
|
void deleteByEmail(String email);
|
||||||
|
|
||||||
|
Optional<User> findByPasswordCode(String passwordCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ public class SecurityConfig extends VaadinWebSecurity {
|
|||||||
new AntPathRequestMatcher("/"),
|
new AntPathRequestMatcher("/"),
|
||||||
new AntPathRequestMatcher("/register"),
|
new AntPathRequestMatcher("/register"),
|
||||||
new AntPathRequestMatcher("/login"),
|
new AntPathRequestMatcher("/login"),
|
||||||
|
new AntPathRequestMatcher("/forget-password"),
|
||||||
|
new AntPathRequestMatcher("/forgot-password-request"),
|
||||||
new AntPathRequestMatcher("/images/**"),
|
new AntPathRequestMatcher("/images/**"),
|
||||||
new AntPathRequestMatcher("/icons/**"),
|
new AntPathRequestMatcher("/icons/**"),
|
||||||
new AntPathRequestMatcher("/favicon.ico"),
|
new AntPathRequestMatcher("/favicon.ico"),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public class MailUtil {
|
|||||||
Properties props = new Properties();
|
Properties props = new Properties();
|
||||||
props.put("mail.smtp.auth", "true");
|
props.put("mail.smtp.auth", "true");
|
||||||
props.put("mail.smtp.starttls.enable", "true");
|
props.put("mail.smtp.starttls.enable", "true");
|
||||||
|
props.put("mail.smtp.starttls.required", "true");
|
||||||
props.put("mail.smtp.host", host);
|
props.put("mail.smtp.host", host);
|
||||||
props.put("mail.smtp.port", String.valueOf(port));
|
props.put("mail.smtp.port", String.valueOf(port));
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ spring.jpa.open-in-view=false
|
|||||||
spring.data.mongodb.uri=mongodb://192.168.180.25:27017/votianlt
|
spring.data.mongodb.uri=mongodb://192.168.180.25:27017/votianlt
|
||||||
|
|
||||||
# Mail Configuration
|
# Mail Configuration
|
||||||
mail.smtp.username=your-email@gmail.com
|
mail.smtp.username=no-reply@appcreation.de
|
||||||
mail.smtp.password=your-password
|
mail.smtp.password=SV1705CA!noreply
|
||||||
mail.smtp.host=smtp.gmail.com
|
mail.smtp.host=smtp.ionos.de
|
||||||
mail.smtp.port=587
|
mail.smtp.port=587
|
||||||
|
|
||||||
# WebSocket and STOMP Configuration
|
# WebSocket and STOMP Configuration
|
||||||
|
|||||||
Reference in New Issue
Block a user