Erweiterungen

This commit is contained in:
2026-01-26 10:55:59 +01:00
parent 608417331b
commit e8904b9667
8 changed files with 59 additions and 12 deletions

View File

@@ -60,4 +60,7 @@ public class User {
// Digitale Abwicklung und App-Nutzer Ortung
private boolean digitalProcessingEnabled = true; // Digitale Abwicklung per App
private boolean locationTrackingEnabled = true; // App-Nutzer orten
// 2-Faktor-Authentifizierung (standardmäßig aktiviert für neue Nutzer)
private boolean twoFactorEnabled = true;
}

View File

@@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired;
@Route(value = "add-app-user", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({ "USER", "ADMIN" })
public class AddAppUserView extends VerticalLayout {
private final AppUserService appUserService;
private final Binder<AppUser> binder = new Binder<>(AppUser.class);
// Form fields
@@ -38,6 +39,7 @@ public class AddAppUserView extends VerticalLayout {
@Autowired
public AddAppUserView(AppUserService appUserService) {
this.appUserService = appUserService;
setSizeFull();
setPadding(true);
setSpacing(true);
@@ -179,6 +181,9 @@ public class AddAppUserView extends VerticalLayout {
AppUser newAppUser = new AppUser();
binder.writeBean(newAppUser);
// Save to database
appUserService.createAppUser(newAppUser);
// Show success message
Notification.show("App-Nutzer erfolgreich angelegt", 3000, Notification.Position.MIDDLE);

View File

@@ -394,11 +394,19 @@ public class EditProfileView extends HorizontalLayout {
// 2-Faktor Auth
Checkbox twoFactor = new Checkbox("2-Faktor-Authentifizierung");
twoFactor.setValue(currentUser.isTwoFactorEnabled());
twoFactor.addValueChangeListener(e -> currentUser.setTwoFactorEnabled(e.getValue()));
Icon twoFactorInfo = VaadinIcon.QUESTION_CIRCLE_O.create();
twoFactorInfo.getStyle().set("marginLeft", "0.3em");
HorizontalLayout twoFactorLayout = new HorizontalLayout(twoFactor, twoFactorInfo);
twoFactorLayout.setAlignItems(Alignment.CENTER);
securityTab.add(twoFactorLayout);
Span twoFactorDescription = new Span("Bei Aktivierung wird bei jeder Anmeldung ein Code per E-Mail gesendet");
twoFactorDescription.getStyle().set("font-size", "var(--lumo-font-size-s)")
.set("color", "var(--lumo-secondary-text-color)")
.set("margin-left", "var(--lumo-space-xl)");
securityTab.add(twoFactorLayout, twoFactorDescription);
// Passwort ändern Button
Button changePassword = new Button("Passwort ändern");

View File

@@ -19,6 +19,8 @@ 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 de.assecutor.votianlt.repository.UserRepository;
import de.assecutor.votianlt.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -49,8 +51,11 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
@Autowired
private Environment environment;
@Autowired
private UserRepository userRepository;
@Value("${app.security.two-factor.enabled:false}")
private boolean twoFactorEnabled;
private boolean twoFactorEnabledGlobal;
@Value("${app.version:unknown}")
private String appVersion;
@@ -133,7 +138,12 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
Authentication auth = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
if (twoFactorEnabled) {
// Prüfe ob 2FA für diesen Nutzer aktiviert ist (global UND nutzer-spezifisch)
boolean userTwoFactorEnabled = userRepository.findByEmail(username)
.map(User::isTwoFactorEnabled)
.orElse(true); // Standardmäßig aktiviert falls Nutzer nicht gefunden
if (twoFactorEnabledGlobal && userTwoFactorEnabled) {
// 2FA aktiviert: Benutzer noch nicht in SecurityContext setzen
this.pendingAuth = auth;
twoFaField.setVisible(true);

View File

@@ -77,8 +77,8 @@ public class ShowJobsView extends VerticalLayout {
startDate.addValueChangeListener(e -> loadData());
endDate.addValueChangeListener(e -> loadData());
// Configure grid columns: Kunde, Auftragsnummer, Auftragsdatum, Zielort
grid.addColumn(Job::getDeliveryCompany).setHeader("Kunde").setAutoWidth(true).setFlexGrow(1).setSortable(true);
// Configure grid columns: Auftraggeber, Auftragsnummer, Auftragsdatum, Zielort
grid.addColumn(job -> extractCompanyName(job.getCustomerSelection())).setHeader("Auftraggeber").setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(Job::getJobNumber).setHeader("Auftragsnummer").setAutoWidth(true).setSortable(true);
grid.addColumn(Job::getCreatedAt).setHeader("Auftragsdatum").setAutoWidth(true).setSortable(true);
grid.addColumn(Job::getDeliveryCity).setHeader("Zielort").setAutoWidth(true).setFlexGrow(1).setSortable(true);
@@ -160,11 +160,11 @@ public class ShowJobsView extends VerticalLayout {
private String generateCsv(java.util.List<Job> jobs) {
StringBuilder csv = new StringBuilder();
// CSV Header
csv.append("Kunde,Auftragsnummer,Auftragsdatum,Zielort\n");
csv.append("Auftraggeber,Auftragsnummer,Auftragsdatum,Zielort\n");
// CSV Data
for (Job job : jobs) {
csv.append(escapeCsv(job.getDeliveryCompany())).append(",");
csv.append(escapeCsv(extractCompanyName(job.getCustomerSelection()))).append(",");
csv.append(escapeCsv(job.getJobNumber())).append(",");
csv.append(job.getCreatedAt() != null ? job.getCreatedAt().toString() : "").append(",");
csv.append(escapeCsv(job.getDeliveryCity())).append("\n");
@@ -181,4 +181,16 @@ public class ShowJobsView extends VerticalLayout {
}
return value;
}
private String extractCompanyName(String customerSelection) {
if (customerSelection == null || customerSelection.isBlank()) {
return "";
}
// Format: "Firmenname | Vorname Nachname" - extrahiere nur den Firmennamen
int separatorIndex = customerSelection.indexOf(" | ");
if (separatorIndex > 0) {
return customerSelection.substring(0, separatorIndex).trim();
}
return customerSelection.trim();
}
}

View File

@@ -17,6 +17,7 @@ import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import de.assecutor.votianlt.security.SecurityService;
import org.springframework.beans.factory.annotation.Value;
@Route("")
@PageTitle("VotianLT - Willkommen")
@@ -24,9 +25,11 @@ import de.assecutor.votianlt.security.SecurityService;
public class StartView extends VerticalLayout implements BeforeEnterObserver {
private final SecurityService securityService;
private final String appVersion;
public StartView(SecurityService securityService) {
public StartView(SecurityService securityService, @Value("${app.version:unknown}") String appVersion) {
this.securityService = securityService;
this.appVersion = appVersion;
setSizeFull();
setPadding(false);
setSpacing(false);
@@ -324,7 +327,13 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
slogan.getStyle().set("font-style", "italic");
slogan.getStyle().set("color", "var(--lumo-primary-color)");
footer.add(companyTitle, companyInfo, ctaText, slogan);
// Versionsnummer
Span versionSpan = new Span("Version " + appVersion);
versionSpan.getStyle().set("color", "var(--lumo-secondary-text-color)")
.set("font-size", "var(--lumo-font-size-s)")
.set("margin-top", "var(--lumo-space-l)");
footer.add(companyTitle, companyInfo, ctaText, slogan, versionSpan);
return footer;
}

View File

@@ -56,8 +56,8 @@ spring.servlet.multipart.max-request-size=64MB
# Jackson message converter limits
spring.jackson.default-property-inclusion=non_null
# 2FA Configuration
app.security.two-factor.enabled=false
# 2FA Configuration (global toggle - individual users can disable in their profile)
app.security.two-factor.enabled=true
# Message Delivery Layer Configuration
app.messaging.delivery.max-retries=3