Erweiterungen
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package de.assecutor.votianlt.pages.base.ui.view;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.UI;
|
||||
import com.vaadin.flow.component.applayout.AppLayout;
|
||||
import com.vaadin.flow.component.avatar.Avatar;
|
||||
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.menu.MenuConfiguration;
|
||||
import com.vaadin.flow.server.menu.MenuEntry;
|
||||
import de.assecutor.votianlt.pages.view.EditProfileView;
|
||||
import de.assecutor.votianlt.security.SecurityService;
|
||||
|
||||
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 {
|
||||
|
||||
private final SecurityService securityService;
|
||||
private Div headerRef;
|
||||
private Scroller navRef;
|
||||
private Component userMenuRef;
|
||||
|
||||
public MainLayout(SecurityService securityService) {
|
||||
this.securityService = securityService;
|
||||
setPrimarySection(Section.DRAWER);
|
||||
|
||||
// Only show navigation drawer if user is logged in
|
||||
if (securityService.isUserLoggedIn()) {
|
||||
addToDrawer(createHeader(), new Scroller(createSideNav()), createUserMenu());
|
||||
} else {
|
||||
// Hide drawer completely for anonymous users
|
||||
setDrawerOpened(false);
|
||||
// Always build the drawer; keep references and toggle visibility on attach and after navigation
|
||||
headerRef = createHeader();
|
||||
navRef = new Scroller(createSideNav());
|
||||
userMenuRef = createUserMenu();
|
||||
addToDrawer(headerRef, navRef, userMenuRef);
|
||||
|
||||
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() {
|
||||
@@ -123,23 +139,38 @@ public final class MainLayout extends AppLayout {
|
||||
}
|
||||
|
||||
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();
|
||||
userMenu.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE);
|
||||
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);
|
||||
userMenuItem.add(currentUser);
|
||||
userMenuItem.getSubMenu().addItem("Profil anzeigen");
|
||||
userMenuItem.add(userNameSpan);
|
||||
|
||||
// Profil anzeigen mit Navigation
|
||||
userMenuItem.getSubMenu().addItem("Profil anzeigen", e ->
|
||||
UI.getCurrent().navigate(EditProfileView.class));
|
||||
userMenuItem.getSubMenu().addItem("Einstellungen");
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -117,47 +117,6 @@ public class AddJobService {
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
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.H3;
|
||||
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.H1;
|
||||
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.VerticalLayout;
|
||||
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.Route;
|
||||
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")
|
||||
@PageTitle("Bei VotianLT anmelden")
|
||||
@@ -22,8 +32,18 @@ import com.vaadin.flow.server.auth.AnonymousAllowed;
|
||||
public class LoginView extends VerticalLayout implements BeforeEnterObserver, AfterNavigationObserver {
|
||||
|
||||
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();
|
||||
|
||||
@Autowired
|
||||
private TwoFactorService twoFactorService;
|
||||
|
||||
@Autowired
|
||||
private AuthenticationManager authenticationManager;
|
||||
|
||||
private Authentication pendingAuth;
|
||||
|
||||
public LoginView() {
|
||||
addClassName("login-view");
|
||||
setSizeFull();
|
||||
@@ -31,7 +51,16 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
|
||||
setJustifyContentMode(FlexComponent.JustifyContentMode.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
|
||||
loginForm.setForgotPasswordButtonVisible(true);
|
||||
loginForm.addForgotPasswordListener(e -> UI.getCurrent().navigate(ForgotPasswordRequestView.class));
|
||||
@@ -56,13 +85,59 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
|
||||
VerticalLayout loginLayout = new VerticalLayout();
|
||||
loginLayout.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||
|
||||
loginLayout.add(flashBox, title, loginForm, registerButton);
|
||||
loginLayout.add(flashBox, title, loginForm, twoFaField, verify2faButton, registerButton);
|
||||
loginLayout.setMaxWidth("400px");
|
||||
loginLayout.setPadding(true);
|
||||
|
||||
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
|
||||
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
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.password.PasswordEncoder;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
@@ -74,4 +76,9 @@ public class SecurityConfig extends VaadinWebSecurity {
|
||||
.deleteCookies("JSESSIONID")
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
|
||||
return configuration.getAuthenticationManager();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package de.assecutor.votianlt.security;
|
||||
|
||||
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.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
@@ -22,15 +25,41 @@ public class SecurityService {
|
||||
}
|
||||
|
||||
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() {
|
||||
// 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()
|
||||
.map(UserDetails::getUsername)
|
||||
.orElse("Anonymous");
|
||||
}
|
||||
|
||||
private String nullToEmpty(String s) { return s == null ? "" : s; }
|
||||
|
||||
/**
|
||||
* 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