diff --git a/backend/src/main/java/de/svencarstensen/muh/domain/Sample.java b/backend/src/main/java/de/svencarstensen/muh/domain/Sample.java index ee98772..205340b 100644 --- a/backend/src/main/java/de/svencarstensen/muh/domain/Sample.java +++ b/backend/src/main/java/de/svencarstensen/muh/domain/Sample.java @@ -27,6 +27,7 @@ public record Sample( LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime completedAt, + String ownerAccountId, String createdByUserCode, String createdByDisplayName ) { diff --git a/backend/src/main/java/de/svencarstensen/muh/security/AuthTokenService.java b/backend/src/main/java/de/svencarstensen/muh/security/AuthTokenService.java index faccb1a..a658ca0 100644 --- a/backend/src/main/java/de/svencarstensen/muh/security/AuthTokenService.java +++ b/backend/src/main/java/de/svencarstensen/muh/security/AuthTokenService.java @@ -22,6 +22,9 @@ public class AuthTokenService { @Value("${muh.security.token-secret}") String secret, @Value("${muh.security.token-validity-hours:12}") long validityHours ) { + if (secret == null || secret.isBlank()) { + throw new IllegalStateException("MUH_TOKEN_SECRET muss gesetzt sein"); + } this.secret = secret.getBytes(StandardCharsets.UTF_8); this.validitySeconds = Math.max(1, validityHours) * 3600L; } diff --git a/backend/src/main/java/de/svencarstensen/muh/security/AuthorizationService.java b/backend/src/main/java/de/svencarstensen/muh/security/AuthorizationService.java new file mode 100644 index 0000000..d590be1 --- /dev/null +++ b/backend/src/main/java/de/svencarstensen/muh/security/AuthorizationService.java @@ -0,0 +1,52 @@ +package de.svencarstensen.muh.security; + +import de.svencarstensen.muh.domain.AppUser; +import de.svencarstensen.muh.domain.UserRole; +import de.svencarstensen.muh.repository.AppUserRepository; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +@Service +public class AuthorizationService { + + private final AppUserRepository appUserRepository; + + public AuthorizationService(AppUserRepository appUserRepository) { + this.appUserRepository = appUserRepository; + } + + public AppUser requireActiveUser(String actorId, String message) { + return appUserRepository.findById(requireText(actorId, message)) + .filter(AppUser::active) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN, message)); + } + + public void requireAdmin(String actorId, String message) { + AppUser actor = requireActiveUser(actorId, message); + if (!isAdmin(actor)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, message); + } + } + + public boolean isAdmin(AppUser user) { + return user.role() == UserRole.ADMIN; + } + + public String accountId(AppUser user) { + if (user.accountId() == null || user.accountId().isBlank()) { + if (user.id() == null || user.id().isBlank()) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Benutzerkonto ungueltig"); + } + return user.id().trim(); + } + return user.accountId().trim(); + } + + private String requireText(String value, String message) { + if (value == null || value.isBlank()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message); + } + return value.trim(); + } +} 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 97a1d27..c7ab3cb 100644 --- a/backend/src/main/java/de/svencarstensen/muh/service/CatalogService.java +++ b/backend/src/main/java/de/svencarstensen/muh/service/CatalogService.java @@ -14,6 +14,7 @@ import de.svencarstensen.muh.repository.FarmerRepository; import de.svencarstensen.muh.repository.MedicationCatalogRepository; import de.svencarstensen.muh.repository.PathogenCatalogRepository; import de.svencarstensen.muh.security.AuthTokenService; +import de.svencarstensen.muh.security.AuthorizationService; import org.springframework.http.HttpStatus; import org.springframework.lang.NonNull; import org.springframework.data.mongodb.core.MongoTemplate; @@ -66,6 +67,7 @@ public class CatalogService { private final AppUserRepository appUserRepository; private final MongoTemplate mongoTemplate; private final AuthTokenService authTokenService; + private final AuthorizationService authorizationService; private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); public CatalogService( @@ -75,7 +77,8 @@ public class CatalogService { AntibioticCatalogRepository antibioticRepository, AppUserRepository appUserRepository, MongoTemplate mongoTemplate, - AuthTokenService authTokenService + AuthTokenService authTokenService, + AuthorizationService authorizationService ) { this.farmerRepository = farmerRepository; this.medicationRepository = medicationRepository; @@ -84,6 +87,7 @@ public class CatalogService { this.appUserRepository = appUserRepository; this.mongoTemplate = mongoTemplate; this.authTokenService = authTokenService; + this.authorizationService = authorizationService; } public ActiveCatalogSummary activeCatalogSummary() { @@ -96,7 +100,8 @@ public class CatalogService { ); } - public AdministrationOverview administrationOverview() { + public AdministrationOverview administrationOverview(String actorId) { + authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); return new AdministrationOverview(listFarmerRows(), listMedicationRows(), listPathogenRows(), listAntibioticRows()); } @@ -128,7 +133,8 @@ public class CatalogService { .toList(); } - public List saveFarmers(List mutations) { + public List saveFarmers(String actorId, List mutations) { + authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); for (FarmerMutation mutation : mutations) { if (isBlank(mutation.name())) { continue; @@ -191,7 +197,8 @@ public class CatalogService { return listFarmerRows(); } - public List saveMedications(List mutations) { + public List saveMedications(String actorId, List mutations) { + authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); for (MedicationMutation mutation : mutations) { if (isBlank(mutation.name()) || mutation.category() == null) { continue; @@ -254,7 +261,8 @@ public class CatalogService { return listMedicationRows(); } - public List savePathogens(List mutations) { + public List savePathogens(String actorId, List mutations) { + authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); for (PathogenMutation mutation : mutations) { if (isBlank(mutation.name()) || mutation.kind() == null) { continue; @@ -322,7 +330,8 @@ public class CatalogService { return listPathogenRows(); } - public List saveAntibiotics(List mutations) { + public List saveAntibiotics(String actorId, List mutations) { + authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); for (AntibioticMutation mutation : mutations) { if (isBlank(mutation.name())) { continue; diff --git a/backend/src/main/java/de/svencarstensen/muh/service/PortalService.java b/backend/src/main/java/de/svencarstensen/muh/service/PortalService.java index 4f32846..19f08b9 100644 --- a/backend/src/main/java/de/svencarstensen/muh/service/PortalService.java +++ b/backend/src/main/java/de/svencarstensen/muh/service/PortalService.java @@ -36,20 +36,20 @@ public class PortalService { List sampleRows; if (sampleNumber != null) { - sampleRows = List.of(toPortalRow(sampleService.getSampleByNumber(sampleNumber))); + sampleRows = List.of(toPortalRow(sampleService.getSampleByNumber(actorId, sampleNumber))); } else if (farmerBusinessKey != null && !farmerBusinessKey.isBlank()) { - sampleRows = sampleService.samplesByFarmerBusinessKey(farmerBusinessKey).stream() + sampleRows = sampleService.samplesByFarmerBusinessKey(actorId, farmerBusinessKey).stream() .filter(sample -> cowQuery == null || cowQuery.isBlank() || cowMatches(sample, cowQuery)) .map(this::toPortalRow) .sorted(Comparator.comparing(PortalSampleRow::createdAt).reversed()) .toList(); } else if (date != null) { - sampleRows = sampleService.samplesByDate(date).stream() + sampleRows = sampleService.samplesByDate(actorId, date).stream() .map(this::toPortalRow) .sorted(Comparator.comparing(PortalSampleRow::completedAt, Comparator.nullsLast(Comparator.reverseOrder()))) .toList(); } else { - sampleRows = sampleService.completedSamples().stream() + sampleRows = sampleService.completedSamples(actorId).stream() .limit(25) .map(this::toPortalRow) .toList(); @@ -58,13 +58,13 @@ public class PortalService { return new PortalSnapshot( matchingFarmers, sampleRows, - reportService.reportCandidates(), + reportService.reportCandidates(actorId), includeUsers ? catalogService.listUsers(actorId) : List.of() ); } - public List searchSamplesByCreatedDate(LocalDate date) { - return sampleService.samplesByCreatedDate(date).stream() + public List searchSamplesByCreatedDate(String actorId, LocalDate date) { + return sampleService.samplesByCreatedDate(actorId, date).stream() .map(this::toPortalRow) .sorted(Comparator.comparing(PortalSampleRow::createdAt).reversed()) .toList(); 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 1bf5e0d..61277fc 100644 --- a/backend/src/main/java/de/svencarstensen/muh/service/ReportService.java +++ b/backend/src/main/java/de/svencarstensen/muh/service/ReportService.java @@ -53,8 +53,8 @@ public class ReportService { this.reportMailTemplate = loadTemplate(reportMailTemplateResource); } - public List reportCandidates() { - return sampleService.completedSamples().stream() + public List reportCandidates(String actorId) { + return sampleService.completedSamples(actorId).stream() .filter(sample -> sample.farmerEmail() != null && !sample.farmerEmail().isBlank()) .map(this::toCandidate) .toList(); @@ -66,7 +66,7 @@ public class ReportService { List skipped = new ArrayList<>(); for (String sampleId : sampleIds) { - Sample sample = sampleService.loadSampleEntity(sampleId); + Sample sample = sampleService.loadSampleEntity(actorId, sampleId); if (sample.farmerEmail() == null || sample.farmerEmail().isBlank() || sample.reportBlocked()) { skipped.add(toCandidate(sample)); continue; @@ -82,12 +82,13 @@ public class ReportService { return new DispatchResult(sent, skipped, mailEnabled && mailSenderProvider.getIfAvailable() != null); } - public byte[] reportPdf(String sampleId) { - return buildPdf(sampleService.loadSampleEntity(sampleId)); + public byte[] reportPdf(String actorId, String sampleId) { + return buildPdf(sampleService.loadSampleEntity(actorId, sampleId)); } - public SampleService.SampleDetail toggleReportBlocked(String sampleId, boolean blocked) { - return sampleService.getSample(sampleService.toggleReportBlocked(sampleId, blocked).id()); + public SampleService.SampleDetail toggleReportBlocked(String actorId, String sampleId, boolean blocked) { + Sample sample = sampleService.loadSampleEntity(actorId, sampleId); + return sampleService.getSample(actorId, sampleService.toggleReportBlocked(sample.id(), blocked).id()); } private void sendMail(Sample sample, byte[] pdf, String customerSignature) { diff --git a/backend/src/main/java/de/svencarstensen/muh/service/SampleService.java b/backend/src/main/java/de/svencarstensen/muh/service/SampleService.java index cec3a6e..a2f7266 100644 --- a/backend/src/main/java/de/svencarstensen/muh/service/SampleService.java +++ b/backend/src/main/java/de/svencarstensen/muh/service/SampleService.java @@ -1,6 +1,7 @@ package de.svencarstensen.muh.service; import de.svencarstensen.muh.domain.AntibiogramEntry; +import de.svencarstensen.muh.domain.AppUser; import de.svencarstensen.muh.domain.PathogenCatalogItem; import de.svencarstensen.muh.domain.PathogenKind; import de.svencarstensen.muh.domain.QuarterAntibiogram; @@ -13,6 +14,8 @@ import de.svencarstensen.muh.domain.SamplingMode; import de.svencarstensen.muh.domain.SensitivityResult; import de.svencarstensen.muh.domain.TherapyRecommendation; import de.svencarstensen.muh.repository.SampleRepository; +import de.svencarstensen.muh.repository.AppUserRepository; +import de.svencarstensen.muh.security.AuthorizationService; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; @@ -25,33 +28,54 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; @Service public class SampleService { private final SampleRepository sampleRepository; private final CatalogService catalogService; + private final AppUserRepository appUserRepository; + private final AuthorizationService authorizationService; - public SampleService(SampleRepository sampleRepository, CatalogService catalogService) { + public SampleService( + SampleRepository sampleRepository, + CatalogService catalogService, + AppUserRepository appUserRepository, + AuthorizationService authorizationService + ) { this.sampleRepository = sampleRepository; this.catalogService = catalogService; + this.appUserRepository = appUserRepository; + this.authorizationService = authorizationService; } - public DashboardOverview dashboardOverview() { - List recent = sampleRepository.findTop12ByOrderByUpdatedAtDesc().stream() + public DashboardOverview dashboardOverview(String actorId) { + AppUser actor = requireActor(actorId); + List accessibleSamples = accessibleSamples(actor); + List recent = accessibleSamples.stream() + .sorted(Comparator.comparing(Sample::updatedAt).reversed()) + .limit(12) .map(this::toSummary) .toList(); - long openCount = sampleRepository.findAll().stream().filter(sample -> sample.currentStep() != SampleWorkflowStep.COMPLETED).count(); + long openCount = accessibleSamples.stream() + .filter(sample -> sample.currentStep() != SampleWorkflowStep.COMPLETED) + .count(); LocalDate today = LocalDate.now(); - long completedToday = sampleRepository.findByCompletedAtBetweenOrderByCompletedAtDesc( - today.atStartOfDay(), - today.plusDays(1).atStartOfDay() - ).size(); + long completedToday = accessibleSamples.stream() + .filter(sample -> sample.completedAt() != null) + .filter(sample -> !sample.completedAt().isBefore(today.atStartOfDay())) + .filter(sample -> sample.completedAt().isBefore(today.plusDays(1).atStartOfDay())) + .count(); return new DashboardOverview(nextSampleNumber(), openCount, completedToday, recent); } - public LookupResult lookup(long sampleNumber) { + public LookupResult lookup(String actorId, long sampleNumber) { + AppUser actor = requireActor(actorId); return sampleRepository.findBySampleNumber(sampleNumber) + .filter(sample -> canAccess(actor, sample)) .map(sample -> new LookupResult( true, "Probe gefunden", @@ -62,17 +86,20 @@ public class SampleService { .orElseGet(() -> new LookupResult(false, "Proben-Nummer unbekannt", null, null, null)); } - public SampleDetail getSample(String id) { - return toDetail(loadSample(id)); + public SampleDetail getSample(String actorId, String id) { + return toDetail(loadAccessibleSample(actorId, id)); } - public SampleDetail getSampleByNumber(long sampleNumber) { + public SampleDetail getSampleByNumber(String actorId, long sampleNumber) { + AppUser actor = requireActor(actorId); return sampleRepository.findBySampleNumber(sampleNumber) + .filter(sample -> canAccess(actor, sample)) .map(this::toDetail) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Probe nicht gefunden")); } - public SampleDetail createSample(RegistrationRequest request) { + public SampleDetail createSample(String actorId, RegistrationRequest request) { + AppUser actor = requireActor(actorId); LocalDateTime now = LocalDateTime.now(); CatalogService.FarmerOption farmer = catalogService.activeCatalogSummary().farmers().stream() .filter(candidate -> candidate.businessKey().equals(request.farmerBusinessKey())) @@ -99,6 +126,7 @@ public class SampleService { now, now, null, + authorizationService.accountId(actor), request.userCode(), request.userDisplayName() ); @@ -106,8 +134,8 @@ public class SampleService { return toDetail(sampleRepository.save(sample)); } - public SampleDetail saveRegistration(String id, RegistrationRequest request) { - Sample existing = loadSample(id); + public SampleDetail saveRegistration(String actorId, String id, RegistrationRequest request) { + Sample existing = loadAccessibleSample(actorId, id); if (!SampleWorkflowRules.canEditRegistration(existing)) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Stammdaten können nicht mehr geändert werden"); } @@ -137,6 +165,7 @@ public class SampleService { existing.createdAt(), LocalDateTime.now(), existing.completedAt(), + existing.ownerAccountId(), existing.createdByUserCode(), existing.createdByDisplayName() )); @@ -144,8 +173,8 @@ public class SampleService { return toDetail(saved); } - public SampleDetail saveAnamnesis(String id, AnamnesisRequest request) { - Sample existing = loadSample(id); + public SampleDetail saveAnamnesis(String actorId, String id, AnamnesisRequest request) { + Sample existing = loadAccessibleSample(actorId, id); if (!SampleWorkflowRules.canEditAnamnesis(existing)) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Anamnese kann an dieser Stelle nicht geändert werden"); } @@ -203,14 +232,15 @@ public class SampleService { existing.createdAt(), LocalDateTime.now(), existing.completedAt(), + existing.ownerAccountId(), existing.createdByUserCode(), existing.createdByDisplayName() )); return toDetail(saved); } - public SampleDetail saveAntibiogram(String id, AntibiogramRequest request) { - Sample existing = loadSample(id); + public SampleDetail saveAntibiogram(String actorId, String id, AntibiogramRequest request) { + Sample existing = loadAccessibleSample(actorId, id); if (!SampleWorkflowRules.canEditAntibiogram(existing)) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Antibiogramm kann nicht mehr geändert werden"); } @@ -293,14 +323,15 @@ public class SampleService { existing.createdAt(), LocalDateTime.now(), existing.completedAt(), + existing.ownerAccountId(), existing.createdByUserCode(), existing.createdByDisplayName() )); return toDetail(saved); } - public SampleDetail saveTherapy(String id, TherapyRequest request) { - Sample existing = loadSample(id); + public SampleDetail saveTherapy(String actorId, String id, TherapyRequest request) { + Sample existing = loadAccessibleSample(actorId, id); if (existing.currentStep() == SampleWorkflowStep.COMPLETED) { TherapyRecommendation previous = existing.therapyRecommendation(); TherapyRecommendation updated = previous == null @@ -341,6 +372,7 @@ public class SampleService { existing.createdAt(), LocalDateTime.now(), existing.completedAt(), + existing.ownerAccountId(), existing.createdByUserCode(), existing.createdByDisplayName() ))); @@ -388,6 +420,7 @@ public class SampleService { existing.createdAt(), LocalDateTime.now(), LocalDateTime.now(), + existing.ownerAccountId(), existing.createdByUserCode(), existing.createdByDisplayName() )); @@ -416,6 +449,7 @@ public class SampleService { existing.createdAt(), LocalDateTime.now(), existing.completedAt(), + existing.ownerAccountId(), existing.createdByUserCode(), existing.createdByDisplayName() )); @@ -443,25 +477,41 @@ public class SampleService { existing.createdAt(), LocalDateTime.now(), existing.completedAt(), + existing.ownerAccountId(), existing.createdByUserCode(), existing.createdByDisplayName() )); } - public List completedSamples() { - return sampleRepository.findByCompletedAtNotNullOrderByCompletedAtDesc(); + public List completedSamples(String actorId) { + return accessibleSamples(requireActor(actorId)).stream() + .filter(sample -> sample.completedAt() != null) + .sorted(Comparator.comparing(Sample::completedAt).reversed()) + .toList(); } - public List samplesByFarmerBusinessKey(String businessKey) { - return sampleRepository.findByFarmerBusinessKeyOrderByCreatedAtDesc(businessKey); + public List samplesByFarmerBusinessKey(String actorId, String businessKey) { + return accessibleSamples(requireActor(actorId)).stream() + .filter(sample -> Objects.equals(sample.farmerBusinessKey(), businessKey)) + .sorted(Comparator.comparing(Sample::createdAt).reversed()) + .toList(); } - public List samplesByCreatedDate(LocalDate date) { - return sampleRepository.findByCreatedAtBetweenOrderByCreatedAtDesc(date.atStartOfDay(), date.plusDays(1).atStartOfDay()); + public List samplesByCreatedDate(String actorId, LocalDate date) { + return accessibleSamples(requireActor(actorId)).stream() + .filter(sample -> !sample.createdAt().isBefore(date.atStartOfDay())) + .filter(sample -> sample.createdAt().isBefore(date.plusDays(1).atStartOfDay())) + .sorted(Comparator.comparing(Sample::createdAt).reversed()) + .toList(); } - public List samplesByDate(LocalDate date) { - return sampleRepository.findByCompletedAtBetweenOrderByCompletedAtDesc(date.atStartOfDay(), date.plusDays(1).atStartOfDay()); + public List samplesByDate(String actorId, LocalDate date) { + return accessibleSamples(requireActor(actorId)).stream() + .filter(sample -> sample.completedAt() != null) + .filter(sample -> !sample.completedAt().isBefore(date.atStartOfDay())) + .filter(sample -> sample.completedAt().isBefore(date.plusDays(1).atStartOfDay())) + .sorted(Comparator.comparing(Sample::completedAt).reversed()) + .toList(); } public long nextSampleNumber() { @@ -470,15 +520,119 @@ public class SampleService { .orElse(100001L); } - public Sample loadSampleEntity(String id) { - return loadSample(id); + public Sample loadSampleEntity(String actorId, String id) { + return loadAccessibleSample(actorId, id); + } + + private AppUser requireActor(String actorId) { + ensureSampleOwnershipMigration(); + return authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); + } + + private List accessibleSamples(AppUser actor) { + List samples = sampleRepository.findAll(); + if (authorizationService.isAdmin(actor)) { + return samples; + } + String accountId = authorizationService.accountId(actor); + return samples.stream() + .filter(sample -> Objects.equals(sample.ownerAccountId(), accountId)) + .toList(); + } + + private boolean canAccess(AppUser actor, Sample sample) { + return authorizationService.isAdmin(actor) + || Objects.equals(sample.ownerAccountId(), authorizationService.accountId(actor)); + } + + private Sample loadAccessibleSample(String actorId, String id) { + AppUser actor = requireActor(actorId); + Sample sample = loadSample(id); + if (!canAccess(actor, sample)) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Probe nicht gefunden"); + } + return sample; } private Sample loadSample(String id) { + ensureSampleOwnershipMigration(); return sampleRepository.findById(Objects.requireNonNull(id)) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Probe nicht gefunden")); } + private void ensureSampleOwnershipMigration() { + List samples = sampleRepository.findAll().stream() + .filter(sample -> sample.ownerAccountId() == null || sample.ownerAccountId().isBlank()) + .toList(); + if (samples.isEmpty()) { + return; + } + + List users = appUserRepository.findAll().stream() + .filter(AppUser::active) + .toList(); + Map> usersByDisplayName = users.stream() + .filter(user -> user.displayName() != null && !user.displayName().isBlank()) + .collect(Collectors.groupingBy(user -> user.displayName().trim().toLowerCase())); + List primaryCustomerAccounts = users.stream() + .filter(user -> user.role() != de.svencarstensen.muh.domain.UserRole.ADMIN) + .filter(user -> Boolean.TRUE.equals(user.primaryUser())) + .map(authorizationService::accountId) + .distinct() + .toList(); + String fallbackAccountId = primaryCustomerAccounts.size() == 1 ? primaryCustomerAccounts.get(0) : null; + + for (Sample sample : samples) { + String resolvedAccountId = resolveSampleOwnerAccountId(sample, usersByDisplayName, fallbackAccountId); + if (resolvedAccountId == null) { + continue; + } + sampleRepository.save(new Sample( + sample.id(), + sample.sampleNumber(), + sample.farmerBusinessKey(), + sample.farmerName(), + sample.farmerEmail(), + sample.cowNumber(), + sample.cowName(), + sample.sampleKind(), + sample.samplingMode(), + sample.currentStep(), + sample.quarters(), + sample.antibiograms(), + sample.therapyRecommendation(), + sample.reportSent(), + sample.reportBlocked(), + sample.reportSentAt(), + sample.createdAt(), + sample.updatedAt(), + sample.completedAt(), + resolvedAccountId, + sample.createdByUserCode(), + sample.createdByDisplayName() + )); + } + } + + private String resolveSampleOwnerAccountId( + Sample sample, + Map> usersByDisplayName, + String fallbackAccountId + ) { + if (sample.createdByDisplayName() != null && !sample.createdByDisplayName().isBlank()) { + List matchingAccounts = usersByDisplayName + .getOrDefault(sample.createdByDisplayName().trim().toLowerCase(), List.of()) + .stream() + .map(authorizationService::accountId) + .distinct() + .toList(); + if (matchingAccounts.size() == 1) { + return matchingAccounts.get(0); + } + } + return fallbackAccountId; + } + private SampleSummary toSummary(Sample sample) { return new SampleSummary( sample.id(), 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 0a1cce4..1d1555d 100644 --- a/backend/src/main/java/de/svencarstensen/muh/web/CatalogController.java +++ b/backend/src/main/java/de/svencarstensen/muh/web/CatalogController.java @@ -32,27 +32,27 @@ public class CatalogController { @GetMapping("/admin") public CatalogService.AdministrationOverview administrationOverview() { - return catalogService.administrationOverview(); + return catalogService.administrationOverview(securitySupport.currentUser().id()); } @PostMapping("/admin/farmers") public List saveFarmers(@RequestBody List mutations) { - return catalogService.saveFarmers(mutations); + return catalogService.saveFarmers(securitySupport.currentUser().id(), mutations); } @PostMapping("/admin/medications") public List saveMedications(@RequestBody List mutations) { - return catalogService.saveMedications(mutations); + return catalogService.saveMedications(securitySupport.currentUser().id(), mutations); } @PostMapping("/admin/pathogens") public List savePathogens(@RequestBody List mutations) { - return catalogService.savePathogens(mutations); + return catalogService.savePathogens(securitySupport.currentUser().id(), mutations); } @PostMapping("/admin/antibiotics") public List saveAntibiotics(@RequestBody List mutations) { - return catalogService.saveAntibiotics(mutations); + return catalogService.saveAntibiotics(securitySupport.currentUser().id(), mutations); } @GetMapping("/portal/users") 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 01f5ed4..da29ced 100644 --- a/backend/src/main/java/de/svencarstensen/muh/web/PortalController.java +++ b/backend/src/main/java/de/svencarstensen/muh/web/PortalController.java @@ -58,12 +58,12 @@ public class PortalController { @GetMapping("/reports") public List reports() { - return reportService.reportCandidates(); + return reportService.reportCandidates(securitySupport.currentUser().id()); } @GetMapping("/search/by-date") public List searchByDate(@RequestParam LocalDate date) { - return portalService.searchSamplesByCreatedDate(date); + return portalService.searchSamplesByCreatedDate(securitySupport.currentUser().id(), date); } @PostMapping("/reports/send") @@ -73,12 +73,12 @@ public class PortalController { @PatchMapping("/reports/{sampleId}/block") public SampleService.SampleDetail block(@PathVariable String sampleId, @RequestBody BlockRequest request) { - return reportService.toggleReportBlocked(sampleId, request.blocked()); + return reportService.toggleReportBlocked(securitySupport.currentUser().id(), sampleId, request.blocked()); } @GetMapping("/reports/{sampleId}/pdf") public ResponseEntity pdf(@PathVariable String sampleId) { - byte[] pdf = reportService.reportPdf(sampleId); + byte[] pdf = reportService.reportPdf(securitySupport.currentUser().id(), sampleId); return ResponseEntity.ok() .contentType(Objects.requireNonNull(MediaType.APPLICATION_PDF)) .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.inline() diff --git a/backend/src/main/java/de/svencarstensen/muh/web/SampleController.java b/backend/src/main/java/de/svencarstensen/muh/web/SampleController.java index b33ca87..7e211d6 100644 --- a/backend/src/main/java/de/svencarstensen/muh/web/SampleController.java +++ b/backend/src/main/java/de/svencarstensen/muh/web/SampleController.java @@ -25,28 +25,28 @@ public class SampleController { @GetMapping("/dashboard") public SampleService.DashboardOverview dashboardOverview() { - return sampleService.dashboardOverview(); + return sampleService.dashboardOverview(securitySupport.currentUser().id()); } @GetMapping("/dashboard/lookup/{sampleNumber}") public SampleService.LookupResult lookup(@PathVariable long sampleNumber) { - return sampleService.lookup(sampleNumber); + return sampleService.lookup(securitySupport.currentUser().id(), sampleNumber); } @GetMapping("/samples/{id}") public SampleService.SampleDetail sample(@PathVariable String id) { - return sampleService.getSample(id); + return sampleService.getSample(securitySupport.currentUser().id(), id); } @GetMapping("/samples/by-number/{sampleNumber}") public SampleService.SampleDetail sampleByNumber(@PathVariable long sampleNumber) { - return sampleService.getSampleByNumber(sampleNumber); + return sampleService.getSampleByNumber(securitySupport.currentUser().id(), sampleNumber); } @PostMapping("/samples") public SampleService.SampleDetail create(@RequestBody SampleService.RegistrationRequest request) { AuthenticatedUser user = securitySupport.currentUser(); - return sampleService.createSample(new SampleService.RegistrationRequest( + return sampleService.createSample(user.id(), new SampleService.RegistrationRequest( request.farmerBusinessKey(), request.cowNumber(), request.cowName(), @@ -60,22 +60,22 @@ public class SampleController { @PutMapping("/samples/{id}/registration") public SampleService.SampleDetail saveRegistration(@PathVariable String id, @RequestBody SampleService.RegistrationRequest request) { - return sampleService.saveRegistration(id, request); + return sampleService.saveRegistration(securitySupport.currentUser().id(), id, request); } @PutMapping("/samples/{id}/anamnesis") public SampleService.SampleDetail saveAnamnesis(@PathVariable String id, @RequestBody SampleService.AnamnesisRequest request) { - return sampleService.saveAnamnesis(id, request); + return sampleService.saveAnamnesis(securitySupport.currentUser().id(), id, request); } @PutMapping("/samples/{id}/antibiogram") public SampleService.SampleDetail saveAntibiogram(@PathVariable String id, @RequestBody SampleService.AntibiogramRequest request) { - return sampleService.saveAntibiogram(id, request); + return sampleService.saveAntibiogram(securitySupport.currentUser().id(), id, request); } @PutMapping("/samples/{id}/therapy") public SampleService.SampleDetail saveTherapy(@PathVariable String id, @RequestBody SampleService.TherapyRequest request) { - return sampleService.saveTherapy(id, request); + return sampleService.saveTherapy(securitySupport.currentUser().id(), id, request); } private String deriveUserLabel(String displayName) { diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 9a953c3..56b7d16 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -34,7 +34,7 @@ muh: cors: allowed-origins: ${MUH_ALLOWED_ORIGINS:http://localhost:5173,http://localhost:3000} security: - token-secret: ${MUH_TOKEN_SECRET:change-me-in-production} + token-secret: ${MUH_TOKEN_SECRET:} token-validity-hours: ${MUH_TOKEN_VALIDITY_HOURS:12} mongodb: url: ${MUH_MONGODB_URL:mongodb://192.168.180.25:27017/muh} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e77a182..a4fac72 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,6 +16,7 @@ import UserManagementPage from "./pages/UserManagementPage"; function ProtectedRoutes() { const { user, ready } = useSession(); + const isAdmin = user?.role === "ADMIN"; if (!ready) { return
Sitzung wird geladen ...
; @@ -34,7 +35,7 @@ function ProtectedRoutes() { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/layout/AppShell.tsx b/frontend/src/layout/AppShell.tsx index b82e1db..b2fcd85 100644 --- a/frontend/src/layout/AppShell.tsx +++ b/frontend/src/layout/AppShell.tsx @@ -21,19 +21,19 @@ function resolvePageTitle(pathname: string) { return "Probe bearbeiten"; } if (pathname.startsWith("/admin/landwirte")) { - return "Verwaltung | Landwirte"; + return "Die Verwaltung der Landwirte"; } if (pathname.startsWith("/admin/benutzer")) { return "Verwaltung | Benutzer"; } if (pathname.startsWith("/admin/medikamente")) { - return "Verwaltung | Medikamente"; + return "Die Verwaltung der Medikamente"; } if (pathname.startsWith("/admin/erreger")) { - return "Verwaltung | Erreger"; + return "Die Verwaltung der Erreger"; } if (pathname.startsWith("/admin/antibiogramm")) { - return "Verwaltung | Antibiogramm"; + return "Die Verwaltung der Antibiogramme"; } if (pathname.startsWith("/search/landwirt")) { return "Suche | Landwirt"; @@ -79,9 +79,6 @@ export default function AppShell() {
Verwaltung
- `nav-sublink ${isActive ? "is-active" : ""}`}> - Benutzer - `nav-sublink ${isActive ? "is-active" : ""}`}> Landwirte @@ -94,6 +91,9 @@ export default function AppShell() { `nav-sublink ${isActive ? "is-active" : ""}`}> Antibiogramm + `nav-sublink ${isActive ? "is-active" : ""}`}> + Benutzer +
diff --git a/frontend/src/pages/AdministrationPage.tsx b/frontend/src/pages/AdministrationPage.tsx index bac786a..bc72fd2 100644 --- a/frontend/src/pages/AdministrationPage.tsx +++ b/frontend/src/pages/AdministrationPage.tsx @@ -26,6 +26,13 @@ const DATASET_LABELS: Record = { antibiotics: "Antibiogramm", }; +const DATASET_TITLES: Record = { + farmers: "Die Verwaltung der Landwirte", + medications: "Die Verwaltung der Medikamente", + pathogens: "Die Verwaltung der Erreger", + antibiotics: "Die Verwaltung der Antibiogramme", +}; + function normalizeOverview(overview: AdministrationOverview): DatasetsState { return { farmers: overview.farmers.map((entry) => ({ @@ -216,7 +223,7 @@ export default function AdministrationPage() {

Verwaltung

-

Stammdaten direkt pflegen

+

{DATASET_TITLES[selectedDataset]}

Bestehende Datensaetze lassen sich inline aendern. Bei Umbenennungen bleibt der alte Satz inaktiv sichtbar.