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
This commit is contained in:
2026-03-18 20:40:10 +01:00
parent 09e6d07c2d
commit f9b83a166d
18 changed files with 256 additions and 123 deletions

View File

@@ -107,6 +107,7 @@ Kundenregistrierung:
- `cd frontend && npm run build` - `cd frontend && npm run build`
## Docker ## 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 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 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

View File

@@ -8,6 +8,7 @@ import java.time.LocalDateTime;
@Document("antibiotics") @Document("antibiotics")
public record AntibioticCatalogItem( public record AntibioticCatalogItem(
@Id String id, @Id String id,
String accountId,
String businessKey, String businessKey,
String code, String code,
String name, String name,

View File

@@ -8,6 +8,7 @@ import java.time.LocalDateTime;
@Document("farmers") @Document("farmers")
public record Farmer( public record Farmer(
@Id String id, @Id String id,
String accountId,
String businessKey, String businessKey,
String name, String name,
String email, String email,

View File

@@ -8,6 +8,7 @@ import java.time.LocalDateTime;
@Document("medications") @Document("medications")
public record MedicationCatalogItem( public record MedicationCatalogItem(
@Id String id, @Id String id,
String accountId,
String businessKey, String businessKey,
String name, String name,
MedicationCategory category, MedicationCategory category,

View File

@@ -8,6 +8,7 @@ import java.time.LocalDateTime;
@Document("pathogens") @Document("pathogens")
public record PathogenCatalogItem( public record PathogenCatalogItem(
@Id String id, @Id String id,
String accountId,
String businessKey, String businessKey,
String code, String code,
String name, String name,

View File

@@ -7,4 +7,8 @@ import java.util.List;
public interface AntibioticCatalogRepository extends MongoRepository<AntibioticCatalogItem, String> { public interface AntibioticCatalogRepository extends MongoRepository<AntibioticCatalogItem, String> {
List<AntibioticCatalogItem> findByActiveTrueOrderByNameAsc(); List<AntibioticCatalogItem> findByActiveTrueOrderByNameAsc();
List<AntibioticCatalogItem> findByAccountIdOrderByNameAsc(String accountId);
List<AntibioticCatalogItem> findByAccountIdAndActiveTrueOrderByNameAsc(String accountId);
} }

View File

@@ -8,5 +8,9 @@ import java.util.List;
public interface FarmerRepository extends MongoRepository<Farmer, String> { public interface FarmerRepository extends MongoRepository<Farmer, String> {
List<Farmer> findByActiveTrueOrderByNameAsc(); List<Farmer> findByActiveTrueOrderByNameAsc();
List<Farmer> findByAccountIdOrderByNameAsc(String accountId);
List<Farmer> findByAccountIdAndActiveTrueOrderByNameAsc(String accountId);
List<Farmer> findByNameContainingIgnoreCaseOrderByNameAsc(String name); List<Farmer> findByNameContainingIgnoreCaseOrderByNameAsc(String name);
} }

View File

@@ -7,4 +7,8 @@ import java.util.List;
public interface MedicationCatalogRepository extends MongoRepository<MedicationCatalogItem, String> { public interface MedicationCatalogRepository extends MongoRepository<MedicationCatalogItem, String> {
List<MedicationCatalogItem> findByActiveTrueOrderByNameAsc(); List<MedicationCatalogItem> findByActiveTrueOrderByNameAsc();
List<MedicationCatalogItem> findByAccountIdOrderByNameAsc(String accountId);
List<MedicationCatalogItem> findByAccountIdAndActiveTrueOrderByNameAsc(String accountId);
} }

View File

@@ -7,4 +7,8 @@ import java.util.List;
public interface PathogenCatalogRepository extends MongoRepository<PathogenCatalogItem, String> { public interface PathogenCatalogRepository extends MongoRepository<PathogenCatalogItem, String> {
List<PathogenCatalogItem> findByActiveTrueOrderByNameAsc(); List<PathogenCatalogItem> findByActiveTrueOrderByNameAsc();
List<PathogenCatalogItem> findByAccountIdOrderByNameAsc(String accountId);
List<PathogenCatalogItem> findByAccountIdAndActiveTrueOrderByNameAsc(String accountId);
} }

View File

@@ -25,6 +25,7 @@ public class SecurityConfig {
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize .authorizeHttpRequests(authorize -> authorize
.requestMatchers(HttpMethod.POST, "/api/session/password-login", "/api/session/register").permitAll() .requestMatchers(HttpMethod.POST, "/api/session/password-login", "/api/session/register").permitAll()
.requestMatchers("/api/catalog/**").hasRole("CUSTOMER")
.requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").authenticated() .requestMatchers("/api/**").authenticated()
.anyRequest().permitAll() .anyRequest().permitAll()

View File

@@ -90,51 +90,75 @@ public class CatalogService {
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
} }
public ActiveCatalogSummary activeCatalogSummary() { public ActiveCatalogSummary activeCatalogSummary(String actorId) {
AppUser actor = requireActiveActor(actorId, "Nicht berechtigt");
return new ActiveCatalogSummary( return new ActiveCatalogSummary(
farmerRepository.findByActiveTrueOrderByNameAsc().stream().map(this::toFarmerOption).toList(), listActiveFarmersForActor(actor).stream().map(this::toFarmerOption).toList(),
medicationRepository.findByActiveTrueOrderByNameAsc().stream().map(this::toMedicationOption).toList(), listActiveMedicationsForActor(actor).stream().map(this::toMedicationOption).toList(),
pathogenRepository.findByActiveTrueOrderByNameAsc().stream().map(this::toPathogenOption).toList(), listActivePathogensForActor(actor).stream().map(this::toPathogenOption).toList(),
antibioticRepository.findByActiveTrueOrderByNameAsc().stream().map(this::toAntibioticOption).toList(), listActiveAntibioticsForActor(actor).stream().map(this::toAntibioticOption).toList(),
List.of() List.of()
); );
} }
public AdministrationOverview administrationOverview(String actorId) { public AdministrationOverview administrationOverview(String actorId) {
authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); AppUser actor = requireActiveActor(actorId, "Nicht berechtigt");
return new AdministrationOverview(listFarmerRows(), listMedicationRows(), listPathogenRows(), listAntibioticRows()); return new AdministrationOverview(
listFarmerRowsForActor(actor),
listMedicationRowsForActor(actor),
listPathogenRowsForActor(actor),
listAntibioticRowsForActor(actor)
);
} }
public List<FarmerRow> listFarmerRows() { // Hilfsmethoden für Datenzugriff (immer nur eigene Daten des Hauptbenutzers)
return farmerRepository.findAll().stream() private List<Farmer> listActiveFarmersForActor(AppUser actor) {
return farmerRepository.findByAccountIdAndActiveTrueOrderByNameAsc(resolveAccountId(actor));
}
private List<MedicationCatalogItem> listActiveMedicationsForActor(AppUser actor) {
return medicationRepository.findByAccountIdAndActiveTrueOrderByNameAsc(resolveAccountId(actor));
}
private List<PathogenCatalogItem> listActivePathogensForActor(AppUser actor) {
return pathogenRepository.findByAccountIdAndActiveTrueOrderByNameAsc(resolveAccountId(actor));
}
private List<AntibioticCatalogItem> listActiveAntibioticsForActor(AppUser actor) {
return antibioticRepository.findByAccountIdAndActiveTrueOrderByNameAsc(resolveAccountId(actor));
}
private List<FarmerRow> listFarmerRowsForActor(AppUser actor) {
return farmerRepository.findByAccountIdOrderByNameAsc(resolveAccountId(actor)).stream()
.map(this::toFarmerRow) .map(this::toFarmerRow)
.sorted(FARMER_ROW_COMPARATOR) .sorted(FARMER_ROW_COMPARATOR)
.toList(); .toList();
} }
public List<MedicationRow> listMedicationRows() { private List<MedicationRow> listMedicationRowsForActor(AppUser actor) {
return medicationRepository.findAll().stream() return medicationRepository.findByAccountIdOrderByNameAsc(resolveAccountId(actor)).stream()
.map(this::toMedicationRow) .map(this::toMedicationRow)
.sorted(MEDICATION_ROW_COMPARATOR) .sorted(MEDICATION_ROW_COMPARATOR)
.toList(); .toList();
} }
public List<PathogenRow> listPathogenRows() { private List<PathogenRow> listPathogenRowsForActor(AppUser actor) {
return pathogenRepository.findAll().stream() return pathogenRepository.findByAccountIdOrderByNameAsc(resolveAccountId(actor)).stream()
.map(this::toPathogenRow) .map(this::toPathogenRow)
.sorted(PATHOGEN_ROW_COMPARATOR) .sorted(PATHOGEN_ROW_COMPARATOR)
.toList(); .toList();
} }
public List<AntibioticRow> listAntibioticRows() { private List<AntibioticRow> listAntibioticRowsForActor(AppUser actor) {
return antibioticRepository.findAll().stream() return antibioticRepository.findByAccountIdOrderByNameAsc(resolveAccountId(actor)).stream()
.map(this::toAntibioticRow) .map(this::toAntibioticRow)
.sorted(ANTIBIOTIC_ROW_COMPARATOR) .sorted(ANTIBIOTIC_ROW_COMPARATOR)
.toList(); .toList();
} }
public List<FarmerRow> saveFarmers(String actorId, List<FarmerMutation> mutations) { public List<FarmerRow> saveFarmers(String actorId, List<FarmerMutation> mutations) {
authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); AppUser actor = requireActiveActor(actorId, "Nicht berechtigt");
String accountId = resolveAccountId(actor);
for (FarmerMutation mutation : mutations) { for (FarmerMutation mutation : mutations) {
if (isBlank(mutation.name())) { if (isBlank(mutation.name())) {
continue; continue;
@@ -143,6 +167,7 @@ public class CatalogService {
if (isBlank(mutation.id())) { if (isBlank(mutation.id())) {
farmerRepository.save(new Farmer( farmerRepository.save(new Farmer(
null, null,
accountId,
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
mutation.name().trim(), mutation.name().trim(),
blankToNull(mutation.email()), blankToNull(mutation.email()),
@@ -156,11 +181,16 @@ public class CatalogService {
String mutationId = requireText(mutation.id(), "Landwirt-ID fehlt"); String mutationId = requireText(mutation.id(), "Landwirt-ID fehlt");
Farmer existing = farmerRepository.findById(mutationId) Farmer existing = farmerRepository.findById(mutationId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Landwirt nicht gefunden")); .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()) boolean changed = !existing.name().equals(mutation.name().trim())
|| !safeEquals(existing.email(), blankToNull(mutation.email())); || !safeEquals(existing.email(), blankToNull(mutation.email()));
if (changed) { if (changed) {
farmerRepository.save(new Farmer( farmerRepository.save(new Farmer(
existing.id(), existing.id(),
existing.accountId(),
existing.businessKey(), existing.businessKey(),
existing.name(), existing.name(),
existing.email(), existing.email(),
@@ -171,6 +201,7 @@ public class CatalogService {
)); ));
farmerRepository.save(new Farmer( farmerRepository.save(new Farmer(
null, null,
existing.accountId(),
existing.businessKey(), existing.businessKey(),
mutation.name().trim(), mutation.name().trim(),
blankToNull(mutation.email()), blankToNull(mutation.email()),
@@ -184,6 +215,7 @@ public class CatalogService {
if (existing.active() != mutation.active()) { if (existing.active() != mutation.active()) {
farmerRepository.save(new Farmer( farmerRepository.save(new Farmer(
existing.id(), existing.id(),
existing.accountId(),
existing.businessKey(), existing.businessKey(),
existing.name(), existing.name(),
existing.email(), existing.email(),
@@ -194,11 +226,12 @@ public class CatalogService {
)); ));
} }
} }
return listFarmerRows(); return listFarmerRowsForActor(actor);
} }
public List<MedicationRow> saveMedications(String actorId, List<MedicationMutation> mutations) { public List<MedicationRow> saveMedications(String actorId, List<MedicationMutation> mutations) {
authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); AppUser actor = requireActiveActor(actorId, "Nicht berechtigt");
String accountId = resolveAccountId(actor);
for (MedicationMutation mutation : mutations) { for (MedicationMutation mutation : mutations) {
if (isBlank(mutation.name()) || mutation.category() == null) { if (isBlank(mutation.name()) || mutation.category() == null) {
continue; continue;
@@ -207,6 +240,7 @@ public class CatalogService {
if (isBlank(mutation.id())) { if (isBlank(mutation.id())) {
medicationRepository.save(new MedicationCatalogItem( medicationRepository.save(new MedicationCatalogItem(
null, null,
accountId,
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
mutation.name().trim(), mutation.name().trim(),
mutation.category(), mutation.category(),
@@ -220,11 +254,16 @@ public class CatalogService {
String mutationId = requireText(mutation.id(), "Medikament-ID fehlt"); String mutationId = requireText(mutation.id(), "Medikament-ID fehlt");
MedicationCatalogItem existing = medicationRepository.findById(mutationId) MedicationCatalogItem existing = medicationRepository.findById(mutationId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Medikament nicht gefunden")); .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()) boolean changed = !existing.name().equals(mutation.name().trim())
|| existing.category() != mutation.category(); || existing.category() != mutation.category();
if (changed) { if (changed) {
medicationRepository.save(new MedicationCatalogItem( medicationRepository.save(new MedicationCatalogItem(
existing.id(), existing.id(),
existing.accountId(),
existing.businessKey(), existing.businessKey(),
existing.name(), existing.name(),
existing.category(), existing.category(),
@@ -235,6 +274,7 @@ public class CatalogService {
)); ));
medicationRepository.save(new MedicationCatalogItem( medicationRepository.save(new MedicationCatalogItem(
null, null,
existing.accountId(),
existing.businessKey(), existing.businessKey(),
mutation.name().trim(), mutation.name().trim(),
mutation.category(), mutation.category(),
@@ -248,6 +288,7 @@ public class CatalogService {
if (existing.active() != mutation.active()) { if (existing.active() != mutation.active()) {
medicationRepository.save(new MedicationCatalogItem( medicationRepository.save(new MedicationCatalogItem(
existing.id(), existing.id(),
existing.accountId(),
existing.businessKey(), existing.businessKey(),
existing.name(), existing.name(),
existing.category(), existing.category(),
@@ -258,11 +299,12 @@ public class CatalogService {
)); ));
} }
} }
return listMedicationRows(); return listMedicationRowsForActor(actor);
} }
public List<PathogenRow> savePathogens(String actorId, List<PathogenMutation> mutations) { public List<PathogenRow> savePathogens(String actorId, List<PathogenMutation> mutations) {
authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); AppUser actor = requireActiveActor(actorId, "Nicht berechtigt");
String accountId = resolveAccountId(actor);
for (PathogenMutation mutation : mutations) { for (PathogenMutation mutation : mutations) {
if (isBlank(mutation.name()) || mutation.kind() == null) { if (isBlank(mutation.name()) || mutation.kind() == null) {
continue; continue;
@@ -271,6 +313,7 @@ public class CatalogService {
if (isBlank(mutation.id())) { if (isBlank(mutation.id())) {
pathogenRepository.save(new PathogenCatalogItem( pathogenRepository.save(new PathogenCatalogItem(
null, null,
accountId,
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
blankToNull(mutation.code()), blankToNull(mutation.code()),
mutation.name().trim(), mutation.name().trim(),
@@ -285,12 +328,17 @@ public class CatalogService {
String mutationId = requireText(mutation.id(), "Erreger-ID fehlt"); String mutationId = requireText(mutation.id(), "Erreger-ID fehlt");
PathogenCatalogItem existing = pathogenRepository.findById(mutationId) PathogenCatalogItem existing = pathogenRepository.findById(mutationId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Erreger nicht gefunden")); .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()) boolean changed = !existing.name().equals(mutation.name().trim())
|| !safeEquals(existing.code(), blankToNull(mutation.code())) || !safeEquals(existing.code(), blankToNull(mutation.code()))
|| existing.kind() != mutation.kind(); || existing.kind() != mutation.kind();
if (changed) { if (changed) {
pathogenRepository.save(new PathogenCatalogItem( pathogenRepository.save(new PathogenCatalogItem(
existing.id(), existing.id(),
existing.accountId(),
existing.businessKey(), existing.businessKey(),
existing.code(), existing.code(),
existing.name(), existing.name(),
@@ -302,6 +350,7 @@ public class CatalogService {
)); ));
pathogenRepository.save(new PathogenCatalogItem( pathogenRepository.save(new PathogenCatalogItem(
null, null,
existing.accountId(),
existing.businessKey(), existing.businessKey(),
blankToNull(mutation.code()), blankToNull(mutation.code()),
mutation.name().trim(), mutation.name().trim(),
@@ -316,6 +365,7 @@ public class CatalogService {
if (existing.active() != mutation.active()) { if (existing.active() != mutation.active()) {
pathogenRepository.save(new PathogenCatalogItem( pathogenRepository.save(new PathogenCatalogItem(
existing.id(), existing.id(),
existing.accountId(),
existing.businessKey(), existing.businessKey(),
existing.code(), existing.code(),
existing.name(), existing.name(),
@@ -327,11 +377,12 @@ public class CatalogService {
)); ));
} }
} }
return listPathogenRows(); return listPathogenRowsForActor(actor);
} }
public List<AntibioticRow> saveAntibiotics(String actorId, List<AntibioticMutation> mutations) { public List<AntibioticRow> saveAntibiotics(String actorId, List<AntibioticMutation> mutations) {
authorizationService.requireActiveUser(actorId, "Nicht berechtigt"); AppUser actor = requireActiveActor(actorId, "Nicht berechtigt");
String accountId = resolveAccountId(actor);
for (AntibioticMutation mutation : mutations) { for (AntibioticMutation mutation : mutations) {
if (isBlank(mutation.name())) { if (isBlank(mutation.name())) {
continue; continue;
@@ -340,6 +391,7 @@ public class CatalogService {
if (isBlank(mutation.id())) { if (isBlank(mutation.id())) {
antibioticRepository.save(new AntibioticCatalogItem( antibioticRepository.save(new AntibioticCatalogItem(
null, null,
accountId,
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
blankToNull(mutation.code()), blankToNull(mutation.code()),
mutation.name().trim(), mutation.name().trim(),
@@ -353,11 +405,16 @@ public class CatalogService {
String mutationId = requireText(mutation.id(), "Antibiotika-ID fehlt"); String mutationId = requireText(mutation.id(), "Antibiotika-ID fehlt");
AntibioticCatalogItem existing = antibioticRepository.findById(mutationId) AntibioticCatalogItem existing = antibioticRepository.findById(mutationId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Antibiotikum nicht gefunden")); .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()) boolean changed = !existing.name().equals(mutation.name().trim())
|| !safeEquals(existing.code(), blankToNull(mutation.code())); || !safeEquals(existing.code(), blankToNull(mutation.code()));
if (changed) { if (changed) {
antibioticRepository.save(new AntibioticCatalogItem( antibioticRepository.save(new AntibioticCatalogItem(
existing.id(), existing.id(),
existing.accountId(),
existing.businessKey(), existing.businessKey(),
existing.code(), existing.code(),
existing.name(), existing.name(),
@@ -368,6 +425,7 @@ public class CatalogService {
)); ));
antibioticRepository.save(new AntibioticCatalogItem( antibioticRepository.save(new AntibioticCatalogItem(
null, null,
existing.accountId(),
existing.businessKey(), existing.businessKey(),
blankToNull(mutation.code()), blankToNull(mutation.code()),
mutation.name().trim(), mutation.name().trim(),
@@ -381,6 +439,7 @@ public class CatalogService {
if (existing.active() != mutation.active()) { if (existing.active() != mutation.active()) {
antibioticRepository.save(new AntibioticCatalogItem( antibioticRepository.save(new AntibioticCatalogItem(
existing.id(), existing.id(),
existing.accountId(),
existing.businessKey(), existing.businessKey(),
existing.code(), existing.code(),
existing.name(), existing.name(),
@@ -391,7 +450,7 @@ public class CatalogService {
)); ));
} }
} }
return listAntibioticRows(); return listAntibioticRowsForActor(actor);
} }
public List<UserRow> listUsers(String actorId) { public List<UserRow> listUsers(String actorId) {
@@ -653,28 +712,33 @@ public class CatalogService {
backfillDefaultUserEmails(); backfillDefaultUserEmails();
removeLegacyPortalLoginField(); removeLegacyPortalLoginField();
migrateCustomerNumbers(); migrateCustomerNumbers();
migrateCatalogAccountIds();
ensureDefaultUser("Administrator", "admin@muh.local", "Admin123!", UserRole.ADMIN); ensureDefaultUser("Administrator", "admin@muh.local", "Admin123!", UserRole.ADMIN);
} }
public Farmer requireActiveFarmer(String businessKey) { public Farmer requireActiveFarmer(String actorId, String businessKey) {
return farmerRepository.findByActiveTrueOrderByNameAsc().stream() AppUser actor = requireActiveActor(actorId, "Nicht berechtigt");
return listActiveFarmersForActor(actor).stream()
.filter(farmer -> farmer.businessKey().equals(businessKey)) .filter(farmer -> farmer.businessKey().equals(businessKey))
.findFirst() .findFirst()
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden")); .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden"));
} }
public Map<String, PathogenCatalogItem> activePathogensByBusinessKey() { public Map<String, PathogenCatalogItem> activePathogensByBusinessKey(String actorId) {
return pathogenRepository.findByActiveTrueOrderByNameAsc().stream() AppUser actor = requireActiveActor(actorId, "Nicht berechtigt");
return listActivePathogensForActor(actor).stream()
.collect(Collectors.toMap(PathogenCatalogItem::businessKey, Function.identity())); .collect(Collectors.toMap(PathogenCatalogItem::businessKey, Function.identity()));
} }
public Map<String, AntibioticCatalogItem> activeAntibioticsByBusinessKey() { public Map<String, AntibioticCatalogItem> activeAntibioticsByBusinessKey(String actorId) {
return antibioticRepository.findByActiveTrueOrderByNameAsc().stream() AppUser actor = requireActiveActor(actorId, "Nicht berechtigt");
return listActiveAntibioticsForActor(actor).stream()
.collect(Collectors.toMap(AntibioticCatalogItem::businessKey, Function.identity())); .collect(Collectors.toMap(AntibioticCatalogItem::businessKey, Function.identity()));
} }
public Map<String, MedicationCatalogItem> activeMedicationsByBusinessKey() { public Map<String, MedicationCatalogItem> activeMedicationsByBusinessKey(String actorId) {
return medicationRepository.findByActiveTrueOrderByNameAsc().stream() AppUser actor = requireActiveActor(actorId, "Nicht berechtigt");
return listActiveMedicationsForActor(actor).stream()
.collect(Collectors.toMap(MedicationCatalogItem::businessKey, Function.identity())); .collect(Collectors.toMap(MedicationCatalogItem::businessKey, Function.identity()));
} }
@@ -999,6 +1063,82 @@ public class CatalogService {
backfillDefaultUserEmail("admin", "admin@muh.local"); 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) { private void backfillDefaultUserEmail(String legacyPortalLogin, String email) {
mongoTemplate.updateMulti( mongoTemplate.updateMulti(
new Query(new Criteria().andOperator( new Query(new Criteria().andOperator(

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -30,7 +30,7 @@ public class PortalService {
Long sampleNumber, Long sampleNumber,
LocalDate date LocalDate date
) { ) {
List<CatalogService.FarmerOption> matchingFarmers = catalogService.activeCatalogSummary().farmers().stream() List<CatalogService.FarmerOption> matchingFarmers = catalogService.activeCatalogSummary(actorId).farmers().stream()
.filter(farmer -> farmerQuery == null || farmerQuery.isBlank() || farmer.name().toLowerCase(Locale.ROOT).contains(farmerQuery.toLowerCase(Locale.ROOT))) .filter(farmer -> farmerQuery == null || farmerQuery.isBlank() || farmer.name().toLowerCase(Locale.ROOT).contains(farmerQuery.toLowerCase(Locale.ROOT)))
.toList(); .toList();

View File

@@ -102,7 +102,7 @@ public class SampleService {
public SampleDetail createSample(String actorId, RegistrationRequest request) { public SampleDetail createSample(String actorId, RegistrationRequest request) {
AppUser actor = requireActor(actorId); AppUser actor = requireActor(actorId);
LocalDateTime now = LocalDateTime.now(); 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())) .filter(candidate -> candidate.businessKey().equals(request.farmerBusinessKey()))
.findFirst() .findFirst()
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden")); .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"); 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())) .filter(candidate -> candidate.businessKey().equals(request.farmerBusinessKey()))
.findFirst() .findFirst()
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden")); .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden"));
@@ -220,7 +220,7 @@ public class SampleService {
current.put(quarter.quarterKey(), quarter); current.put(quarter.quarterKey(), quarter);
} }
Map<String, PathogenCatalogItem> pathogens = catalogService.activePathogensByBusinessKey(); Map<String, PathogenCatalogItem> pathogens = catalogService.activePathogensByBusinessKey(actorId);
List<QuarterFinding> updatedQuarters = new ArrayList<>(); List<QuarterFinding> updatedQuarters = new ArrayList<>();
for (AnamnesisQuarterRequest quarterRequest : request.quarters()) { for (AnamnesisQuarterRequest quarterRequest : request.quarters()) {
QuarterFinding base = current.get(quarterRequest.quarterKey()); 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"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Antibiogramm kann nicht mehr geändert werden");
} }
Map<String, de.svencarstensen.muh.domain.AntibioticCatalogItem> antibiotics = catalogService.activeAntibioticsByBusinessKey(); Map<String, de.svencarstensen.muh.domain.AntibioticCatalogItem> antibiotics = catalogService.activeAntibioticsByBusinessKey(actorId);
Map<QuarterKey, QuarterAntibiogram> groups = new HashMap<>(); Map<QuarterKey, QuarterAntibiogram> groups = new HashMap<>();
Map<QuarterKey, QuarterFinding> quartersByKey = existing.quarters().stream() Map<QuarterKey, QuarterFinding> quartersByKey = existing.quarters().stream()
.collect(java.util.stream.Collectors.toMap(QuarterFinding::quarterKey, quarter -> quarter)); .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"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Therapie kann nicht bearbeitet werden");
} }
Map<String, de.svencarstensen.muh.domain.MedicationCatalogItem> medications = catalogService.activeMedicationsByBusinessKey(); Map<String, de.svencarstensen.muh.domain.MedicationCatalogItem> medications = catalogService.activeMedicationsByBusinessKey(actorId);
TherapyRecommendation therapy = new TherapyRecommendation( TherapyRecommendation therapy = new TherapyRecommendation(
request.continueStarted(), request.continueStarted(),
request.switchTherapy(), request.switchTherapy(),

View File

@@ -26,30 +26,57 @@ public class CatalogController {
@GetMapping("/catalogs/summary") @GetMapping("/catalogs/summary")
public CatalogService.ActiveCatalogSummary catalogSummary() { public CatalogService.ActiveCatalogSummary catalogSummary() {
return catalogService.activeCatalogSummary(); return catalogService.activeCatalogSummary(securitySupport.currentUser().id());
} }
// Legacy admin endpoints - ADMIN only
@GetMapping("/admin") @GetMapping("/admin")
public CatalogService.AdministrationOverview administrationOverview() { public CatalogService.AdministrationOverview administrationOverview() {
return catalogService.administrationOverview(securitySupport.currentUser().id()); return catalogService.administrationOverview(securitySupport.currentUser().id());
} }
@PostMapping("/admin/farmers") @PostMapping("/admin/farmers")
public List<CatalogService.FarmerRow> saveFarmers(@RequestBody List<CatalogService.FarmerMutation> mutations) { public List<CatalogService.FarmerRow> saveFarmersAdmin(@RequestBody List<CatalogService.FarmerMutation> mutations) {
return catalogService.saveFarmers(securitySupport.currentUser().id(), mutations); return catalogService.saveFarmers(securitySupport.currentUser().id(), mutations);
} }
@PostMapping("/admin/medications") @PostMapping("/admin/medications")
public List<CatalogService.MedicationRow> saveMedications(@RequestBody List<CatalogService.MedicationMutation> mutations) { public List<CatalogService.MedicationRow> saveMedicationsAdmin(@RequestBody List<CatalogService.MedicationMutation> mutations) {
return catalogService.saveMedications(securitySupport.currentUser().id(), mutations); return catalogService.saveMedications(securitySupport.currentUser().id(), mutations);
} }
@PostMapping("/admin/pathogens") @PostMapping("/admin/pathogens")
public List<CatalogService.PathogenRow> savePathogens(@RequestBody List<CatalogService.PathogenMutation> mutations) { public List<CatalogService.PathogenRow> savePathogensAdmin(@RequestBody List<CatalogService.PathogenMutation> mutations) {
return catalogService.savePathogens(securitySupport.currentUser().id(), mutations); return catalogService.savePathogens(securitySupport.currentUser().id(), mutations);
} }
@PostMapping("/admin/antibiotics") @PostMapping("/admin/antibiotics")
public List<CatalogService.AntibioticRow> saveAntibioticsAdmin(@RequestBody List<CatalogService.AntibioticMutation> 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<CatalogService.FarmerRow> saveFarmers(@RequestBody List<CatalogService.FarmerMutation> mutations) {
return catalogService.saveFarmers(securitySupport.currentUser().id(), mutations);
}
@PostMapping("/catalog/medications")
public List<CatalogService.MedicationRow> saveMedications(@RequestBody List<CatalogService.MedicationMutation> mutations) {
return catalogService.saveMedications(securitySupport.currentUser().id(), mutations);
}
@PostMapping("/catalog/pathogens")
public List<CatalogService.PathogenRow> savePathogens(@RequestBody List<CatalogService.PathogenMutation> mutations) {
return catalogService.savePathogens(securitySupport.currentUser().id(), mutations);
}
@PostMapping("/catalog/antibiotics")
public List<CatalogService.AntibioticRow> saveAntibiotics(@RequestBody List<CatalogService.AntibioticMutation> mutations) { public List<CatalogService.AntibioticRow> saveAntibiotics(@RequestBody List<CatalogService.AntibioticMutation> mutations) {
return catalogService.saveAntibiotics(securitySupport.currentUser().id(), mutations); return catalogService.saveAntibiotics(securitySupport.currentUser().id(), mutations);
} }

View File

@@ -6,7 +6,7 @@
* - MINOR: New functionality (backward compatible) * - MINOR: New functionality (backward compatible)
* - PATCH: Bug fixes (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 * Build date - set at build time

View File

@@ -122,7 +122,7 @@ export default function AdministrationPage() {
useEffect(() => { useEffect(() => {
async function load() { async function load() {
try { try {
const response = await apiGet<AdministrationOverview>("/admin"); const response = await apiGet<AdministrationOverview>("/catalog/overview");
setDatasets(normalizeOverview(response)); setDatasets(normalizeOverview(response));
} catch (loadError) { } catch (loadError) {
setMessage((loadError as Error).message); setMessage((loadError as Error).message);
@@ -176,7 +176,7 @@ export default function AdministrationPage() {
let response: EditableRow[]; let response: EditableRow[];
switch (selectedDataset) { switch (selectedDataset) {
case "farmers": case "farmers":
response = await apiPost<EditableRow[]>("/admin/farmers", rows.map((row) => ({ response = await apiPost<EditableRow[]>("/catalog/farmers", rows.map((row) => ({
id: row.id || null, id: row.id || null,
name: row.name, name: row.name,
email: row.email || null, email: row.email || null,
@@ -184,7 +184,7 @@ export default function AdministrationPage() {
}))); })));
break; break;
case "medications": case "medications":
response = await apiPost<EditableRow[]>("/admin/medications", rows.map((row) => ({ response = await apiPost<EditableRow[]>("/catalog/medications", rows.map((row) => ({
id: row.id || null, id: row.id || null,
name: row.name, name: row.name,
category: row.category, category: row.category,
@@ -192,7 +192,7 @@ export default function AdministrationPage() {
}))); })));
break; break;
case "pathogens": case "pathogens":
response = await apiPost<EditableRow[]>("/admin/pathogens", rows.map((row) => ({ response = await apiPost<EditableRow[]>("/catalog/pathogens", rows.map((row) => ({
id: row.id || null, id: row.id || null,
code: row.code || null, code: row.code || null,
name: row.name, name: row.name,
@@ -201,7 +201,7 @@ export default function AdministrationPage() {
}))); })));
break; break;
case "antibiotics": case "antibiotics":
response = await apiPost<EditableRow[]>("/admin/antibiotics", rows.map((row) => ({ response = await apiPost<EditableRow[]>("/catalog/antibiotics", rows.map((row) => ({
id: row.id || null, id: row.id || null,
code: row.code || null, code: row.code || null,
name: row.name, name: row.name,