From f9b83a166d4f110d53e850c0b45a3bfc6dbce9bf Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Wed, 18 Mar 2026 20:40:10 +0100 Subject: [PATCH] feat: isolate catalog data per primary user (accountId) - Add accountId to Farmer, MedicationCatalogItem, PathogenCatalogItem, AntibioticCatalogItem - Create new /api/catalog endpoints for CUSTOMER role only - Remove DemoDataInitializer (no more demo data generation) - Add DefaultUserInitializer for admin user creation only - Update repositories with accountId-based query methods - Update CatalogService with accountId isolation and role-based access - Update SecurityConfig: /api/catalog/** for CUSTOMER, /api/admin/** for ADMIN - Update frontend AdministrationPage to use new /api/catalog endpoints - Migrate existing data without accountId to first admin user --- README.md | 1 + .../muh/domain/AntibioticCatalogItem.java | 1 + .../de/svencarstensen/muh/domain/Farmer.java | 1 + .../muh/domain/MedicationCatalogItem.java | 1 + .../muh/domain/PathogenCatalogItem.java | 1 + .../AntibioticCatalogRepository.java | 4 + .../muh/repository/FarmerRepository.java | 4 + .../MedicationCatalogRepository.java | 4 + .../repository/PathogenCatalogRepository.java | 4 + .../muh/security/SecurityConfig.java | 1 + .../muh/service/CatalogService.java | 202 +++++++++++++++--- .../muh/service/DefaultUserInitializer.java | 20 ++ .../muh/service/DemoDataInitializer.java | 76 ------- .../muh/service/PortalService.java | 2 +- .../muh/service/SampleService.java | 10 +- .../muh/web/CatalogController.java | 35 ++- frontend/src/lib/version.ts | 2 +- frontend/src/pages/AdministrationPage.tsx | 10 +- 18 files changed, 256 insertions(+), 123 deletions(-) create mode 100644 backend/src/main/java/de/svencarstensen/muh/service/DefaultUserInitializer.java delete mode 100644 backend/src/main/java/de/svencarstensen/muh/service/DemoDataInitializer.java diff --git a/README.md b/README.md index d3b0034..55aee10 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ Kundenregistrierung: - `cd frontend && npm run build` ## Docker +docker build -t muh:0.9.1 . docker buildx build --platform linux/amd64 -t gitea.appcreation.de/sven/muh:0.8.0 --push . docker run -d --name muh --network br0 --ip 192.168.180.26 --restart unless-stopped -e MUH_MONGODB_URL=mongodb://192.168.180.25:27017/muh -e MUH_TOKEN_SECRET=local-dev-muh-token-secret-2026-03-13 -e MUH_TOKEN_VALIDITY_HOURS=12 -e MUH_ALLOWED_ORIGINS=https://muh.appcreation.de gitea.appcreation.de/sven/muh:0.8.0 \ No newline at end of file diff --git a/backend/src/main/java/de/svencarstensen/muh/domain/AntibioticCatalogItem.java b/backend/src/main/java/de/svencarstensen/muh/domain/AntibioticCatalogItem.java index 22b69e4..2cd97d4 100644 --- a/backend/src/main/java/de/svencarstensen/muh/domain/AntibioticCatalogItem.java +++ b/backend/src/main/java/de/svencarstensen/muh/domain/AntibioticCatalogItem.java @@ -8,6 +8,7 @@ import java.time.LocalDateTime; @Document("antibiotics") public record AntibioticCatalogItem( @Id String id, + String accountId, String businessKey, String code, String name, diff --git a/backend/src/main/java/de/svencarstensen/muh/domain/Farmer.java b/backend/src/main/java/de/svencarstensen/muh/domain/Farmer.java index fc37fd2..5cb5548 100644 --- a/backend/src/main/java/de/svencarstensen/muh/domain/Farmer.java +++ b/backend/src/main/java/de/svencarstensen/muh/domain/Farmer.java @@ -8,6 +8,7 @@ import java.time.LocalDateTime; @Document("farmers") public record Farmer( @Id String id, + String accountId, String businessKey, String name, String email, diff --git a/backend/src/main/java/de/svencarstensen/muh/domain/MedicationCatalogItem.java b/backend/src/main/java/de/svencarstensen/muh/domain/MedicationCatalogItem.java index 3468b64..058520d 100644 --- a/backend/src/main/java/de/svencarstensen/muh/domain/MedicationCatalogItem.java +++ b/backend/src/main/java/de/svencarstensen/muh/domain/MedicationCatalogItem.java @@ -8,6 +8,7 @@ import java.time.LocalDateTime; @Document("medications") public record MedicationCatalogItem( @Id String id, + String accountId, String businessKey, String name, MedicationCategory category, diff --git a/backend/src/main/java/de/svencarstensen/muh/domain/PathogenCatalogItem.java b/backend/src/main/java/de/svencarstensen/muh/domain/PathogenCatalogItem.java index 0c110d2..5c6f450 100644 --- a/backend/src/main/java/de/svencarstensen/muh/domain/PathogenCatalogItem.java +++ b/backend/src/main/java/de/svencarstensen/muh/domain/PathogenCatalogItem.java @@ -8,6 +8,7 @@ import java.time.LocalDateTime; @Document("pathogens") public record PathogenCatalogItem( @Id String id, + String accountId, String businessKey, String code, String name, diff --git a/backend/src/main/java/de/svencarstensen/muh/repository/AntibioticCatalogRepository.java b/backend/src/main/java/de/svencarstensen/muh/repository/AntibioticCatalogRepository.java index 64f8107..780becc 100644 --- a/backend/src/main/java/de/svencarstensen/muh/repository/AntibioticCatalogRepository.java +++ b/backend/src/main/java/de/svencarstensen/muh/repository/AntibioticCatalogRepository.java @@ -7,4 +7,8 @@ import java.util.List; public interface AntibioticCatalogRepository extends MongoRepository { List findByActiveTrueOrderByNameAsc(); + + List findByAccountIdOrderByNameAsc(String accountId); + + List findByAccountIdAndActiveTrueOrderByNameAsc(String accountId); } diff --git a/backend/src/main/java/de/svencarstensen/muh/repository/FarmerRepository.java b/backend/src/main/java/de/svencarstensen/muh/repository/FarmerRepository.java index 741cec0..ab2ac2a 100644 --- a/backend/src/main/java/de/svencarstensen/muh/repository/FarmerRepository.java +++ b/backend/src/main/java/de/svencarstensen/muh/repository/FarmerRepository.java @@ -8,5 +8,9 @@ import java.util.List; public interface FarmerRepository extends MongoRepository { List findByActiveTrueOrderByNameAsc(); + List findByAccountIdOrderByNameAsc(String accountId); + + List findByAccountIdAndActiveTrueOrderByNameAsc(String accountId); + List findByNameContainingIgnoreCaseOrderByNameAsc(String name); } diff --git a/backend/src/main/java/de/svencarstensen/muh/repository/MedicationCatalogRepository.java b/backend/src/main/java/de/svencarstensen/muh/repository/MedicationCatalogRepository.java index 867777c..8229c6c 100644 --- a/backend/src/main/java/de/svencarstensen/muh/repository/MedicationCatalogRepository.java +++ b/backend/src/main/java/de/svencarstensen/muh/repository/MedicationCatalogRepository.java @@ -7,4 +7,8 @@ import java.util.List; public interface MedicationCatalogRepository extends MongoRepository { List findByActiveTrueOrderByNameAsc(); + + List findByAccountIdOrderByNameAsc(String accountId); + + List findByAccountIdAndActiveTrueOrderByNameAsc(String accountId); } diff --git a/backend/src/main/java/de/svencarstensen/muh/repository/PathogenCatalogRepository.java b/backend/src/main/java/de/svencarstensen/muh/repository/PathogenCatalogRepository.java index 10f557c..d4803b5 100644 --- a/backend/src/main/java/de/svencarstensen/muh/repository/PathogenCatalogRepository.java +++ b/backend/src/main/java/de/svencarstensen/muh/repository/PathogenCatalogRepository.java @@ -7,4 +7,8 @@ import java.util.List; public interface PathogenCatalogRepository extends MongoRepository { List findByActiveTrueOrderByNameAsc(); + + List findByAccountIdOrderByNameAsc(String accountId); + + List findByAccountIdAndActiveTrueOrderByNameAsc(String accountId); } diff --git a/backend/src/main/java/de/svencarstensen/muh/security/SecurityConfig.java b/backend/src/main/java/de/svencarstensen/muh/security/SecurityConfig.java index ce2f85c..7f0dee9 100644 --- a/backend/src/main/java/de/svencarstensen/muh/security/SecurityConfig.java +++ b/backend/src/main/java/de/svencarstensen/muh/security/SecurityConfig.java @@ -25,6 +25,7 @@ public class SecurityConfig { .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authorize -> authorize .requestMatchers(HttpMethod.POST, "/api/session/password-login", "/api/session/register").permitAll() + .requestMatchers("/api/catalog/**").hasRole("CUSTOMER") .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/**").authenticated() .anyRequest().permitAll() 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 e8b1d76..d8720d3 100644 --- a/backend/src/main/java/de/svencarstensen/muh/service/CatalogService.java +++ b/backend/src/main/java/de/svencarstensen/muh/service/CatalogService.java @@ -90,51 +90,75 @@ public class CatalogService { this.authorizationService = authorizationService; } - public ActiveCatalogSummary activeCatalogSummary() { + public ActiveCatalogSummary activeCatalogSummary(String actorId) { + AppUser actor = requireActiveActor(actorId, "Nicht berechtigt"); return new ActiveCatalogSummary( - farmerRepository.findByActiveTrueOrderByNameAsc().stream().map(this::toFarmerOption).toList(), - medicationRepository.findByActiveTrueOrderByNameAsc().stream().map(this::toMedicationOption).toList(), - pathogenRepository.findByActiveTrueOrderByNameAsc().stream().map(this::toPathogenOption).toList(), - antibioticRepository.findByActiveTrueOrderByNameAsc().stream().map(this::toAntibioticOption).toList(), + listActiveFarmersForActor(actor).stream().map(this::toFarmerOption).toList(), + listActiveMedicationsForActor(actor).stream().map(this::toMedicationOption).toList(), + listActivePathogensForActor(actor).stream().map(this::toPathogenOption).toList(), + listActiveAntibioticsForActor(actor).stream().map(this::toAntibioticOption).toList(), List.of() ); } public AdministrationOverview administrationOverview(String actorId) { - authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); - return new AdministrationOverview(listFarmerRows(), listMedicationRows(), listPathogenRows(), listAntibioticRows()); + AppUser actor = requireActiveActor(actorId, "Nicht berechtigt"); + return new AdministrationOverview( + listFarmerRowsForActor(actor), + listMedicationRowsForActor(actor), + listPathogenRowsForActor(actor), + listAntibioticRowsForActor(actor) + ); } - public List listFarmerRows() { - return farmerRepository.findAll().stream() + // Hilfsmethoden für Datenzugriff (immer nur eigene Daten des Hauptbenutzers) + private List listActiveFarmersForActor(AppUser actor) { + return farmerRepository.findByAccountIdAndActiveTrueOrderByNameAsc(resolveAccountId(actor)); + } + + private List listActiveMedicationsForActor(AppUser actor) { + return medicationRepository.findByAccountIdAndActiveTrueOrderByNameAsc(resolveAccountId(actor)); + } + + private List listActivePathogensForActor(AppUser actor) { + return pathogenRepository.findByAccountIdAndActiveTrueOrderByNameAsc(resolveAccountId(actor)); + } + + private List listActiveAntibioticsForActor(AppUser actor) { + return antibioticRepository.findByAccountIdAndActiveTrueOrderByNameAsc(resolveAccountId(actor)); + } + + private List listFarmerRowsForActor(AppUser actor) { + return farmerRepository.findByAccountIdOrderByNameAsc(resolveAccountId(actor)).stream() .map(this::toFarmerRow) .sorted(FARMER_ROW_COMPARATOR) .toList(); } - public List listMedicationRows() { - return medicationRepository.findAll().stream() + private List listMedicationRowsForActor(AppUser actor) { + return medicationRepository.findByAccountIdOrderByNameAsc(resolveAccountId(actor)).stream() .map(this::toMedicationRow) .sorted(MEDICATION_ROW_COMPARATOR) .toList(); } - public List listPathogenRows() { - return pathogenRepository.findAll().stream() + private List listPathogenRowsForActor(AppUser actor) { + return pathogenRepository.findByAccountIdOrderByNameAsc(resolveAccountId(actor)).stream() .map(this::toPathogenRow) .sorted(PATHOGEN_ROW_COMPARATOR) .toList(); } - public List listAntibioticRows() { - return antibioticRepository.findAll().stream() + private List listAntibioticRowsForActor(AppUser actor) { + return antibioticRepository.findByAccountIdOrderByNameAsc(resolveAccountId(actor)).stream() .map(this::toAntibioticRow) .sorted(ANTIBIOTIC_ROW_COMPARATOR) .toList(); } public List saveFarmers(String actorId, List mutations) { - authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); + AppUser actor = requireActiveActor(actorId, "Nicht berechtigt"); + String accountId = resolveAccountId(actor); for (FarmerMutation mutation : mutations) { if (isBlank(mutation.name())) { continue; @@ -143,6 +167,7 @@ public class CatalogService { if (isBlank(mutation.id())) { farmerRepository.save(new Farmer( null, + accountId, UUID.randomUUID().toString(), mutation.name().trim(), blankToNull(mutation.email()), @@ -156,11 +181,16 @@ public class CatalogService { String mutationId = requireText(mutation.id(), "Landwirt-ID fehlt"); Farmer existing = farmerRepository.findById(mutationId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Landwirt nicht gefunden")); + // Sicherstellen, dass der Benutzer nur seine eigenen Daten bearbeiten kann + if (!accountId.equals(existing.accountId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Nicht berechtigt"); + } boolean changed = !existing.name().equals(mutation.name().trim()) || !safeEquals(existing.email(), blankToNull(mutation.email())); if (changed) { farmerRepository.save(new Farmer( existing.id(), + existing.accountId(), existing.businessKey(), existing.name(), existing.email(), @@ -171,6 +201,7 @@ public class CatalogService { )); farmerRepository.save(new Farmer( null, + existing.accountId(), existing.businessKey(), mutation.name().trim(), blankToNull(mutation.email()), @@ -184,6 +215,7 @@ public class CatalogService { if (existing.active() != mutation.active()) { farmerRepository.save(new Farmer( existing.id(), + existing.accountId(), existing.businessKey(), existing.name(), existing.email(), @@ -194,11 +226,12 @@ public class CatalogService { )); } } - return listFarmerRows(); + return listFarmerRowsForActor(actor); } public List saveMedications(String actorId, List mutations) { - authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); + AppUser actor = requireActiveActor(actorId, "Nicht berechtigt"); + String accountId = resolveAccountId(actor); for (MedicationMutation mutation : mutations) { if (isBlank(mutation.name()) || mutation.category() == null) { continue; @@ -207,6 +240,7 @@ public class CatalogService { if (isBlank(mutation.id())) { medicationRepository.save(new MedicationCatalogItem( null, + accountId, UUID.randomUUID().toString(), mutation.name().trim(), mutation.category(), @@ -220,11 +254,16 @@ public class CatalogService { String mutationId = requireText(mutation.id(), "Medikament-ID fehlt"); MedicationCatalogItem existing = medicationRepository.findById(mutationId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Medikament nicht gefunden")); + // Sicherstellen, dass der Benutzer nur seine eigenen Daten bearbeiten kann + if (!accountId.equals(existing.accountId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Nicht berechtigt"); + } boolean changed = !existing.name().equals(mutation.name().trim()) || existing.category() != mutation.category(); if (changed) { medicationRepository.save(new MedicationCatalogItem( existing.id(), + existing.accountId(), existing.businessKey(), existing.name(), existing.category(), @@ -235,6 +274,7 @@ public class CatalogService { )); medicationRepository.save(new MedicationCatalogItem( null, + existing.accountId(), existing.businessKey(), mutation.name().trim(), mutation.category(), @@ -248,6 +288,7 @@ public class CatalogService { if (existing.active() != mutation.active()) { medicationRepository.save(new MedicationCatalogItem( existing.id(), + existing.accountId(), existing.businessKey(), existing.name(), existing.category(), @@ -258,11 +299,12 @@ public class CatalogService { )); } } - return listMedicationRows(); + return listMedicationRowsForActor(actor); } public List savePathogens(String actorId, List mutations) { - authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); + AppUser actor = requireActiveActor(actorId, "Nicht berechtigt"); + String accountId = resolveAccountId(actor); for (PathogenMutation mutation : mutations) { if (isBlank(mutation.name()) || mutation.kind() == null) { continue; @@ -271,6 +313,7 @@ public class CatalogService { if (isBlank(mutation.id())) { pathogenRepository.save(new PathogenCatalogItem( null, + accountId, UUID.randomUUID().toString(), blankToNull(mutation.code()), mutation.name().trim(), @@ -285,12 +328,17 @@ public class CatalogService { String mutationId = requireText(mutation.id(), "Erreger-ID fehlt"); PathogenCatalogItem existing = pathogenRepository.findById(mutationId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Erreger nicht gefunden")); + // Sicherstellen, dass der Benutzer nur seine eigenen Daten bearbeiten kann + if (!accountId.equals(existing.accountId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Nicht berechtigt"); + } boolean changed = !existing.name().equals(mutation.name().trim()) || !safeEquals(existing.code(), blankToNull(mutation.code())) || existing.kind() != mutation.kind(); if (changed) { pathogenRepository.save(new PathogenCatalogItem( existing.id(), + existing.accountId(), existing.businessKey(), existing.code(), existing.name(), @@ -302,6 +350,7 @@ public class CatalogService { )); pathogenRepository.save(new PathogenCatalogItem( null, + existing.accountId(), existing.businessKey(), blankToNull(mutation.code()), mutation.name().trim(), @@ -316,6 +365,7 @@ public class CatalogService { if (existing.active() != mutation.active()) { pathogenRepository.save(new PathogenCatalogItem( existing.id(), + existing.accountId(), existing.businessKey(), existing.code(), existing.name(), @@ -327,11 +377,12 @@ public class CatalogService { )); } } - return listPathogenRows(); + return listPathogenRowsForActor(actor); } public List saveAntibiotics(String actorId, List mutations) { - authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); + AppUser actor = requireActiveActor(actorId, "Nicht berechtigt"); + String accountId = resolveAccountId(actor); for (AntibioticMutation mutation : mutations) { if (isBlank(mutation.name())) { continue; @@ -340,6 +391,7 @@ public class CatalogService { if (isBlank(mutation.id())) { antibioticRepository.save(new AntibioticCatalogItem( null, + accountId, UUID.randomUUID().toString(), blankToNull(mutation.code()), mutation.name().trim(), @@ -353,11 +405,16 @@ public class CatalogService { String mutationId = requireText(mutation.id(), "Antibiotika-ID fehlt"); AntibioticCatalogItem existing = antibioticRepository.findById(mutationId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Antibiotikum nicht gefunden")); + // Sicherstellen, dass der Benutzer nur seine eigenen Daten bearbeiten kann + if (!accountId.equals(existing.accountId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Nicht berechtigt"); + } boolean changed = !existing.name().equals(mutation.name().trim()) || !safeEquals(existing.code(), blankToNull(mutation.code())); if (changed) { antibioticRepository.save(new AntibioticCatalogItem( existing.id(), + existing.accountId(), existing.businessKey(), existing.code(), existing.name(), @@ -368,6 +425,7 @@ public class CatalogService { )); antibioticRepository.save(new AntibioticCatalogItem( null, + existing.accountId(), existing.businessKey(), blankToNull(mutation.code()), mutation.name().trim(), @@ -381,6 +439,7 @@ public class CatalogService { if (existing.active() != mutation.active()) { antibioticRepository.save(new AntibioticCatalogItem( existing.id(), + existing.accountId(), existing.businessKey(), existing.code(), existing.name(), @@ -391,7 +450,7 @@ public class CatalogService { )); } } - return listAntibioticRows(); + return listAntibioticRowsForActor(actor); } public List listUsers(String actorId) { @@ -653,28 +712,33 @@ public class CatalogService { backfillDefaultUserEmails(); removeLegacyPortalLoginField(); migrateCustomerNumbers(); + migrateCatalogAccountIds(); ensureDefaultUser("Administrator", "admin@muh.local", "Admin123!", UserRole.ADMIN); } - public Farmer requireActiveFarmer(String businessKey) { - return farmerRepository.findByActiveTrueOrderByNameAsc().stream() + public Farmer requireActiveFarmer(String actorId, String businessKey) { + AppUser actor = requireActiveActor(actorId, "Nicht berechtigt"); + return listActiveFarmersForActor(actor).stream() .filter(farmer -> farmer.businessKey().equals(businessKey)) .findFirst() .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden")); } - public Map activePathogensByBusinessKey() { - return pathogenRepository.findByActiveTrueOrderByNameAsc().stream() + public Map activePathogensByBusinessKey(String actorId) { + AppUser actor = requireActiveActor(actorId, "Nicht berechtigt"); + return listActivePathogensForActor(actor).stream() .collect(Collectors.toMap(PathogenCatalogItem::businessKey, Function.identity())); } - public Map activeAntibioticsByBusinessKey() { - return antibioticRepository.findByActiveTrueOrderByNameAsc().stream() + public Map activeAntibioticsByBusinessKey(String actorId) { + AppUser actor = requireActiveActor(actorId, "Nicht berechtigt"); + return listActiveAntibioticsForActor(actor).stream() .collect(Collectors.toMap(AntibioticCatalogItem::businessKey, Function.identity())); } - public Map activeMedicationsByBusinessKey() { - return medicationRepository.findByActiveTrueOrderByNameAsc().stream() + public Map activeMedicationsByBusinessKey(String actorId) { + AppUser actor = requireActiveActor(actorId, "Nicht berechtigt"); + return listActiveMedicationsForActor(actor).stream() .collect(Collectors.toMap(MedicationCatalogItem::businessKey, Function.identity())); } @@ -999,6 +1063,82 @@ public class CatalogService { backfillDefaultUserEmail("admin", "admin@muh.local"); } + private void migrateCatalogAccountIds() { + // Finde den ersten Admin-Benutzer als Fallback + String defaultAccountId = appUserRepository.findAll().stream() + .filter(user -> user.role() == UserRole.ADMIN) + .findFirst() + .map(AppUser::id) + .orElse(null); + + if (defaultAccountId == null) { + return; + } + + LocalDateTime now = LocalDateTime.now(); + + // Migriere Farmers ohne accountId + farmerRepository.findAll().stream() + .filter(farmer -> isBlank(farmer.accountId())) + .forEach(farmer -> farmerRepository.save(new Farmer( + farmer.id(), + defaultAccountId, + farmer.businessKey(), + farmer.name(), + farmer.email(), + farmer.active(), + farmer.supersedesId(), + farmer.createdAt(), + now + ))); + + // Migriere Medications ohne accountId + medicationRepository.findAll().stream() + .filter(med -> isBlank(med.accountId())) + .forEach(med -> medicationRepository.save(new MedicationCatalogItem( + med.id(), + defaultAccountId, + med.businessKey(), + med.name(), + med.category(), + med.active(), + med.supersedesId(), + med.createdAt(), + now + ))); + + // Migriere Pathogens ohne accountId + pathogenRepository.findAll().stream() + .filter(pathogen -> isBlank(pathogen.accountId())) + .forEach(pathogen -> pathogenRepository.save(new PathogenCatalogItem( + pathogen.id(), + defaultAccountId, + pathogen.businessKey(), + pathogen.code(), + pathogen.name(), + pathogen.kind(), + pathogen.active(), + pathogen.supersedesId(), + pathogen.createdAt(), + now + ))); + + // Migriere Antibiotics ohne accountId + antibioticRepository.findAll().stream() + .filter(antibiotic -> isBlank(antibiotic.accountId())) + .forEach(antibiotic -> antibioticRepository.save(new AntibioticCatalogItem( + antibiotic.id(), + defaultAccountId, + antibiotic.businessKey(), + antibiotic.code(), + antibiotic.name(), + antibiotic.active(), + antibiotic.supersedesId(), + antibiotic.createdAt(), + now + ))); + } + private void backfillDefaultUserEmail(String legacyPortalLogin, String email) { mongoTemplate.updateMulti( new Query(new Criteria().andOperator( diff --git a/backend/src/main/java/de/svencarstensen/muh/service/DefaultUserInitializer.java b/backend/src/main/java/de/svencarstensen/muh/service/DefaultUserInitializer.java new file mode 100644 index 0000000..bbbb791 --- /dev/null +++ b/backend/src/main/java/de/svencarstensen/muh/service/DefaultUserInitializer.java @@ -0,0 +1,20 @@ +package de.svencarstensen.muh.service; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +@Component +public class DefaultUserInitializer implements ApplicationRunner { + + private final CatalogService catalogService; + + public DefaultUserInitializer(CatalogService catalogService) { + this.catalogService = catalogService; + } + + @Override + public void run(ApplicationArguments args) { + catalogService.ensureDefaultUsers(); + } +} diff --git a/backend/src/main/java/de/svencarstensen/muh/service/DemoDataInitializer.java b/backend/src/main/java/de/svencarstensen/muh/service/DemoDataInitializer.java deleted file mode 100644 index c446b62..0000000 --- a/backend/src/main/java/de/svencarstensen/muh/service/DemoDataInitializer.java +++ /dev/null @@ -1,76 +0,0 @@ -package de.svencarstensen.muh.service; - -import de.svencarstensen.muh.domain.AntibioticCatalogItem; -import de.svencarstensen.muh.domain.Farmer; -import de.svencarstensen.muh.domain.MedicationCatalogItem; -import de.svencarstensen.muh.domain.MedicationCategory; -import de.svencarstensen.muh.domain.PathogenCatalogItem; -import de.svencarstensen.muh.domain.PathogenKind; -import de.svencarstensen.muh.repository.AntibioticCatalogRepository; -import de.svencarstensen.muh.repository.FarmerRepository; -import de.svencarstensen.muh.repository.MedicationCatalogRepository; -import de.svencarstensen.muh.repository.PathogenCatalogRepository; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.stereotype.Component; - -import java.time.LocalDateTime; -import java.util.UUID; - -@Component -public class DemoDataInitializer implements ApplicationRunner { - - private final FarmerRepository farmerRepository; - private final MedicationCatalogRepository medicationRepository; - private final PathogenCatalogRepository pathogenRepository; - private final AntibioticCatalogRepository antibioticRepository; - private final CatalogService catalogService; - - public DemoDataInitializer( - FarmerRepository farmerRepository, - MedicationCatalogRepository medicationRepository, - PathogenCatalogRepository pathogenRepository, - AntibioticCatalogRepository antibioticRepository, - CatalogService catalogService - ) { - this.farmerRepository = farmerRepository; - this.medicationRepository = medicationRepository; - this.pathogenRepository = pathogenRepository; - this.antibioticRepository = antibioticRepository; - this.catalogService = catalogService; - } - - @Override - public void run(ApplicationArguments args) { - LocalDateTime now = LocalDateTime.now(); - - if (farmerRepository.count() == 0) { - farmerRepository.save(new Farmer(null, UUID.randomUUID().toString(), "Hof Hansen", "hansen@example.com", true, null, now, now)); - farmerRepository.save(new Farmer(null, UUID.randomUUID().toString(), "Agrar Lindenblick", "lindenblick@example.com", true, null, now, now)); - farmerRepository.save(new Farmer(null, UUID.randomUUID().toString(), "Gut Westerkamp", "westerkamp@example.com", true, null, now, now)); - } - - if (medicationRepository.count() == 0) { - medicationRepository.save(new MedicationCatalogItem(null, UUID.randomUUID().toString(), "Mastijet", MedicationCategory.IN_UDDER, true, null, now, now)); - medicationRepository.save(new MedicationCatalogItem(null, UUID.randomUUID().toString(), "Metacam", MedicationCategory.SYSTEMIC_PAIN, true, null, now, now)); - medicationRepository.save(new MedicationCatalogItem(null, UUID.randomUUID().toString(), "Cobactan", MedicationCategory.SYSTEMIC_ANTIBIOTIC, true, null, now, now)); - medicationRepository.save(new MedicationCatalogItem(null, UUID.randomUUID().toString(), "Orbeseal", MedicationCategory.DRY_SEALER, true, null, now, now)); - medicationRepository.save(new MedicationCatalogItem(null, UUID.randomUUID().toString(), "Nafpenzal", MedicationCategory.DRY_ANTIBIOTIC, true, null, now, now)); - } - - if (pathogenRepository.count() == 0) { - pathogenRepository.save(new PathogenCatalogItem(null, UUID.randomUUID().toString(), "SAU", "Staph. aureus", PathogenKind.BACTERIAL, true, null, now, now)); - pathogenRepository.save(new PathogenCatalogItem(null, UUID.randomUUID().toString(), "ECO", "E. coli", PathogenKind.BACTERIAL, true, null, now, now)); - pathogenRepository.save(new PathogenCatalogItem(null, UUID.randomUUID().toString(), "NG", "Kein Wachstum", PathogenKind.NO_GROWTH, true, null, now, now)); - pathogenRepository.save(new PathogenCatalogItem(null, UUID.randomUUID().toString(), "VER", "Verunreinigt", PathogenKind.CONTAMINATED, true, null, now, now)); - } - - if (antibioticRepository.count() == 0) { - antibioticRepository.save(new AntibioticCatalogItem(null, UUID.randomUUID().toString(), "PEN", "Penicillin", true, null, now, now)); - antibioticRepository.save(new AntibioticCatalogItem(null, UUID.randomUUID().toString(), "CEF", "Cefalexin", true, null, now, now)); - antibioticRepository.save(new AntibioticCatalogItem(null, UUID.randomUUID().toString(), "ENR", "Enrofloxacin", true, null, now, now)); - } - - catalogService.ensureDefaultUsers(); - } -} 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 19f08b9..31b6108 100644 --- a/backend/src/main/java/de/svencarstensen/muh/service/PortalService.java +++ b/backend/src/main/java/de/svencarstensen/muh/service/PortalService.java @@ -30,7 +30,7 @@ public class PortalService { Long sampleNumber, LocalDate date ) { - List matchingFarmers = catalogService.activeCatalogSummary().farmers().stream() + List matchingFarmers = catalogService.activeCatalogSummary(actorId).farmers().stream() .filter(farmer -> farmerQuery == null || farmerQuery.isBlank() || farmer.name().toLowerCase(Locale.ROOT).contains(farmerQuery.toLowerCase(Locale.ROOT))) .toList(); 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 77da158..1764283 100644 --- a/backend/src/main/java/de/svencarstensen/muh/service/SampleService.java +++ b/backend/src/main/java/de/svencarstensen/muh/service/SampleService.java @@ -102,7 +102,7 @@ public class SampleService { public SampleDetail createSample(String actorId, RegistrationRequest request) { AppUser actor = requireActor(actorId); LocalDateTime now = LocalDateTime.now(); - CatalogService.FarmerOption farmer = catalogService.activeCatalogSummary().farmers().stream() + CatalogService.FarmerOption farmer = catalogService.activeCatalogSummary(actorId).farmers().stream() .filter(candidate -> candidate.businessKey().equals(request.farmerBusinessKey())) .findFirst() .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden")); @@ -157,7 +157,7 @@ public class SampleService { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Stammdaten können nicht mehr geändert werden"); } - CatalogService.FarmerOption farmer = catalogService.activeCatalogSummary().farmers().stream() + CatalogService.FarmerOption farmer = catalogService.activeCatalogSummary(actorId).farmers().stream() .filter(candidate -> candidate.businessKey().equals(request.farmerBusinessKey())) .findFirst() .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden")); @@ -220,7 +220,7 @@ public class SampleService { current.put(quarter.quarterKey(), quarter); } - Map pathogens = catalogService.activePathogensByBusinessKey(); + Map pathogens = catalogService.activePathogensByBusinessKey(actorId); List updatedQuarters = new ArrayList<>(); for (AnamnesisQuarterRequest quarterRequest : request.quarters()) { QuarterFinding base = current.get(quarterRequest.quarterKey()); @@ -284,7 +284,7 @@ public class SampleService { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Antibiogramm kann nicht mehr geändert werden"); } - Map antibiotics = catalogService.activeAntibioticsByBusinessKey(); + Map antibiotics = catalogService.activeAntibioticsByBusinessKey(actorId); Map groups = new HashMap<>(); Map quartersByKey = existing.quarters().stream() .collect(java.util.stream.Collectors.toMap(QuarterFinding::quarterKey, quarter -> quarter)); @@ -435,7 +435,7 @@ public class SampleService { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Therapie kann nicht bearbeitet werden"); } - Map medications = catalogService.activeMedicationsByBusinessKey(); + Map medications = catalogService.activeMedicationsByBusinessKey(actorId); TherapyRecommendation therapy = new TherapyRecommendation( request.continueStarted(), request.switchTherapy(), 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 6b2251b..98d72d2 100644 --- a/backend/src/main/java/de/svencarstensen/muh/web/CatalogController.java +++ b/backend/src/main/java/de/svencarstensen/muh/web/CatalogController.java @@ -26,30 +26,57 @@ public class CatalogController { @GetMapping("/catalogs/summary") public CatalogService.ActiveCatalogSummary catalogSummary() { - return catalogService.activeCatalogSummary(); + return catalogService.activeCatalogSummary(securitySupport.currentUser().id()); } + // Legacy admin endpoints - ADMIN only @GetMapping("/admin") public CatalogService.AdministrationOverview administrationOverview() { return catalogService.administrationOverview(securitySupport.currentUser().id()); } @PostMapping("/admin/farmers") - public List saveFarmers(@RequestBody List mutations) { + public List saveFarmersAdmin(@RequestBody List mutations) { return catalogService.saveFarmers(securitySupport.currentUser().id(), mutations); } @PostMapping("/admin/medications") - public List saveMedications(@RequestBody List mutations) { + public List saveMedicationsAdmin(@RequestBody List mutations) { return catalogService.saveMedications(securitySupport.currentUser().id(), mutations); } @PostMapping("/admin/pathogens") - public List savePathogens(@RequestBody List mutations) { + public List savePathogensAdmin(@RequestBody List mutations) { return catalogService.savePathogens(securitySupport.currentUser().id(), mutations); } @PostMapping("/admin/antibiotics") + public List saveAntibioticsAdmin(@RequestBody List mutations) { + return catalogService.saveAntibiotics(securitySupport.currentUser().id(), mutations); + } + + // New catalog endpoints - ADMIN and CUSTOMER + @GetMapping("/catalog/overview") + public CatalogService.AdministrationOverview catalogOverview() { + return catalogService.administrationOverview(securitySupport.currentUser().id()); + } + + @PostMapping("/catalog/farmers") + public List saveFarmers(@RequestBody List mutations) { + return catalogService.saveFarmers(securitySupport.currentUser().id(), mutations); + } + + @PostMapping("/catalog/medications") + public List saveMedications(@RequestBody List mutations) { + return catalogService.saveMedications(securitySupport.currentUser().id(), mutations); + } + + @PostMapping("/catalog/pathogens") + public List savePathogens(@RequestBody List mutations) { + return catalogService.savePathogens(securitySupport.currentUser().id(), mutations); + } + + @PostMapping("/catalog/antibiotics") public List saveAntibiotics(@RequestBody List mutations) { return catalogService.saveAntibiotics(securitySupport.currentUser().id(), mutations); } diff --git a/frontend/src/lib/version.ts b/frontend/src/lib/version.ts index 734967c..d229e53 100644 --- a/frontend/src/lib/version.ts +++ b/frontend/src/lib/version.ts @@ -6,7 +6,7 @@ * - MINOR: New functionality (backward compatible) * - PATCH: Bug fixes (backward compatible) */ -export const APP_VERSION = "0.8.0"; +export const APP_VERSION = "0.9.2"; /** * Build date - set at build time diff --git a/frontend/src/pages/AdministrationPage.tsx b/frontend/src/pages/AdministrationPage.tsx index 8ad100d..353913b 100644 --- a/frontend/src/pages/AdministrationPage.tsx +++ b/frontend/src/pages/AdministrationPage.tsx @@ -122,7 +122,7 @@ export default function AdministrationPage() { useEffect(() => { async function load() { try { - const response = await apiGet("/admin"); + const response = await apiGet("/catalog/overview"); setDatasets(normalizeOverview(response)); } catch (loadError) { setMessage((loadError as Error).message); @@ -176,7 +176,7 @@ export default function AdministrationPage() { let response: EditableRow[]; switch (selectedDataset) { case "farmers": - response = await apiPost("/admin/farmers", rows.map((row) => ({ + response = await apiPost("/catalog/farmers", rows.map((row) => ({ id: row.id || null, name: row.name, email: row.email || null, @@ -184,7 +184,7 @@ export default function AdministrationPage() { }))); break; case "medications": - response = await apiPost("/admin/medications", rows.map((row) => ({ + response = await apiPost("/catalog/medications", rows.map((row) => ({ id: row.id || null, name: row.name, category: row.category, @@ -192,7 +192,7 @@ export default function AdministrationPage() { }))); break; case "pathogens": - response = await apiPost("/admin/pathogens", rows.map((row) => ({ + response = await apiPost("/catalog/pathogens", rows.map((row) => ({ id: row.id || null, code: row.code || null, name: row.name, @@ -201,7 +201,7 @@ export default function AdministrationPage() { }))); break; case "antibiotics": - response = await apiPost("/admin/antibiotics", rows.map((row) => ({ + response = await apiPost("/catalog/antibiotics", rows.map((row) => ({ id: row.id || null, code: row.code || null, name: row.name,