diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1fab7de --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.gitignore +.DS_Store +.vscode + +backend/target +frontend/dist +frontend/node_modules diff --git a/.gitignore b/.gitignore index 10d353b..5ae036d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ backend/target/ frontend/node_modules/ frontend/dist/ .DS_Store +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d897adc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM node:20-alpine AS frontend-build +WORKDIR /build/frontend + +ARG VITE_API_URL=/api +ENV VITE_API_URL=${VITE_API_URL} + +COPY frontend/package*.json ./ +RUN npm ci + +COPY frontend/ ./ +RUN npm run build + + +FROM maven:3.9.11-eclipse-temurin-17 AS backend-build +WORKDIR /build/backend + +COPY backend/pom.xml ./ +RUN mvn -B -q -DskipTests dependency:go-offline + +COPY backend/ ./ +COPY --from=frontend-build /build/frontend/dist ./src/main/resources/static +RUN mvn -B -q -DskipTests package + + +FROM eclipse-temurin:17-jre-alpine AS runtime +WORKDIR /app + +RUN addgroup -S spring && adduser -S spring -G spring + +COPY --from=backend-build /build/backend/target/muh-backend-0.0.1-SNAPSHOT.jar /app/app.jar + +USER spring:spring + +EXPOSE 8090 + +ENTRYPOINT ["java", "-jar", "/app/app.jar"] diff --git a/README.md b/README.md index 71346ad..0a59b27 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,26 @@ Therapieempfehlungen sowie Verwaltungs- und Portalaufgaben. ## Konfiguration -MongoDB ist bereits im Backend vorkonfiguriert: +Die Anwendung liest Konfigurationswerte aus einer `.env` im Projektverzeichnis +oder aus Umgebungsvariablen. -- `mongodb://192.168.180.25:27017/muh` +Die lokale `.env` ist in `.gitignore` eingetragen und sollte nicht mit echten Zugangsdaten committed werden. + +MongoDB: + +- `MUH_MONGODB_URL` +- `MUH_MONGODB_USERNAME` +- `MUH_MONGODB_PASSWORD` Optional fuer echten Mailversand im Portal: - `MUH_MAIL_ENABLED=true` +- `MUH_MAIL_FROM=...` - `MUH_MAIL_HOST=...` - `MUH_MAIL_PORT=587` - `MUH_MAIL_USERNAME=...` - `MUH_MAIL_PASSWORD=...` -- `MUH_MAIL_FROM=...` +- `MUH_MAIL_PROTOCOL=smtp` - `MUH_MAIL_AUTH=true` - `MUH_MAIL_STARTTLS=true` @@ -52,13 +60,34 @@ Frontend-URL: Optional kann die API-URL im Frontend ueber `VITE_API_URL` gesetzt werden. +## Docker Deployment + +Produktions-Image bauen: + +```bash +docker build -t muh-app . +``` + +Container starten: + +```bash +docker run --rm --env-file .env -p 8090:8090 muh-app +``` + +Die Anwendung ist danach unter `http://localhost:8090` erreichbar. + +Hinweis: + +- Das Dockerfile baut das React-Frontend und das Spring-Boot-Backend in einem Image. +- Das Frontend wird im Container direkt ueber Spring Boot ausgeliefert. +- API-Aufrufe laufen in Produktion relativ ueber `/api`. + ## Anmeldung -Es gibt jetzt drei Varianten: +Es gibt jetzt zwei Varianten: -- Schnelllogin ueber Benutzerkuerzel - Login ueber E-Mail oder Benutzername plus Passwort -- Registrierung eines neuen Kundenkontos ueber Firmenname, Adresse, E-Mail und Passwort +- Registrierung eines neuen Kundenkontos ueber Firmenname, Strasse, Hausnummer, PLZ, Ort, E-Mail und Passwort Vordefinierter Admin: diff --git a/backend/src/main/java/de/svencarstensen/muh/config/MongoConfig.java b/backend/src/main/java/de/svencarstensen/muh/config/MongoConfig.java new file mode 100644 index 0000000..b9b9cc0 --- /dev/null +++ b/backend/src/main/java/de/svencarstensen/muh/config/MongoConfig.java @@ -0,0 +1,37 @@ +package de.svencarstensen.muh.config; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoCredential; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +@Configuration +public class MongoConfig { + + @Bean + MongoClient mongoClient( + @Value("${muh.mongodb.url}") String mongoUrl, + @Value("${muh.mongodb.username:}") String username, + @Value("${muh.mongodb.password:}") String password + ) { + ConnectionString connectionString = new ConnectionString(mongoUrl); + MongoClientSettings.Builder builder = MongoClientSettings.builder() + .applyConnectionString(connectionString); + + if (StringUtils.hasText(username)) { + String authDatabase = connectionString.getDatabase() == null ? "admin" : connectionString.getDatabase(); + builder.credential(MongoCredential.createCredential( + username.trim(), + authDatabase, + password == null ? new char[0] : password.toCharArray() + )); + } + + return MongoClients.create(builder.build()); + } +} diff --git a/backend/src/main/java/de/svencarstensen/muh/domain/AppUser.java b/backend/src/main/java/de/svencarstensen/muh/domain/AppUser.java index 0508920..09c4607 100644 --- a/backend/src/main/java/de/svencarstensen/muh/domain/AppUser.java +++ b/backend/src/main/java/de/svencarstensen/muh/domain/AppUser.java @@ -12,7 +12,12 @@ public record AppUser( String displayName, String companyName, String address, + String street, + String houseNumber, + String postalCode, + String city, String email, + String phoneNumber, String portalLogin, String passwordHash, boolean active, diff --git a/backend/src/main/java/de/svencarstensen/muh/service/CatalogService.java b/backend/src/main/java/de/svencarstensen/muh/service/CatalogService.java index c7f5182..ef70667 100644 --- a/backend/src/main/java/de/svencarstensen/muh/service/CatalogService.java +++ b/backend/src/main/java/de/svencarstensen/muh/service/CatalogService.java @@ -20,6 +20,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; import java.time.LocalDateTime; +import java.util.Arrays; import java.util.Comparator; import java.util.HashSet; import java.util.List; @@ -383,7 +384,8 @@ public class CatalogService { .toList(); } - public UserRow createOrUpdateUser(UserMutation mutation) { + public UserRow createOrUpdateUser(String actorId, UserMutation mutation) { + requireAdminActor(actorId); if (isBlank(mutation.displayName()) || isBlank(mutation.code())) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Benutzername und Kürzel sind erforderlich"); } @@ -396,11 +398,16 @@ public class CatalogService { mutation.displayName().trim(), blankToNull(mutation.companyName()), blankToNull(mutation.address()), + null, + null, + null, + null, normalizeEmail(mutation.email()), + null, blankToNull(mutation.portalLogin()), encodeIfPresent(mutation.password()), mutation.active(), - Optional.ofNullable(mutation.role()).orElse(UserRole.APP), + normalizeManagedRole(mutation.role()), now, now )); @@ -415,11 +422,16 @@ public class CatalogService { mutation.displayName().trim(), blankToNull(mutation.companyName()), blankToNull(mutation.address()), + existing.street(), + existing.houseNumber(), + existing.postalCode(), + existing.city(), normalizeEmail(mutation.email()), + existing.phoneNumber(), blankToNull(mutation.portalLogin()), isBlank(mutation.password()) ? existing.passwordHash() : passwordEncoder.encode(mutation.password()), mutation.active(), - Optional.ofNullable(mutation.role()).orElse(existing.role()), + mutation.role() == null ? normalizeStoredRole(existing.role()) : normalizeManagedRole(mutation.role()), existing.createdAt(), now )); @@ -442,7 +454,12 @@ public class CatalogService { existing.displayName(), existing.companyName(), existing.address(), + existing.street(), + existing.houseNumber(), + existing.postalCode(), + existing.city(), existing.email(), + existing.phoneNumber(), existing.portalLogin(), passwordEncoder.encode(newPassword), existing.active(), @@ -473,8 +490,18 @@ public class CatalogService { } public UserOption registerCustomer(RegistrationMutation mutation) { - if (isBlank(mutation.companyName()) || isBlank(mutation.address()) || isBlank(mutation.email()) || isBlank(mutation.password())) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Firmenname, Adresse, E-Mail und Passwort sind erforderlich"); + if (isBlank(mutation.companyName()) + || isBlank(mutation.street()) + || isBlank(mutation.houseNumber()) + || isBlank(mutation.postalCode()) + || isBlank(mutation.city()) + || isBlank(mutation.email()) + || isBlank(mutation.phoneNumber()) + || isBlank(mutation.password())) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Firmenname, Strasse, Hausnummer, PLZ, Ort, E-Mail, Telefonnummer und Passwort sind erforderlich" + ); } String normalizedEmail = normalizeEmail(mutation.email()); @@ -483,7 +510,12 @@ public class CatalogService { } String companyName = mutation.companyName().trim(); - String address = mutation.address().trim(); + String street = mutation.street().trim(); + String houseNumber = mutation.houseNumber().trim(); + String postalCode = mutation.postalCode().trim(); + String city = mutation.city().trim(); + String phoneNumber = mutation.phoneNumber().trim(); + String address = formatAddress(street, houseNumber, postalCode, city); String displayName = companyName; String portalLogin = generateUniquePortalLogin(localPart(normalizedEmail)); String code = generateUniqueCode("K" + companyName); @@ -495,7 +527,12 @@ public class CatalogService { displayName, companyName, address, + street, + houseNumber, + postalCode, + city, normalizedEmail, + phoneNumber, portalLogin, passwordEncoder.encode(mutation.password()), true, @@ -518,10 +555,11 @@ public class CatalogService { } public void ensureDefaultUsers() { + migrateLegacyAppUsers(); ensureDefaultUser("ADM", "Administrator", "admin@muh.local", "admin", "Admin123!", UserRole.ADMIN); - ensureDefaultUser("SV", "Sven", "sven@muh.local", "sven", "muh123", UserRole.APP); - ensureDefaultUser("AK", "Anna", "anna@muh.local", "anna", "muh123", UserRole.APP); - ensureDefaultUser("LH", "Lena", "lena@muh.local", "lena", "muh123", UserRole.APP); + ensureDefaultUser("SV", "Sven", "sven@muh.local", "sven", "muh123", UserRole.CUSTOMER); + ensureDefaultUser("AK", "Anna", "anna@muh.local", "anna", "muh123", UserRole.CUSTOMER); + ensureDefaultUser("LH", "Lena", "lena@muh.local", "lena", "muh123", UserRole.CUSTOMER); } public Farmer requireActiveFarmer(String businessKey) { @@ -597,11 +635,12 @@ public class CatalogService { user.code(), user.displayName(), user.companyName(), - user.address(), + resolveAddress(user), user.email(), + user.phoneNumber(), user.portalLogin(), user.active(), - user.role(), + normalizeStoredRole(user.role()), user.updatedAt() ); } @@ -628,10 +667,11 @@ public class CatalogService { user.code(), user.displayName(), user.companyName(), - user.address(), + resolveAddress(user), user.email(), + user.phoneNumber(), user.portalLogin(), - user.role() + normalizeStoredRole(user.role()) ); } @@ -690,6 +730,46 @@ public class CatalogService { .or(() -> appUserRepository.findByPortalLoginIgnoreCase(identifier)); } + private void requireAdminActor(String actorId) { + if (isBlank(actorId)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Nur Administratoren koennen Benutzer anlegen"); + } + + String actorIdValue = requireText(actorId, "Nur Administratoren koennen Benutzer anlegen"); + AppUser actor = appUserRepository.findById(actorIdValue) + .filter(AppUser::active) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN, "Nur Administratoren koennen Benutzer anlegen")); + + if (actor.role() != UserRole.ADMIN) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Nur Administratoren koennen Benutzer anlegen"); + } + } + + private void migrateLegacyAppUsers() { + LocalDateTime now = LocalDateTime.now(); + appUserRepository.findAll().stream() + .filter(user -> user.role() == UserRole.APP) + .forEach(user -> appUserRepository.save(new AppUser( + user.id(), + user.code(), + user.displayName(), + user.companyName(), + user.address(), + user.street(), + user.houseNumber(), + user.postalCode(), + user.city(), + user.email(), + user.phoneNumber(), + user.portalLogin(), + user.passwordHash(), + user.active(), + UserRole.CUSTOMER, + user.createdAt(), + now + ))); + } + private void ensureDefaultUser( String code, String displayName, @@ -712,7 +792,12 @@ public class CatalogService { displayName, null, null, + null, + null, + null, + null, email, + null, portalLogin, passwordEncoder.encode(rawPassword), true, @@ -726,6 +811,37 @@ public class CatalogService { return isBlank(email) ? null : email.trim().toLowerCase(Locale.ROOT); } + private UserRole normalizeStoredRole(UserRole role) { + return role == null || role == UserRole.APP ? UserRole.CUSTOMER : role; + } + + private UserRole normalizeManagedRole(UserRole role) { + return role == null || role == UserRole.APP ? UserRole.CUSTOMER : role; + } + + private String resolveAddress(AppUser user) { + if (!isBlank(user.address())) { + return user.address(); + } + if (isBlank(user.street()) && isBlank(user.houseNumber()) && isBlank(user.postalCode()) && isBlank(user.city())) { + return null; + } + return formatAddress(user.street(), user.houseNumber(), user.postalCode(), user.city()); + } + + private String formatAddress(String street, String houseNumber, String postalCode, String city) { + String streetPart = joinParts(" ", street, houseNumber); + String cityPart = joinParts(" ", postalCode, city); + return joinParts(", ", streetPart, cityPart); + } + + private String joinParts(String separator, String... parts) { + return Arrays.stream(parts) + .filter(part -> !isBlank(part)) + .map(String::trim) + .collect(Collectors.joining(separator)); + } + private String localPart(String email) { int separator = email.indexOf('@'); return separator >= 0 ? email.substring(0, separator) : email; @@ -810,6 +926,7 @@ public class CatalogService { String companyName, String address, String email, + String phoneNumber, String portalLogin, UserRole role ) { @@ -875,6 +992,7 @@ public class CatalogService { String companyName, String address, String email, + String phoneNumber, String portalLogin, boolean active, UserRole role, @@ -898,8 +1016,12 @@ public class CatalogService { public record RegistrationMutation( String companyName, - String address, + String street, + String houseNumber, + String postalCode, + String city, String email, + String phoneNumber, String password ) { } diff --git a/backend/src/main/java/de/svencarstensen/muh/service/ReportService.java b/backend/src/main/java/de/svencarstensen/muh/service/ReportService.java index df515e6..f553711 100644 --- a/backend/src/main/java/de/svencarstensen/muh/service/ReportService.java +++ b/backend/src/main/java/de/svencarstensen/muh/service/ReportService.java @@ -1,42 +1,56 @@ package de.svencarstensen.muh.service; import de.svencarstensen.muh.domain.Sample; +import de.svencarstensen.muh.domain.AppUser; +import de.svencarstensen.muh.repository.AppUserRepository; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; import org.springframework.lang.NonNull; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; +import org.springframework.util.FileCopyUtils; import org.springframework.web.server.ResponseStatusException; import jakarta.mail.internet.MimeMessage; +import java.io.IOException; +import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; @Service public class ReportService { private final SampleService sampleService; + private final AppUserRepository appUserRepository; private final ObjectProvider mailSenderProvider; private final boolean mailEnabled; private final String mailFrom; + private final String reportMailTemplate; public ReportService( SampleService sampleService, + AppUserRepository appUserRepository, ObjectProvider mailSenderProvider, + @Value("classpath:mail/report-mail-template.txt") Resource reportMailTemplateResource, @Value("${muh.mail.enabled:false}") boolean mailEnabled, @Value("${muh.mail.from:no-reply@muh.local}") String mailFrom ) { this.sampleService = sampleService; + this.appUserRepository = appUserRepository; this.mailSenderProvider = mailSenderProvider; this.mailEnabled = mailEnabled; this.mailFrom = mailFrom; + this.reportMailTemplate = loadTemplate(reportMailTemplateResource); } public List reportCandidates() { @@ -46,7 +60,8 @@ public class ReportService { .toList(); } - public DispatchResult sendReports(List sampleIds) { + public DispatchResult sendReports(String actorId, List sampleIds) { + String customerSignature = buildCustomerSignature(actorId); List sent = new ArrayList<>(); List skipped = new ArrayList<>(); @@ -59,7 +74,7 @@ public class ReportService { byte[] pdf = buildPdf(sample); if (mailEnabled && mailSenderProvider.getIfAvailable() != null) { - sendMail(sample, pdf); + sendMail(sample, pdf, customerSignature); } Sample updated = sampleService.markReportSent(sample.id(), LocalDateTime.now()); sent.add(toCandidate(updated)); @@ -75,7 +90,7 @@ public class ReportService { return sampleService.getSample(sampleService.toggleReportBlocked(sampleId, blocked).id()); } - private void sendMail(Sample sample, byte[] pdf) { + private void sendMail(Sample sample, byte[] pdf, String customerSignature) { try { JavaMailSender sender = mailSenderProvider.getIfAvailable(); if (sender == null) { @@ -86,7 +101,7 @@ public class ReportService { helper.setFrom(requireText(mailFrom, "Absender fehlt")); helper.setTo(requireText(sample.farmerEmail(), "Empfänger fehlt")); helper.setSubject("MUH-Bericht Probe " + sample.sampleNumber()); - helper.setText("Im Anhang befindet sich der Bericht zur Probe " + sample.sampleNumber() + ".", false); + helper.setText(buildMailBody(sample, customerSignature), false); helper.addAttachment("MUH-Bericht-" + sample.sampleNumber() + ".pdf", new ByteArrayResource(Objects.requireNonNull(pdf))); sender.send(message); } catch (Exception exception) { @@ -136,6 +151,78 @@ public class ReportService { return value == null || value.isBlank() ? "-" : value; } + private String buildMailBody(Sample sample, String customerSignature) { + return reportMailTemplate + .replace("{{sampleNumber}}", String.valueOf(sample.sampleNumber())) + .replace("{{cowLabel}}", resolveCowLabel(sample)) + .replace("{{customerSignature}}", customerSignature); + } + + private String resolveCowLabel(Sample sample) { + if (sample.cowName() == null || sample.cowName().isBlank()) { + return sample.cowNumber(); + } + return sample.cowNumber() + " / " + sample.cowName(); + } + + private String buildCustomerSignature(String actorId) { + if (actorId == null || actorId.isBlank()) { + return defaultCustomerSignature(); + } + + AppUser actor = appUserRepository.findById(actorId.trim()).orElse(null); + if (actor == null) { + return defaultCustomerSignature(); + } + + String primaryName = firstNonBlank(actor.companyName(), actor.displayName(), actor.code(), "MUH App"); + String secondaryName = actor.companyName() != null + && actor.displayName() != null + && !actor.companyName().equalsIgnoreCase(actor.displayName()) + ? actor.displayName() + : null; + String streetLine = joinParts(" ", actor.street(), actor.houseNumber()); + String cityLine = joinParts(" ", actor.postalCode(), actor.city()); + + return Stream.of( + primaryName, + secondaryName, + streetLine, + cityLine, + actor.email(), + actor.phoneNumber() + ) + .filter(part -> part != null && !part.isBlank()) + .collect(Collectors.joining(System.lineSeparator())); + } + + private String defaultCustomerSignature() { + return firstNonBlank(mailFrom, "MUH App"); + } + + private String joinParts(String separator, String... values) { + return Stream.of(values) + .filter(value -> value != null && !value.isBlank()) + .map(String::trim) + .collect(Collectors.joining(separator)); + } + + private String firstNonBlank(String... values) { + return Stream.of(values) + .filter(value -> value != null && !value.isBlank()) + .map(String::trim) + .findFirst() + .orElse(""); + } + + private String loadTemplate(Resource resource) { + try (InputStreamReader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) { + return FileCopyUtils.copyToString(reader); + } catch (IOException exception) { + throw new IllegalStateException("Mail-Template konnte nicht geladen werden", exception); + } + } + private @NonNull String requireText(String value, String message) { if (value == null || value.isBlank()) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message); diff --git a/backend/src/main/java/de/svencarstensen/muh/web/CatalogController.java b/backend/src/main/java/de/svencarstensen/muh/web/CatalogController.java index 164b8d6..df38fdb 100644 --- a/backend/src/main/java/de/svencarstensen/muh/web/CatalogController.java +++ b/backend/src/main/java/de/svencarstensen/muh/web/CatalogController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -57,8 +58,11 @@ public class CatalogController { } @PostMapping("/portal/users") - public CatalogService.UserRow saveUser(@RequestBody CatalogService.UserMutation mutation) { - return catalogService.createOrUpdateUser(mutation); + public CatalogService.UserRow saveUser( + @RequestHeader(value = "X-MUH-Actor-Id", required = false) String actorId, + @RequestBody CatalogService.UserMutation mutation + ) { + return catalogService.createOrUpdateUser(actorId, mutation); } @DeleteMapping("/portal/users/{id}") diff --git a/backend/src/main/java/de/svencarstensen/muh/web/PortalController.java b/backend/src/main/java/de/svencarstensen/muh/web/PortalController.java index b31d0f2..85ba538 100644 --- a/backend/src/main/java/de/svencarstensen/muh/web/PortalController.java +++ b/backend/src/main/java/de/svencarstensen/muh/web/PortalController.java @@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -49,8 +50,11 @@ public class PortalController { } @PostMapping("/reports/send") - public ReportService.DispatchResult send(@RequestBody ReportDispatchRequest request) { - return reportService.sendReports(request.sampleIds()); + public ReportService.DispatchResult send( + @RequestHeader(value = "X-MUH-Actor-Id", required = false) String actorId, + @RequestBody ReportDispatchRequest request + ) { + return reportService.sendReports(actorId, request.sampleIds()); } @PatchMapping("/reports/{sampleId}/block") diff --git a/backend/src/main/java/de/svencarstensen/muh/web/SessionController.java b/backend/src/main/java/de/svencarstensen/muh/web/SessionController.java index f2219ed..737f2d5 100644 --- a/backend/src/main/java/de/svencarstensen/muh/web/SessionController.java +++ b/backend/src/main/java/de/svencarstensen/muh/web/SessionController.java @@ -39,8 +39,12 @@ public class SessionController { public CatalogService.UserOption register(@RequestBody RegistrationRequest request) { return catalogService.registerCustomer(new CatalogService.RegistrationMutation( request.companyName(), - request.address(), + request.street(), + request.houseNumber(), + request.postalCode(), + request.city(), request.email(), + request.phoneNumber(), request.password() )); } @@ -53,8 +57,12 @@ public class SessionController { public record RegistrationRequest( @NotBlank String companyName, - @NotBlank String address, + @NotBlank String street, + @NotBlank String houseNumber, + @NotBlank String postalCode, + @NotBlank String city, @NotBlank String email, + @NotBlank String phoneNumber, @NotBlank String password ) { } diff --git a/backend/src/main/java/de/svencarstensen/muh/web/SpaForwardController.java b/backend/src/main/java/de/svencarstensen/muh/web/SpaForwardController.java new file mode 100644 index 0000000..4f52783 --- /dev/null +++ b/backend/src/main/java/de/svencarstensen/muh/web/SpaForwardController.java @@ -0,0 +1,18 @@ +package de.svencarstensen.muh.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class SpaForwardController { + + @GetMapping({ + "/", + "/{path:^(?!api$)[^.]+}", + "/{path:^(?!api$)[^.]+}/{segment:[^.]+}", + "/{path:^(?!api$)[^.]+}/{segment:[^.]+}/{leaf:[^.]+}" + }) + public String forward() { + return "forward:/index.html"; + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 5af48ce..ea916e8 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,12 +1,18 @@ server: port: 8090 + error: + include-message: always spring: + config: + import: + - optional:file:./.env[.properties] + - optional:file:../.env[.properties] application: name: muh-backend data: mongodb: - uri: mongodb://192.168.180.25:27017/muh + uri: ${MUH_MONGODB_URL:mongodb://192.168.180.25:27017/muh} jackson: time-zone: Europe/Berlin mail: @@ -14,8 +20,11 @@ spring: port: ${MUH_MAIL_PORT:587} username: ${MUH_MAIL_USERNAME:} password: ${MUH_MAIL_PASSWORD:} + protocol: ${MUH_MAIL_PROTOCOL:smtp} properties: mail: + transport: + protocol: ${MUH_MAIL_PROTOCOL:smtp} smtp: auth: ${MUH_MAIL_AUTH:false} starttls: @@ -24,6 +33,10 @@ spring: muh: cors: allowed-origins: ${MUH_ALLOWED_ORIGINS:http://localhost:5173,http://localhost:3000} + mongodb: + url: ${MUH_MONGODB_URL:mongodb://192.168.180.25:27017/muh} + username: ${MUH_MONGODB_USERNAME:} + password: ${MUH_MONGODB_PASSWORD:} mail: enabled: ${MUH_MAIL_ENABLED:false} from: ${MUH_MAIL_FROM:no-reply@muh.local} diff --git a/backend/src/main/resources/mail/report-mail-template.txt b/backend/src/main/resources/mail/report-mail-template.txt new file mode 100644 index 0000000..c699229 --- /dev/null +++ b/backend/src/main/resources/mail/report-mail-template.txt @@ -0,0 +1,39 @@ +Anbei finden Sie die Milchprobenauswertung Nr. {{sampleNumber}} fuer Ihre Kuh {{cowLabel}}. + +Dies ist eine automatisch generierte E-Mail von einer System-E-Mail-Adresse, bitte nicht antworten. + +Mit freundlichen Gruessen + +{{customerSignature}} + + +Liste der Handelsnamen und Wirkstoffe + + +Laktation + +Penicillin: Mastinject, Revozyn RTU, Penicillin Injektoren, Ingel Mamyzin +Amoxicillin Clavulansaeure: Synulox RTU/LC, Noroclav +Sulfadoxin Trimetoprim: Diatrim, Sulphix +Lincomycin Neomycin: Albiotic +Tylosin: Pharmasin +Cefquinom: Cobactan +Cefoperacon: Peracef +Enrofloxacin: Powerflox +Cefalexin/Kanamycin: Ubrolexin +Ampicillin/Cloxacillin: Gelstamp + + +Trockensteher + +Framycetin: Benestermycin +Cefalonium: Cepravin +Penicillin: Mastinject, Revozyn RTU, Penicillin Injektoren, Naphpenzal T +Cloxacillin: Orbenin und Cloxacillin +Amoxicillin Clavulansaeure: Synulox RTU/LC +Sulfadoxin Trimetoprim: Diatrim +Lincomycin Neomycin: Albiotic +Tylosin: Pharmasin +Cefquinom: Cobactan +Cefoperacon: Peracef +Enrofloxacin: Powerflox diff --git a/frontend/src/layout/AppShell.tsx b/frontend/src/layout/AppShell.tsx index 7705212..c5798f9 100644 --- a/frontend/src/layout/AppShell.tsx +++ b/frontend/src/layout/AppShell.tsx @@ -1,13 +1,6 @@ import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; import { useSession } from "../lib/session"; -const NAV_ITEMS = [ - { to: "/home", label: "Start" }, - { to: "/samples/new", label: "Neue Probe" }, - { to: "/admin", label: "Verwaltung" }, - { to: "/portal", label: "Portal" }, -]; - const PAGE_TITLES: Record = { "/home": "Startseite", "/samples/new": "Neuanlage einer Probe", @@ -35,6 +28,14 @@ export default function AppShell() { const { user, setUser } = useSession(); const location = useLocation(); const navigate = useNavigate(); + const navItems = user?.role === "ADMIN" + ? [{ to: "/portal", label: "Benutzerverwaltung" }] + : [ + { to: "/home", label: "Start" }, + { to: "/samples/new", label: "Neue Probe" }, + { to: "/admin", label: "Verwaltung" }, + { to: "/portal", label: "Portal" }, + ]; return (
@@ -44,7 +45,7 @@ export default function AppShell() {