Harden access control and restore customer admin pages
This commit is contained in:
@@ -27,6 +27,7 @@ public record Sample(
|
|||||||
LocalDateTime createdAt,
|
LocalDateTime createdAt,
|
||||||
LocalDateTime updatedAt,
|
LocalDateTime updatedAt,
|
||||||
LocalDateTime completedAt,
|
LocalDateTime completedAt,
|
||||||
|
String ownerAccountId,
|
||||||
String createdByUserCode,
|
String createdByUserCode,
|
||||||
String createdByDisplayName
|
String createdByDisplayName
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ public class AuthTokenService {
|
|||||||
@Value("${muh.security.token-secret}") String secret,
|
@Value("${muh.security.token-secret}") String secret,
|
||||||
@Value("${muh.security.token-validity-hours:12}") long validityHours
|
@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.secret = secret.getBytes(StandardCharsets.UTF_8);
|
||||||
this.validitySeconds = Math.max(1, validityHours) * 3600L;
|
this.validitySeconds = Math.max(1, validityHours) * 3600L;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import de.svencarstensen.muh.repository.FarmerRepository;
|
|||||||
import de.svencarstensen.muh.repository.MedicationCatalogRepository;
|
import de.svencarstensen.muh.repository.MedicationCatalogRepository;
|
||||||
import de.svencarstensen.muh.repository.PathogenCatalogRepository;
|
import de.svencarstensen.muh.repository.PathogenCatalogRepository;
|
||||||
import de.svencarstensen.muh.security.AuthTokenService;
|
import de.svencarstensen.muh.security.AuthTokenService;
|
||||||
|
import de.svencarstensen.muh.security.AuthorizationService;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||||
@@ -66,6 +67,7 @@ public class CatalogService {
|
|||||||
private final AppUserRepository appUserRepository;
|
private final AppUserRepository appUserRepository;
|
||||||
private final MongoTemplate mongoTemplate;
|
private final MongoTemplate mongoTemplate;
|
||||||
private final AuthTokenService authTokenService;
|
private final AuthTokenService authTokenService;
|
||||||
|
private final AuthorizationService authorizationService;
|
||||||
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||||
|
|
||||||
public CatalogService(
|
public CatalogService(
|
||||||
@@ -75,7 +77,8 @@ public class CatalogService {
|
|||||||
AntibioticCatalogRepository antibioticRepository,
|
AntibioticCatalogRepository antibioticRepository,
|
||||||
AppUserRepository appUserRepository,
|
AppUserRepository appUserRepository,
|
||||||
MongoTemplate mongoTemplate,
|
MongoTemplate mongoTemplate,
|
||||||
AuthTokenService authTokenService
|
AuthTokenService authTokenService,
|
||||||
|
AuthorizationService authorizationService
|
||||||
) {
|
) {
|
||||||
this.farmerRepository = farmerRepository;
|
this.farmerRepository = farmerRepository;
|
||||||
this.medicationRepository = medicationRepository;
|
this.medicationRepository = medicationRepository;
|
||||||
@@ -84,6 +87,7 @@ public class CatalogService {
|
|||||||
this.appUserRepository = appUserRepository;
|
this.appUserRepository = appUserRepository;
|
||||||
this.mongoTemplate = mongoTemplate;
|
this.mongoTemplate = mongoTemplate;
|
||||||
this.authTokenService = authTokenService;
|
this.authTokenService = authTokenService;
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ActiveCatalogSummary activeCatalogSummary() {
|
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());
|
return new AdministrationOverview(listFarmerRows(), listMedicationRows(), listPathogenRows(), listAntibioticRows());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +133,8 @@ public class CatalogService {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FarmerRow> saveFarmers(List<FarmerMutation> mutations) {
|
public List<FarmerRow> saveFarmers(String actorId, List<FarmerMutation> mutations) {
|
||||||
|
authorizationService.requireActiveUser(actorId, "Nicht berechtigt");
|
||||||
for (FarmerMutation mutation : mutations) {
|
for (FarmerMutation mutation : mutations) {
|
||||||
if (isBlank(mutation.name())) {
|
if (isBlank(mutation.name())) {
|
||||||
continue;
|
continue;
|
||||||
@@ -191,7 +197,8 @@ public class CatalogService {
|
|||||||
return listFarmerRows();
|
return listFarmerRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<MedicationRow> saveMedications(List<MedicationMutation> mutations) {
|
public List<MedicationRow> saveMedications(String actorId, List<MedicationMutation> mutations) {
|
||||||
|
authorizationService.requireActiveUser(actorId, "Nicht berechtigt");
|
||||||
for (MedicationMutation mutation : mutations) {
|
for (MedicationMutation mutation : mutations) {
|
||||||
if (isBlank(mutation.name()) || mutation.category() == null) {
|
if (isBlank(mutation.name()) || mutation.category() == null) {
|
||||||
continue;
|
continue;
|
||||||
@@ -254,7 +261,8 @@ public class CatalogService {
|
|||||||
return listMedicationRows();
|
return listMedicationRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<PathogenRow> savePathogens(List<PathogenMutation> mutations) {
|
public List<PathogenRow> savePathogens(String actorId, List<PathogenMutation> mutations) {
|
||||||
|
authorizationService.requireActiveUser(actorId, "Nicht berechtigt");
|
||||||
for (PathogenMutation mutation : mutations) {
|
for (PathogenMutation mutation : mutations) {
|
||||||
if (isBlank(mutation.name()) || mutation.kind() == null) {
|
if (isBlank(mutation.name()) || mutation.kind() == null) {
|
||||||
continue;
|
continue;
|
||||||
@@ -322,7 +330,8 @@ public class CatalogService {
|
|||||||
return listPathogenRows();
|
return listPathogenRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<AntibioticRow> saveAntibiotics(List<AntibioticMutation> mutations) {
|
public List<AntibioticRow> saveAntibiotics(String actorId, List<AntibioticMutation> mutations) {
|
||||||
|
authorizationService.requireActiveUser(actorId, "Nicht berechtigt");
|
||||||
for (AntibioticMutation mutation : mutations) {
|
for (AntibioticMutation mutation : mutations) {
|
||||||
if (isBlank(mutation.name())) {
|
if (isBlank(mutation.name())) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -36,20 +36,20 @@ public class PortalService {
|
|||||||
|
|
||||||
List<PortalSampleRow> sampleRows;
|
List<PortalSampleRow> sampleRows;
|
||||||
if (sampleNumber != null) {
|
if (sampleNumber != null) {
|
||||||
sampleRows = List.of(toPortalRow(sampleService.getSampleByNumber(sampleNumber)));
|
sampleRows = List.of(toPortalRow(sampleService.getSampleByNumber(actorId, sampleNumber)));
|
||||||
} else if (farmerBusinessKey != null && !farmerBusinessKey.isBlank()) {
|
} 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))
|
.filter(sample -> cowQuery == null || cowQuery.isBlank() || cowMatches(sample, cowQuery))
|
||||||
.map(this::toPortalRow)
|
.map(this::toPortalRow)
|
||||||
.sorted(Comparator.comparing(PortalSampleRow::createdAt).reversed())
|
.sorted(Comparator.comparing(PortalSampleRow::createdAt).reversed())
|
||||||
.toList();
|
.toList();
|
||||||
} else if (date != null) {
|
} else if (date != null) {
|
||||||
sampleRows = sampleService.samplesByDate(date).stream()
|
sampleRows = sampleService.samplesByDate(actorId, date).stream()
|
||||||
.map(this::toPortalRow)
|
.map(this::toPortalRow)
|
||||||
.sorted(Comparator.comparing(PortalSampleRow::completedAt, Comparator.nullsLast(Comparator.reverseOrder())))
|
.sorted(Comparator.comparing(PortalSampleRow::completedAt, Comparator.nullsLast(Comparator.reverseOrder())))
|
||||||
.toList();
|
.toList();
|
||||||
} else {
|
} else {
|
||||||
sampleRows = sampleService.completedSamples().stream()
|
sampleRows = sampleService.completedSamples(actorId).stream()
|
||||||
.limit(25)
|
.limit(25)
|
||||||
.map(this::toPortalRow)
|
.map(this::toPortalRow)
|
||||||
.toList();
|
.toList();
|
||||||
@@ -58,13 +58,13 @@ public class PortalService {
|
|||||||
return new PortalSnapshot(
|
return new PortalSnapshot(
|
||||||
matchingFarmers,
|
matchingFarmers,
|
||||||
sampleRows,
|
sampleRows,
|
||||||
reportService.reportCandidates(),
|
reportService.reportCandidates(actorId),
|
||||||
includeUsers ? catalogService.listUsers(actorId) : List.of()
|
includeUsers ? catalogService.listUsers(actorId) : List.of()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<PortalSampleRow> searchSamplesByCreatedDate(LocalDate date) {
|
public List<PortalSampleRow> searchSamplesByCreatedDate(String actorId, LocalDate date) {
|
||||||
return sampleService.samplesByCreatedDate(date).stream()
|
return sampleService.samplesByCreatedDate(actorId, date).stream()
|
||||||
.map(this::toPortalRow)
|
.map(this::toPortalRow)
|
||||||
.sorted(Comparator.comparing(PortalSampleRow::createdAt).reversed())
|
.sorted(Comparator.comparing(PortalSampleRow::createdAt).reversed())
|
||||||
.toList();
|
.toList();
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ public class ReportService {
|
|||||||
this.reportMailTemplate = loadTemplate(reportMailTemplateResource);
|
this.reportMailTemplate = loadTemplate(reportMailTemplateResource);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ReportCandidate> reportCandidates() {
|
public List<ReportCandidate> reportCandidates(String actorId) {
|
||||||
return sampleService.completedSamples().stream()
|
return sampleService.completedSamples(actorId).stream()
|
||||||
.filter(sample -> sample.farmerEmail() != null && !sample.farmerEmail().isBlank())
|
.filter(sample -> sample.farmerEmail() != null && !sample.farmerEmail().isBlank())
|
||||||
.map(this::toCandidate)
|
.map(this::toCandidate)
|
||||||
.toList();
|
.toList();
|
||||||
@@ -66,7 +66,7 @@ public class ReportService {
|
|||||||
List<ReportCandidate> skipped = new ArrayList<>();
|
List<ReportCandidate> skipped = new ArrayList<>();
|
||||||
|
|
||||||
for (String sampleId : sampleIds) {
|
for (String sampleId : sampleIds) {
|
||||||
Sample sample = sampleService.loadSampleEntity(sampleId);
|
Sample sample = sampleService.loadSampleEntity(actorId, sampleId);
|
||||||
if (sample.farmerEmail() == null || sample.farmerEmail().isBlank() || sample.reportBlocked()) {
|
if (sample.farmerEmail() == null || sample.farmerEmail().isBlank() || sample.reportBlocked()) {
|
||||||
skipped.add(toCandidate(sample));
|
skipped.add(toCandidate(sample));
|
||||||
continue;
|
continue;
|
||||||
@@ -82,12 +82,13 @@ public class ReportService {
|
|||||||
return new DispatchResult(sent, skipped, mailEnabled && mailSenderProvider.getIfAvailable() != null);
|
return new DispatchResult(sent, skipped, mailEnabled && mailSenderProvider.getIfAvailable() != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] reportPdf(String sampleId) {
|
public byte[] reportPdf(String actorId, String sampleId) {
|
||||||
return buildPdf(sampleService.loadSampleEntity(sampleId));
|
return buildPdf(sampleService.loadSampleEntity(actorId, sampleId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public SampleService.SampleDetail toggleReportBlocked(String sampleId, boolean blocked) {
|
public SampleService.SampleDetail toggleReportBlocked(String actorId, String sampleId, boolean blocked) {
|
||||||
return sampleService.getSample(sampleService.toggleReportBlocked(sampleId, blocked).id());
|
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) {
|
private void sendMail(Sample sample, byte[] pdf, String customerSignature) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.svencarstensen.muh.service;
|
package de.svencarstensen.muh.service;
|
||||||
|
|
||||||
import de.svencarstensen.muh.domain.AntibiogramEntry;
|
import de.svencarstensen.muh.domain.AntibiogramEntry;
|
||||||
|
import de.svencarstensen.muh.domain.AppUser;
|
||||||
import de.svencarstensen.muh.domain.PathogenCatalogItem;
|
import de.svencarstensen.muh.domain.PathogenCatalogItem;
|
||||||
import de.svencarstensen.muh.domain.PathogenKind;
|
import de.svencarstensen.muh.domain.PathogenKind;
|
||||||
import de.svencarstensen.muh.domain.QuarterAntibiogram;
|
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.SensitivityResult;
|
||||||
import de.svencarstensen.muh.domain.TherapyRecommendation;
|
import de.svencarstensen.muh.domain.TherapyRecommendation;
|
||||||
import de.svencarstensen.muh.repository.SampleRepository;
|
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.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
@@ -25,33 +28,54 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class SampleService {
|
public class SampleService {
|
||||||
|
|
||||||
private final SampleRepository sampleRepository;
|
private final SampleRepository sampleRepository;
|
||||||
private final CatalogService catalogService;
|
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.sampleRepository = sampleRepository;
|
||||||
this.catalogService = catalogService;
|
this.catalogService = catalogService;
|
||||||
|
this.appUserRepository = appUserRepository;
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DashboardOverview dashboardOverview() {
|
public DashboardOverview dashboardOverview(String actorId) {
|
||||||
List<SampleSummary> recent = sampleRepository.findTop12ByOrderByUpdatedAtDesc().stream()
|
AppUser actor = requireActor(actorId);
|
||||||
|
List<Sample> accessibleSamples = accessibleSamples(actor);
|
||||||
|
List<SampleSummary> recent = accessibleSamples.stream()
|
||||||
|
.sorted(Comparator.comparing(Sample::updatedAt).reversed())
|
||||||
|
.limit(12)
|
||||||
.map(this::toSummary)
|
.map(this::toSummary)
|
||||||
.toList();
|
.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();
|
LocalDate today = LocalDate.now();
|
||||||
long completedToday = sampleRepository.findByCompletedAtBetweenOrderByCompletedAtDesc(
|
long completedToday = accessibleSamples.stream()
|
||||||
today.atStartOfDay(),
|
.filter(sample -> sample.completedAt() != null)
|
||||||
today.plusDays(1).atStartOfDay()
|
.filter(sample -> !sample.completedAt().isBefore(today.atStartOfDay()))
|
||||||
).size();
|
.filter(sample -> sample.completedAt().isBefore(today.plusDays(1).atStartOfDay()))
|
||||||
|
.count();
|
||||||
return new DashboardOverview(nextSampleNumber(), openCount, completedToday, recent);
|
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)
|
return sampleRepository.findBySampleNumber(sampleNumber)
|
||||||
|
.filter(sample -> canAccess(actor, sample))
|
||||||
.map(sample -> new LookupResult(
|
.map(sample -> new LookupResult(
|
||||||
true,
|
true,
|
||||||
"Probe gefunden",
|
"Probe gefunden",
|
||||||
@@ -62,17 +86,20 @@ public class SampleService {
|
|||||||
.orElseGet(() -> new LookupResult(false, "Proben-Nummer unbekannt", null, null, null));
|
.orElseGet(() -> new LookupResult(false, "Proben-Nummer unbekannt", null, null, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
public SampleDetail getSample(String id) {
|
public SampleDetail getSample(String actorId, String id) {
|
||||||
return toDetail(loadSample(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)
|
return sampleRepository.findBySampleNumber(sampleNumber)
|
||||||
|
.filter(sample -> canAccess(actor, sample))
|
||||||
.map(this::toDetail)
|
.map(this::toDetail)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Probe nicht gefunden"));
|
.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();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
CatalogService.FarmerOption farmer = catalogService.activeCatalogSummary().farmers().stream()
|
CatalogService.FarmerOption farmer = catalogService.activeCatalogSummary().farmers().stream()
|
||||||
.filter(candidate -> candidate.businessKey().equals(request.farmerBusinessKey()))
|
.filter(candidate -> candidate.businessKey().equals(request.farmerBusinessKey()))
|
||||||
@@ -99,6 +126,7 @@ public class SampleService {
|
|||||||
now,
|
now,
|
||||||
now,
|
now,
|
||||||
null,
|
null,
|
||||||
|
authorizationService.accountId(actor),
|
||||||
request.userCode(),
|
request.userCode(),
|
||||||
request.userDisplayName()
|
request.userDisplayName()
|
||||||
);
|
);
|
||||||
@@ -106,8 +134,8 @@ public class SampleService {
|
|||||||
return toDetail(sampleRepository.save(sample));
|
return toDetail(sampleRepository.save(sample));
|
||||||
}
|
}
|
||||||
|
|
||||||
public SampleDetail saveRegistration(String id, RegistrationRequest request) {
|
public SampleDetail saveRegistration(String actorId, String id, RegistrationRequest request) {
|
||||||
Sample existing = loadSample(id);
|
Sample existing = loadAccessibleSample(actorId, id);
|
||||||
if (!SampleWorkflowRules.canEditRegistration(existing)) {
|
if (!SampleWorkflowRules.canEditRegistration(existing)) {
|
||||||
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");
|
||||||
}
|
}
|
||||||
@@ -137,6 +165,7 @@ public class SampleService {
|
|||||||
existing.createdAt(),
|
existing.createdAt(),
|
||||||
LocalDateTime.now(),
|
LocalDateTime.now(),
|
||||||
existing.completedAt(),
|
existing.completedAt(),
|
||||||
|
existing.ownerAccountId(),
|
||||||
existing.createdByUserCode(),
|
existing.createdByUserCode(),
|
||||||
existing.createdByDisplayName()
|
existing.createdByDisplayName()
|
||||||
));
|
));
|
||||||
@@ -144,8 +173,8 @@ public class SampleService {
|
|||||||
return toDetail(saved);
|
return toDetail(saved);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SampleDetail saveAnamnesis(String id, AnamnesisRequest request) {
|
public SampleDetail saveAnamnesis(String actorId, String id, AnamnesisRequest request) {
|
||||||
Sample existing = loadSample(id);
|
Sample existing = loadAccessibleSample(actorId, id);
|
||||||
if (!SampleWorkflowRules.canEditAnamnesis(existing)) {
|
if (!SampleWorkflowRules.canEditAnamnesis(existing)) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Anamnese kann an dieser Stelle nicht geändert werden");
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Anamnese kann an dieser Stelle nicht geändert werden");
|
||||||
}
|
}
|
||||||
@@ -203,14 +232,15 @@ public class SampleService {
|
|||||||
existing.createdAt(),
|
existing.createdAt(),
|
||||||
LocalDateTime.now(),
|
LocalDateTime.now(),
|
||||||
existing.completedAt(),
|
existing.completedAt(),
|
||||||
|
existing.ownerAccountId(),
|
||||||
existing.createdByUserCode(),
|
existing.createdByUserCode(),
|
||||||
existing.createdByDisplayName()
|
existing.createdByDisplayName()
|
||||||
));
|
));
|
||||||
return toDetail(saved);
|
return toDetail(saved);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SampleDetail saveAntibiogram(String id, AntibiogramRequest request) {
|
public SampleDetail saveAntibiogram(String actorId, String id, AntibiogramRequest request) {
|
||||||
Sample existing = loadSample(id);
|
Sample existing = loadAccessibleSample(actorId, id);
|
||||||
if (!SampleWorkflowRules.canEditAntibiogram(existing)) {
|
if (!SampleWorkflowRules.canEditAntibiogram(existing)) {
|
||||||
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");
|
||||||
}
|
}
|
||||||
@@ -293,14 +323,15 @@ public class SampleService {
|
|||||||
existing.createdAt(),
|
existing.createdAt(),
|
||||||
LocalDateTime.now(),
|
LocalDateTime.now(),
|
||||||
existing.completedAt(),
|
existing.completedAt(),
|
||||||
|
existing.ownerAccountId(),
|
||||||
existing.createdByUserCode(),
|
existing.createdByUserCode(),
|
||||||
existing.createdByDisplayName()
|
existing.createdByDisplayName()
|
||||||
));
|
));
|
||||||
return toDetail(saved);
|
return toDetail(saved);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SampleDetail saveTherapy(String id, TherapyRequest request) {
|
public SampleDetail saveTherapy(String actorId, String id, TherapyRequest request) {
|
||||||
Sample existing = loadSample(id);
|
Sample existing = loadAccessibleSample(actorId, id);
|
||||||
if (existing.currentStep() == SampleWorkflowStep.COMPLETED) {
|
if (existing.currentStep() == SampleWorkflowStep.COMPLETED) {
|
||||||
TherapyRecommendation previous = existing.therapyRecommendation();
|
TherapyRecommendation previous = existing.therapyRecommendation();
|
||||||
TherapyRecommendation updated = previous == null
|
TherapyRecommendation updated = previous == null
|
||||||
@@ -341,6 +372,7 @@ public class SampleService {
|
|||||||
existing.createdAt(),
|
existing.createdAt(),
|
||||||
LocalDateTime.now(),
|
LocalDateTime.now(),
|
||||||
existing.completedAt(),
|
existing.completedAt(),
|
||||||
|
existing.ownerAccountId(),
|
||||||
existing.createdByUserCode(),
|
existing.createdByUserCode(),
|
||||||
existing.createdByDisplayName()
|
existing.createdByDisplayName()
|
||||||
)));
|
)));
|
||||||
@@ -388,6 +420,7 @@ public class SampleService {
|
|||||||
existing.createdAt(),
|
existing.createdAt(),
|
||||||
LocalDateTime.now(),
|
LocalDateTime.now(),
|
||||||
LocalDateTime.now(),
|
LocalDateTime.now(),
|
||||||
|
existing.ownerAccountId(),
|
||||||
existing.createdByUserCode(),
|
existing.createdByUserCode(),
|
||||||
existing.createdByDisplayName()
|
existing.createdByDisplayName()
|
||||||
));
|
));
|
||||||
@@ -416,6 +449,7 @@ public class SampleService {
|
|||||||
existing.createdAt(),
|
existing.createdAt(),
|
||||||
LocalDateTime.now(),
|
LocalDateTime.now(),
|
||||||
existing.completedAt(),
|
existing.completedAt(),
|
||||||
|
existing.ownerAccountId(),
|
||||||
existing.createdByUserCode(),
|
existing.createdByUserCode(),
|
||||||
existing.createdByDisplayName()
|
existing.createdByDisplayName()
|
||||||
));
|
));
|
||||||
@@ -443,25 +477,41 @@ public class SampleService {
|
|||||||
existing.createdAt(),
|
existing.createdAt(),
|
||||||
LocalDateTime.now(),
|
LocalDateTime.now(),
|
||||||
existing.completedAt(),
|
existing.completedAt(),
|
||||||
|
existing.ownerAccountId(),
|
||||||
existing.createdByUserCode(),
|
existing.createdByUserCode(),
|
||||||
existing.createdByDisplayName()
|
existing.createdByDisplayName()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Sample> completedSamples() {
|
public List<Sample> completedSamples(String actorId) {
|
||||||
return sampleRepository.findByCompletedAtNotNullOrderByCompletedAtDesc();
|
return accessibleSamples(requireActor(actorId)).stream()
|
||||||
|
.filter(sample -> sample.completedAt() != null)
|
||||||
|
.sorted(Comparator.comparing(Sample::completedAt).reversed())
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Sample> samplesByFarmerBusinessKey(String businessKey) {
|
public List<Sample> samplesByFarmerBusinessKey(String actorId, String businessKey) {
|
||||||
return sampleRepository.findByFarmerBusinessKeyOrderByCreatedAtDesc(businessKey);
|
return accessibleSamples(requireActor(actorId)).stream()
|
||||||
|
.filter(sample -> Objects.equals(sample.farmerBusinessKey(), businessKey))
|
||||||
|
.sorted(Comparator.comparing(Sample::createdAt).reversed())
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Sample> samplesByCreatedDate(LocalDate date) {
|
public List<Sample> samplesByCreatedDate(String actorId, LocalDate date) {
|
||||||
return sampleRepository.findByCreatedAtBetweenOrderByCreatedAtDesc(date.atStartOfDay(), date.plusDays(1).atStartOfDay());
|
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<Sample> samplesByDate(LocalDate date) {
|
public List<Sample> samplesByDate(String actorId, LocalDate date) {
|
||||||
return sampleRepository.findByCompletedAtBetweenOrderByCompletedAtDesc(date.atStartOfDay(), date.plusDays(1).atStartOfDay());
|
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() {
|
public long nextSampleNumber() {
|
||||||
@@ -470,15 +520,119 @@ public class SampleService {
|
|||||||
.orElse(100001L);
|
.orElse(100001L);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Sample loadSampleEntity(String id) {
|
public Sample loadSampleEntity(String actorId, String id) {
|
||||||
return loadSample(id);
|
return loadAccessibleSample(actorId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppUser requireActor(String actorId) {
|
||||||
|
ensureSampleOwnershipMigration();
|
||||||
|
return authorizationService.requireActiveUser(actorId, "Nicht berechtigt");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Sample> accessibleSamples(AppUser actor) {
|
||||||
|
List<Sample> 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) {
|
private Sample loadSample(String id) {
|
||||||
|
ensureSampleOwnershipMigration();
|
||||||
return sampleRepository.findById(Objects.requireNonNull(id))
|
return sampleRepository.findById(Objects.requireNonNull(id))
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Probe nicht gefunden"));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Probe nicht gefunden"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ensureSampleOwnershipMigration() {
|
||||||
|
List<Sample> samples = sampleRepository.findAll().stream()
|
||||||
|
.filter(sample -> sample.ownerAccountId() == null || sample.ownerAccountId().isBlank())
|
||||||
|
.toList();
|
||||||
|
if (samples.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AppUser> users = appUserRepository.findAll().stream()
|
||||||
|
.filter(AppUser::active)
|
||||||
|
.toList();
|
||||||
|
Map<String, List<AppUser>> usersByDisplayName = users.stream()
|
||||||
|
.filter(user -> user.displayName() != null && !user.displayName().isBlank())
|
||||||
|
.collect(Collectors.groupingBy(user -> user.displayName().trim().toLowerCase()));
|
||||||
|
List<String> 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<String, List<AppUser>> usersByDisplayName,
|
||||||
|
String fallbackAccountId
|
||||||
|
) {
|
||||||
|
if (sample.createdByDisplayName() != null && !sample.createdByDisplayName().isBlank()) {
|
||||||
|
List<String> 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) {
|
private SampleSummary toSummary(Sample sample) {
|
||||||
return new SampleSummary(
|
return new SampleSummary(
|
||||||
sample.id(),
|
sample.id(),
|
||||||
|
|||||||
@@ -32,27 +32,27 @@ public class CatalogController {
|
|||||||
|
|
||||||
@GetMapping("/admin")
|
@GetMapping("/admin")
|
||||||
public CatalogService.AdministrationOverview administrationOverview() {
|
public CatalogService.AdministrationOverview administrationOverview() {
|
||||||
return catalogService.administrationOverview();
|
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> saveFarmers(@RequestBody List<CatalogService.FarmerMutation> mutations) {
|
||||||
return catalogService.saveFarmers(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> saveMedications(@RequestBody List<CatalogService.MedicationMutation> mutations) {
|
||||||
return catalogService.saveMedications(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> savePathogens(@RequestBody List<CatalogService.PathogenMutation> mutations) {
|
||||||
return catalogService.savePathogens(mutations);
|
return catalogService.savePathogens(securitySupport.currentUser().id(), mutations);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/admin/antibiotics")
|
@PostMapping("/admin/antibiotics")
|
||||||
public List<CatalogService.AntibioticRow> saveAntibiotics(@RequestBody List<CatalogService.AntibioticMutation> mutations) {
|
public List<CatalogService.AntibioticRow> saveAntibiotics(@RequestBody List<CatalogService.AntibioticMutation> mutations) {
|
||||||
return catalogService.saveAntibiotics(mutations);
|
return catalogService.saveAntibiotics(securitySupport.currentUser().id(), mutations);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/portal/users")
|
@GetMapping("/portal/users")
|
||||||
|
|||||||
@@ -58,12 +58,12 @@ public class PortalController {
|
|||||||
|
|
||||||
@GetMapping("/reports")
|
@GetMapping("/reports")
|
||||||
public List<ReportService.ReportCandidate> reports() {
|
public List<ReportService.ReportCandidate> reports() {
|
||||||
return reportService.reportCandidates();
|
return reportService.reportCandidates(securitySupport.currentUser().id());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/search/by-date")
|
@GetMapping("/search/by-date")
|
||||||
public List<PortalService.PortalSampleRow> searchByDate(@RequestParam LocalDate date) {
|
public List<PortalService.PortalSampleRow> searchByDate(@RequestParam LocalDate date) {
|
||||||
return portalService.searchSamplesByCreatedDate(date);
|
return portalService.searchSamplesByCreatedDate(securitySupport.currentUser().id(), date);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/reports/send")
|
@PostMapping("/reports/send")
|
||||||
@@ -73,12 +73,12 @@ public class PortalController {
|
|||||||
|
|
||||||
@PatchMapping("/reports/{sampleId}/block")
|
@PatchMapping("/reports/{sampleId}/block")
|
||||||
public SampleService.SampleDetail block(@PathVariable String sampleId, @RequestBody BlockRequest request) {
|
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")
|
@GetMapping("/reports/{sampleId}/pdf")
|
||||||
public ResponseEntity<byte[]> pdf(@PathVariable String sampleId) {
|
public ResponseEntity<byte[]> pdf(@PathVariable String sampleId) {
|
||||||
byte[] pdf = reportService.reportPdf(sampleId);
|
byte[] pdf = reportService.reportPdf(securitySupport.currentUser().id(), sampleId);
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.contentType(Objects.requireNonNull(MediaType.APPLICATION_PDF))
|
.contentType(Objects.requireNonNull(MediaType.APPLICATION_PDF))
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.inline()
|
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.inline()
|
||||||
|
|||||||
@@ -25,28 +25,28 @@ public class SampleController {
|
|||||||
|
|
||||||
@GetMapping("/dashboard")
|
@GetMapping("/dashboard")
|
||||||
public SampleService.DashboardOverview dashboardOverview() {
|
public SampleService.DashboardOverview dashboardOverview() {
|
||||||
return sampleService.dashboardOverview();
|
return sampleService.dashboardOverview(securitySupport.currentUser().id());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/dashboard/lookup/{sampleNumber}")
|
@GetMapping("/dashboard/lookup/{sampleNumber}")
|
||||||
public SampleService.LookupResult lookup(@PathVariable long sampleNumber) {
|
public SampleService.LookupResult lookup(@PathVariable long sampleNumber) {
|
||||||
return sampleService.lookup(sampleNumber);
|
return sampleService.lookup(securitySupport.currentUser().id(), sampleNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/samples/{id}")
|
@GetMapping("/samples/{id}")
|
||||||
public SampleService.SampleDetail sample(@PathVariable String id) {
|
public SampleService.SampleDetail sample(@PathVariable String id) {
|
||||||
return sampleService.getSample(id);
|
return sampleService.getSample(securitySupport.currentUser().id(), id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/samples/by-number/{sampleNumber}")
|
@GetMapping("/samples/by-number/{sampleNumber}")
|
||||||
public SampleService.SampleDetail sampleByNumber(@PathVariable long sampleNumber) {
|
public SampleService.SampleDetail sampleByNumber(@PathVariable long sampleNumber) {
|
||||||
return sampleService.getSampleByNumber(sampleNumber);
|
return sampleService.getSampleByNumber(securitySupport.currentUser().id(), sampleNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/samples")
|
@PostMapping("/samples")
|
||||||
public SampleService.SampleDetail create(@RequestBody SampleService.RegistrationRequest request) {
|
public SampleService.SampleDetail create(@RequestBody SampleService.RegistrationRequest request) {
|
||||||
AuthenticatedUser user = securitySupport.currentUser();
|
AuthenticatedUser user = securitySupport.currentUser();
|
||||||
return sampleService.createSample(new SampleService.RegistrationRequest(
|
return sampleService.createSample(user.id(), new SampleService.RegistrationRequest(
|
||||||
request.farmerBusinessKey(),
|
request.farmerBusinessKey(),
|
||||||
request.cowNumber(),
|
request.cowNumber(),
|
||||||
request.cowName(),
|
request.cowName(),
|
||||||
@@ -60,22 +60,22 @@ public class SampleController {
|
|||||||
|
|
||||||
@PutMapping("/samples/{id}/registration")
|
@PutMapping("/samples/{id}/registration")
|
||||||
public SampleService.SampleDetail saveRegistration(@PathVariable String id, @RequestBody SampleService.RegistrationRequest request) {
|
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")
|
@PutMapping("/samples/{id}/anamnesis")
|
||||||
public SampleService.SampleDetail saveAnamnesis(@PathVariable String id, @RequestBody SampleService.AnamnesisRequest request) {
|
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")
|
@PutMapping("/samples/{id}/antibiogram")
|
||||||
public SampleService.SampleDetail saveAntibiogram(@PathVariable String id, @RequestBody SampleService.AntibiogramRequest request) {
|
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")
|
@PutMapping("/samples/{id}/therapy")
|
||||||
public SampleService.SampleDetail saveTherapy(@PathVariable String id, @RequestBody SampleService.TherapyRequest request) {
|
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) {
|
private String deriveUserLabel(String displayName) {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ muh:
|
|||||||
cors:
|
cors:
|
||||||
allowed-origins: ${MUH_ALLOWED_ORIGINS:http://localhost:5173,http://localhost:3000}
|
allowed-origins: ${MUH_ALLOWED_ORIGINS:http://localhost:5173,http://localhost:3000}
|
||||||
security:
|
security:
|
||||||
token-secret: ${MUH_TOKEN_SECRET:change-me-in-production}
|
token-secret: ${MUH_TOKEN_SECRET:}
|
||||||
token-validity-hours: ${MUH_TOKEN_VALIDITY_HOURS:12}
|
token-validity-hours: ${MUH_TOKEN_VALIDITY_HOURS:12}
|
||||||
mongodb:
|
mongodb:
|
||||||
url: ${MUH_MONGODB_URL:mongodb://192.168.180.25:27017/muh}
|
url: ${MUH_MONGODB_URL:mongodb://192.168.180.25:27017/muh}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import UserManagementPage from "./pages/UserManagementPage";
|
|||||||
|
|
||||||
function ProtectedRoutes() {
|
function ProtectedRoutes() {
|
||||||
const { user, ready } = useSession();
|
const { user, ready } = useSession();
|
||||||
|
const isAdmin = user?.role === "ADMIN";
|
||||||
|
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
return <div className="empty-state">Sitzung wird geladen ...</div>;
|
return <div className="empty-state">Sitzung wird geladen ...</div>;
|
||||||
@@ -34,7 +35,7 @@ function ProtectedRoutes() {
|
|||||||
<Route path="/samples/:sampleId/anamnesis" element={<AnamnesisPage />} />
|
<Route path="/samples/:sampleId/anamnesis" element={<AnamnesisPage />} />
|
||||||
<Route path="/samples/:sampleId/antibiogram" element={<AntibiogramPage />} />
|
<Route path="/samples/:sampleId/antibiogram" element={<AntibiogramPage />} />
|
||||||
<Route path="/samples/:sampleId/therapy" element={<TherapyPage />} />
|
<Route path="/samples/:sampleId/therapy" element={<TherapyPage />} />
|
||||||
<Route path="/admin" element={<Navigate to="/admin/landwirte" replace />} />
|
<Route path="/admin" element={<Navigate to={isAdmin ? "/admin/landwirte" : "/admin/benutzer"} replace />} />
|
||||||
<Route path="/admin/benutzer" element={<UserManagementPage />} />
|
<Route path="/admin/benutzer" element={<UserManagementPage />} />
|
||||||
<Route path="/admin/landwirte" element={<AdministrationPage />} />
|
<Route path="/admin/landwirte" element={<AdministrationPage />} />
|
||||||
<Route path="/admin/medikamente" element={<AdministrationPage />} />
|
<Route path="/admin/medikamente" element={<AdministrationPage />} />
|
||||||
|
|||||||
@@ -21,19 +21,19 @@ function resolvePageTitle(pathname: string) {
|
|||||||
return "Probe bearbeiten";
|
return "Probe bearbeiten";
|
||||||
}
|
}
|
||||||
if (pathname.startsWith("/admin/landwirte")) {
|
if (pathname.startsWith("/admin/landwirte")) {
|
||||||
return "Verwaltung | Landwirte";
|
return "Die Verwaltung der Landwirte";
|
||||||
}
|
}
|
||||||
if (pathname.startsWith("/admin/benutzer")) {
|
if (pathname.startsWith("/admin/benutzer")) {
|
||||||
return "Verwaltung | Benutzer";
|
return "Verwaltung | Benutzer";
|
||||||
}
|
}
|
||||||
if (pathname.startsWith("/admin/medikamente")) {
|
if (pathname.startsWith("/admin/medikamente")) {
|
||||||
return "Verwaltung | Medikamente";
|
return "Die Verwaltung der Medikamente";
|
||||||
}
|
}
|
||||||
if (pathname.startsWith("/admin/erreger")) {
|
if (pathname.startsWith("/admin/erreger")) {
|
||||||
return "Verwaltung | Erreger";
|
return "Die Verwaltung der Erreger";
|
||||||
}
|
}
|
||||||
if (pathname.startsWith("/admin/antibiogramm")) {
|
if (pathname.startsWith("/admin/antibiogramm")) {
|
||||||
return "Verwaltung | Antibiogramm";
|
return "Die Verwaltung der Antibiogramme";
|
||||||
}
|
}
|
||||||
if (pathname.startsWith("/search/landwirt")) {
|
if (pathname.startsWith("/search/landwirt")) {
|
||||||
return "Suche | Landwirt";
|
return "Suche | Landwirt";
|
||||||
@@ -79,9 +79,6 @@ export default function AppShell() {
|
|||||||
<div className="nav-group">
|
<div className="nav-group">
|
||||||
<div className="nav-group__label">Verwaltung</div>
|
<div className="nav-group__label">Verwaltung</div>
|
||||||
<div className="nav-subnav">
|
<div className="nav-subnav">
|
||||||
<NavLink to="/admin/benutzer" className={({ isActive }) => `nav-sublink ${isActive ? "is-active" : ""}`}>
|
|
||||||
Benutzer
|
|
||||||
</NavLink>
|
|
||||||
<NavLink to="/admin/landwirte" className={({ isActive }) => `nav-sublink ${isActive ? "is-active" : ""}`}>
|
<NavLink to="/admin/landwirte" className={({ isActive }) => `nav-sublink ${isActive ? "is-active" : ""}`}>
|
||||||
Landwirte
|
Landwirte
|
||||||
</NavLink>
|
</NavLink>
|
||||||
@@ -94,6 +91,9 @@ export default function AppShell() {
|
|||||||
<NavLink to="/admin/antibiogramm" className={({ isActive }) => `nav-sublink ${isActive ? "is-active" : ""}`}>
|
<NavLink to="/admin/antibiogramm" className={({ isActive }) => `nav-sublink ${isActive ? "is-active" : ""}`}>
|
||||||
Antibiogramm
|
Antibiogramm
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink to="/admin/benutzer" className={({ isActive }) => `nav-sublink ${isActive ? "is-active" : ""}`}>
|
||||||
|
Benutzer
|
||||||
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ const DATASET_LABELS: Record<DatasetKey, string> = {
|
|||||||
antibiotics: "Antibiogramm",
|
antibiotics: "Antibiogramm",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DATASET_TITLES: Record<DatasetKey, string> = {
|
||||||
|
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 {
|
function normalizeOverview(overview: AdministrationOverview): DatasetsState {
|
||||||
return {
|
return {
|
||||||
farmers: overview.farmers.map((entry) => ({
|
farmers: overview.farmers.map((entry) => ({
|
||||||
@@ -216,7 +223,7 @@ export default function AdministrationPage() {
|
|||||||
<section className="section-card section-card--hero">
|
<section className="section-card section-card--hero">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Verwaltung</p>
|
<p className="eyebrow">Verwaltung</p>
|
||||||
<h3>Stammdaten direkt pflegen</h3>
|
<h3>{DATASET_TITLES[selectedDataset]}</h3>
|
||||||
<p className="muted-text">
|
<p className="muted-text">
|
||||||
Bestehende Datensaetze lassen sich inline aendern. Bei Umbenennungen bleibt der alte
|
Bestehende Datensaetze lassen sich inline aendern. Bei Umbenennungen bleibt der alte
|
||||||
Satz inaktiv sichtbar.
|
Satz inaktiv sichtbar.
|
||||||
|
|||||||
Reference in New Issue
Block a user