Erweiterungen
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package de.assecutor.votianlt.pages.base.ui.view;
|
package de.assecutor.votianlt.pages.base.ui.view;
|
||||||
|
|
||||||
import com.vaadin.flow.component.Component;
|
import com.vaadin.flow.component.Component;
|
||||||
|
import com.vaadin.flow.component.UI;
|
||||||
import com.vaadin.flow.component.applayout.AppLayout;
|
import com.vaadin.flow.component.applayout.AppLayout;
|
||||||
import com.vaadin.flow.component.avatar.Avatar;
|
import com.vaadin.flow.component.avatar.Avatar;
|
||||||
import com.vaadin.flow.component.avatar.AvatarVariant;
|
import com.vaadin.flow.component.avatar.AvatarVariant;
|
||||||
@@ -19,6 +20,7 @@ import com.vaadin.flow.router.Layout;
|
|||||||
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
||||||
import com.vaadin.flow.server.menu.MenuConfiguration;
|
import com.vaadin.flow.server.menu.MenuConfiguration;
|
||||||
import com.vaadin.flow.server.menu.MenuEntry;
|
import com.vaadin.flow.server.menu.MenuEntry;
|
||||||
|
import de.assecutor.votianlt.pages.view.EditProfileView;
|
||||||
import de.assecutor.votianlt.security.SecurityService;
|
import de.assecutor.votianlt.security.SecurityService;
|
||||||
|
|
||||||
import static com.vaadin.flow.theme.lumo.LumoUtility.*;
|
import static com.vaadin.flow.theme.lumo.LumoUtility.*;
|
||||||
@@ -28,18 +30,32 @@ import static com.vaadin.flow.theme.lumo.LumoUtility.*;
|
|||||||
public final class MainLayout extends AppLayout {
|
public final class MainLayout extends AppLayout {
|
||||||
|
|
||||||
private final SecurityService securityService;
|
private final SecurityService securityService;
|
||||||
|
private Div headerRef;
|
||||||
|
private Scroller navRef;
|
||||||
|
private Component userMenuRef;
|
||||||
|
|
||||||
public MainLayout(SecurityService securityService) {
|
public MainLayout(SecurityService securityService) {
|
||||||
this.securityService = securityService;
|
this.securityService = securityService;
|
||||||
setPrimarySection(Section.DRAWER);
|
setPrimarySection(Section.DRAWER);
|
||||||
|
|
||||||
// Only show navigation drawer if user is logged in
|
// Always build the drawer; keep references and toggle visibility on attach and after navigation
|
||||||
if (securityService.isUserLoggedIn()) {
|
headerRef = createHeader();
|
||||||
addToDrawer(createHeader(), new Scroller(createSideNav()), createUserMenu());
|
navRef = new Scroller(createSideNav());
|
||||||
} else {
|
userMenuRef = createUserMenu();
|
||||||
// Hide drawer completely for anonymous users
|
addToDrawer(headerRef, navRef, userMenuRef);
|
||||||
setDrawerOpened(false);
|
|
||||||
|
updateDrawerVisibility();
|
||||||
|
|
||||||
|
// Re-check on attach (new UI/session) and on every navigation cycle
|
||||||
|
addAttachListener(e -> updateDrawerVisibility());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateDrawerVisibility() {
|
||||||
|
boolean loggedIn = securityService.isUserLoggedIn();
|
||||||
|
if (headerRef != null) headerRef.setVisible( loggedIn );
|
||||||
|
if (navRef != null) navRef.setVisible( loggedIn );
|
||||||
|
if (userMenuRef != null) userMenuRef.setVisible( loggedIn );
|
||||||
|
setDrawerOpened(loggedIn);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Div createHeader() {
|
private Div createHeader() {
|
||||||
@@ -123,23 +139,38 @@ public final class MainLayout extends AppLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Component createUserMenu() {
|
private Component createUserMenu() {
|
||||||
String currentUser = securityService.getCurrentUsername();
|
|
||||||
|
|
||||||
var avatar = new Avatar(currentUser);
|
|
||||||
avatar.addThemeVariants(AvatarVariant.LUMO_XSMALL);
|
|
||||||
avatar.addClassNames(Margin.Right.SMALL);
|
|
||||||
avatar.setColorIndex(5);
|
|
||||||
|
|
||||||
var userMenu = new MenuBar();
|
var userMenu = new MenuBar();
|
||||||
userMenu.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE);
|
userMenu.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE);
|
||||||
userMenu.addClassNames(Margin.MEDIUM);
|
userMenu.addClassNames(Margin.MEDIUM);
|
||||||
|
|
||||||
|
// Dynamisch aktualisierbare Komponenten
|
||||||
|
var avatar = new Avatar();
|
||||||
|
avatar.addThemeVariants(AvatarVariant.LUMO_XSMALL);
|
||||||
|
avatar.addClassNames(Margin.Right.SMALL);
|
||||||
|
avatar.setColorIndex(5);
|
||||||
|
|
||||||
|
var userNameSpan = new Span();
|
||||||
|
|
||||||
var userMenuItem = userMenu.addItem(avatar);
|
var userMenuItem = userMenu.addItem(avatar);
|
||||||
userMenuItem.add(currentUser);
|
userMenuItem.add(userNameSpan);
|
||||||
userMenuItem.getSubMenu().addItem("Profil anzeigen");
|
|
||||||
|
// Profil anzeigen mit Navigation
|
||||||
|
userMenuItem.getSubMenu().addItem("Profil anzeigen", e ->
|
||||||
|
UI.getCurrent().navigate(EditProfileView.class));
|
||||||
userMenuItem.getSubMenu().addItem("Einstellungen");
|
userMenuItem.getSubMenu().addItem("Einstellungen");
|
||||||
userMenuItem.getSubMenu().addItem("Abmelden", e -> securityService.logout());
|
userMenuItem.getSubMenu().addItem("Abmelden", e -> securityService.logout());
|
||||||
|
|
||||||
|
// Update-Funktion für Benutzername und Avatar
|
||||||
|
Runnable updateUserInfo = () -> {
|
||||||
|
String currentUser = securityService.getCurrentUsername();
|
||||||
|
avatar.setName(currentUser);
|
||||||
|
userNameSpan.setText(currentUser);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial und bei Attach aktualisieren
|
||||||
|
updateUserInfo.run();
|
||||||
|
addAttachListener(e -> updateUserInfo.run());
|
||||||
|
|
||||||
return userMenu;
|
return userMenu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,47 +117,6 @@ public class AddJobService {
|
|||||||
return jobNumber;
|
return jobNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Aktualisiert einen bestehenden Job mit neuen Formulardaten
|
|
||||||
*/
|
|
||||||
private void updateJobFromForm(Job existingJob, Job formJob) {
|
|
||||||
existingJob.setCustomerSelection(formJob.getCustomerSelection());
|
|
||||||
|
|
||||||
// Pickup address
|
|
||||||
existingJob.setPickupCompany(formJob.getPickupCompany());
|
|
||||||
existingJob.setPickupSalutation(formJob.getPickupSalutation());
|
|
||||||
existingJob.setPickupFirstName(formJob.getPickupFirstName());
|
|
||||||
existingJob.setPickupLastName(formJob.getPickupLastName());
|
|
||||||
existingJob.setPickupPhone(formJob.getPickupPhone());
|
|
||||||
existingJob.setPickupStreet(formJob.getPickupStreet());
|
|
||||||
existingJob.setPickupHouseNumber(formJob.getPickupHouseNumber());
|
|
||||||
existingJob.setPickupAddressAddition(formJob.getPickupAddressAddition());
|
|
||||||
existingJob.setPickupZip(formJob.getPickupZip());
|
|
||||||
existingJob.setPickupCity(formJob.getPickupCity());
|
|
||||||
|
|
||||||
// Delivery address
|
|
||||||
existingJob.setDeliveryCompany(formJob.getDeliveryCompany());
|
|
||||||
existingJob.setDeliverySalutation(formJob.getDeliverySalutation());
|
|
||||||
existingJob.setDeliveryFirstName(formJob.getDeliveryFirstName());
|
|
||||||
existingJob.setDeliveryLastName(formJob.getDeliveryLastName());
|
|
||||||
existingJob.setDeliveryPhone(formJob.getDeliveryPhone());
|
|
||||||
existingJob.setDeliveryStreet(formJob.getDeliveryStreet());
|
|
||||||
existingJob.setDeliveryHouseNumber(formJob.getDeliveryHouseNumber());
|
|
||||||
existingJob.setDeliveryAddressAddition(formJob.getDeliveryAddressAddition());
|
|
||||||
existingJob.setDeliveryZip(formJob.getDeliveryZip());
|
|
||||||
existingJob.setDeliveryCity(formJob.getDeliveryCity());
|
|
||||||
|
|
||||||
// Digital processing
|
|
||||||
existingJob.setDigitalProcessing(formJob.isDigitalProcessing());
|
|
||||||
existingJob.setAppUser(formJob.getAppUser());
|
|
||||||
|
|
||||||
// Termine, Bemerkung, Aufgaben, Preis (Cargo separat persistiert)
|
|
||||||
existingJob.setPickupDate(formJob.getPickupDate());
|
|
||||||
existingJob.setDeliveryDate(formJob.getDeliveryDate());
|
|
||||||
existingJob.setRemark(formJob.getRemark());
|
|
||||||
existingJob.setPrice(formJob.getPrice());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Findet den aktuellen Entwurf eines Benutzers
|
* Findet den aktuellen Entwurf eines Benutzers
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import com.vaadin.flow.component.button.ButtonVariant;
|
|||||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||||
import com.vaadin.flow.component.html.Div;
|
import com.vaadin.flow.component.html.Div;
|
||||||
import com.vaadin.flow.component.html.Paragraph;
|
|
||||||
import com.vaadin.flow.component.html.Span;
|
import com.vaadin.flow.component.html.Span;
|
||||||
import com.vaadin.flow.component.html.H3;
|
import com.vaadin.flow.component.html.H3;
|
||||||
import com.vaadin.flow.component.html.IFrame;
|
import com.vaadin.flow.component.html.IFrame;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import com.vaadin.flow.component.button.ButtonVariant;
|
|||||||
import com.vaadin.flow.component.html.Div;
|
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.textfield.TextField;
|
||||||
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
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.AfterNavigationEvent;
|
||||||
@@ -15,6 +17,14 @@ import com.vaadin.flow.router.BeforeEnterObserver;
|
|||||||
import com.vaadin.flow.router.PageTitle;
|
import com.vaadin.flow.router.PageTitle;
|
||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
||||||
|
import de.assecutor.votianlt.security.totp.TwoFactorService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||||
|
import com.vaadin.flow.server.VaadinSession;
|
||||||
|
|
||||||
@Route("login")
|
@Route("login")
|
||||||
@PageTitle("Bei VotianLT anmelden")
|
@PageTitle("Bei VotianLT anmelden")
|
||||||
@@ -22,8 +32,18 @@ import com.vaadin.flow.server.auth.AnonymousAllowed;
|
|||||||
public class LoginView extends VerticalLayout implements BeforeEnterObserver, AfterNavigationObserver {
|
public class LoginView extends VerticalLayout implements BeforeEnterObserver, AfterNavigationObserver {
|
||||||
|
|
||||||
private final LoginForm loginForm = new LoginForm();
|
private final LoginForm loginForm = new LoginForm();
|
||||||
|
private final TextField twoFaField = new TextField("2FA Code");
|
||||||
|
private final Button verify2faButton = new Button("Code prüfen");
|
||||||
private final Div flashBox = new Div();
|
private final Div flashBox = new Div();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TwoFactorService twoFactorService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AuthenticationManager authenticationManager;
|
||||||
|
|
||||||
|
private Authentication pendingAuth;
|
||||||
|
|
||||||
public LoginView() {
|
public LoginView() {
|
||||||
addClassName("login-view");
|
addClassName("login-view");
|
||||||
setSizeFull();
|
setSizeFull();
|
||||||
@@ -31,7 +51,16 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
|
|||||||
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
|
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
|
||||||
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
|
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||||
|
|
||||||
loginForm.setAction("login");
|
// Eigene Authentifizierung: Kein POST auf /login
|
||||||
|
loginForm.setAction(null);
|
||||||
|
|
||||||
|
twoFaField.setVisible(false);
|
||||||
|
twoFaField.setMaxLength(6);
|
||||||
|
twoFaField.setPattern("[0-9]{6}");
|
||||||
|
twoFaField.setHelperText("Bitte 6-stelligen Code aus der E-Mail eingeben");
|
||||||
|
verify2faButton.setVisible(false);
|
||||||
|
verify2faButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
verify2faButton.addClickListener(e -> handleVerify2fa());
|
||||||
// Enable built-in forgot password link and navigate to request view
|
// Enable built-in forgot password link and navigate to request view
|
||||||
loginForm.setForgotPasswordButtonVisible(true);
|
loginForm.setForgotPasswordButtonVisible(true);
|
||||||
loginForm.addForgotPasswordListener(e -> UI.getCurrent().navigate(ForgotPasswordRequestView.class));
|
loginForm.addForgotPasswordListener(e -> UI.getCurrent().navigate(ForgotPasswordRequestView.class));
|
||||||
@@ -56,13 +85,59 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
|
|||||||
VerticalLayout loginLayout = new VerticalLayout();
|
VerticalLayout loginLayout = new VerticalLayout();
|
||||||
loginLayout.setAlignItems(FlexComponent.Alignment.CENTER);
|
loginLayout.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||||
|
|
||||||
loginLayout.add(flashBox, title, loginForm, registerButton);
|
loginLayout.add(flashBox, title, loginForm, twoFaField, verify2faButton, registerButton);
|
||||||
loginLayout.setMaxWidth("400px");
|
loginLayout.setMaxWidth("400px");
|
||||||
loginLayout.setPadding(true);
|
loginLayout.setPadding(true);
|
||||||
|
|
||||||
add(loginLayout);
|
add(loginLayout);
|
||||||
|
// Login-Schritt 1: Benutzername/Passwort
|
||||||
|
loginForm.addLoginListener(e -> handlePasswordLogin(e.getUsername(), e.getPassword()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handlePasswordLogin(String username, String password) {
|
||||||
|
try {
|
||||||
|
// Prüfe Benutzername/Passwort, aber setze Benutzer noch nicht in den SecurityContext
|
||||||
|
Authentication auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
|
||||||
|
this.pendingAuth = auth;
|
||||||
|
twoFaField.setVisible(true);
|
||||||
|
verify2faButton.setVisible(true);
|
||||||
|
twoFactorService.initiateTwoFactorFor(username);
|
||||||
|
Notification.show("2FA-Code per E-Mail gesendet.", 3000, Notification.Position.BOTTOM_CENTER);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
loginForm.setError(true);
|
||||||
|
this.pendingAuth = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleVerify2fa() {
|
||||||
|
if (pendingAuth == null) {
|
||||||
|
Notification.show("Bitte zuerst Benutzername und Passwort eingeben.", 3000, Notification.Position.BOTTOM_CENTER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String username = pendingAuth.getName();
|
||||||
|
String code = twoFaField.getValue();
|
||||||
|
if (code == null || !code.matches("[0-9]{6}")) {
|
||||||
|
Notification.show("Ungültiger Code.", 3000, Notification.Position.BOTTOM_CENTER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean ok = twoFactorService.verifyTwoFactorCode(username, code);
|
||||||
|
if (!ok) {
|
||||||
|
Notification.show("Falscher oder abgelaufener Code.", 3000, Notification.Position.BOTTOM_CENTER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 2FA korrekt: Benutzer nun anmelden
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(pendingAuth);
|
||||||
|
// Persistiere SecurityContext in der HTTP-Session, damit Vaadin/Security ihn in neuen Requests sieht
|
||||||
|
var vaadinSession = VaadinSession.getCurrent();
|
||||||
|
if (vaadinSession != null) {
|
||||||
|
var wrappedSession = vaadinSession.getSession();
|
||||||
|
wrappedSession.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
|
||||||
|
SecurityContextHolder.getContext());
|
||||||
|
}
|
||||||
|
this.pendingAuth = null;
|
||||||
|
// Full reload, damit der neue SecurityContext im UI sicher greift
|
||||||
|
UI.getCurrent().getPage().setLocation("/dashboard");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
|
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
@@ -74,4 +76,9 @@ public class SecurityConfig extends VaadinWebSecurity {
|
|||||||
.deleteCookies("JSESSIONID")
|
.deleteCookies("JSESSIONID")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
|
||||||
|
return configuration.getAuthenticationManager();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package de.assecutor.votianlt.security;
|
package de.assecutor.votianlt.security;
|
||||||
|
|
||||||
import com.vaadin.flow.spring.security.AuthenticationContext;
|
import com.vaadin.flow.spring.security.AuthenticationContext;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||||
import org.bson.types.ObjectId;
|
import org.bson.types.ObjectId;
|
||||||
import org.springframework.security.core.userdetails.User;
|
import org.springframework.security.core.userdetails.User;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
@@ -22,15 +25,41 @@ public class SecurityService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isUserLoggedIn() {
|
public boolean isUserLoggedIn() {
|
||||||
return authenticationContext.isAuthenticated();
|
if (authenticationContext.isAuthenticated()) return true;
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
return auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCurrentUsername() {
|
public String getCurrentUsername() {
|
||||||
|
// 1) Versuche SecurityContext zuerst (robuster bei Custom Login)
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken)) {
|
||||||
|
Object principal = auth.getPrincipal();
|
||||||
|
if (principal instanceof CustomUserPrincipal cup) {
|
||||||
|
de.assecutor.votianlt.model.User u = cup.getUser();
|
||||||
|
if (u != null) {
|
||||||
|
String namePart = (nullToEmpty(u.getFirstname()) + " " + nullToEmpty(u.getName())).trim();
|
||||||
|
if (!namePart.isBlank()) return namePart;
|
||||||
|
if (u.getEmail() != null && !u.getEmail().isBlank()) return u.getEmail();
|
||||||
|
}
|
||||||
|
return cup.getUsername();
|
||||||
|
}
|
||||||
|
if (principal instanceof UserDetails ud) {
|
||||||
|
return ud.getUsername();
|
||||||
|
}
|
||||||
|
if (principal instanceof String s && !"anonymousUser".equalsIgnoreCase(s)) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Fallback: Vaadin AuthenticationContext
|
||||||
return getAuthenticatedUser()
|
return getAuthenticatedUser()
|
||||||
.map(UserDetails::getUsername)
|
.map(UserDetails::getUsername)
|
||||||
.orElse("Anonymous");
|
.orElse("Anonymous");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String nullToEmpty(String s) { return s == null ? "" : s; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the complete MongoDB User entity from the session
|
* Get the complete MongoDB User entity from the session
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package de.assecutor.votianlt.security.totp;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.model.User;
|
||||||
|
import de.assecutor.votianlt.repository.UserRepository;
|
||||||
|
import de.assecutor.votianlt.util.MailUtil;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class TwoFactorService {
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final MailUtil mailUtil;
|
||||||
|
private final SecureRandom random = new SecureRandom();
|
||||||
|
|
||||||
|
public TwoFactorService(UserRepository userRepository, MailUtil mailUtil) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.mailUtil = mailUtil;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initiateTwoFactorFor(String email) {
|
||||||
|
Optional<User> userOpt = userRepository.findByEmail(email);
|
||||||
|
if (userOpt.isEmpty()) return;
|
||||||
|
User user = userOpt.get();
|
||||||
|
String code = generateSixDigitCode();
|
||||||
|
user.setPasswordCode(code);
|
||||||
|
user.setPasswordTimestamp(LocalDateTime.now());
|
||||||
|
userRepository.save(user);
|
||||||
|
try {
|
||||||
|
mailUtil.sendMail(email, "Ihr Anmeldecode (2FA)", "Ihr 2FA-Code lautet: " + code + "\nGültig für 10 Minuten.");
|
||||||
|
} catch (Exception ignored) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verifyTwoFactorCode(String email, String code) {
|
||||||
|
Optional<User> userOpt = userRepository.findByEmail(email);
|
||||||
|
if (userOpt.isEmpty()) return false;
|
||||||
|
User user = userOpt.get();
|
||||||
|
if (user.getPasswordCode() == null || !user.getPasswordCode().equals(code)) return false;
|
||||||
|
if (user.getPasswordTimestamp() == null) return false;
|
||||||
|
// Gültigkeit 10 Minuten
|
||||||
|
if (user.getPasswordTimestamp().isBefore(LocalDateTime.now().minusMinutes(10))) return false;
|
||||||
|
// Code verbrauchen
|
||||||
|
user.setPasswordCode(null);
|
||||||
|
user.setPasswordTimestamp(null);
|
||||||
|
userRepository.save(user);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateSixDigitCode() {
|
||||||
|
int n = random.nextInt(1_000_000); // 0..999999
|
||||||
|
return String.format("%06d", n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user