diff --git a/src/main/java/org/example/projektarendehantering/application/service/CaseService.java b/src/main/java/org/example/projektarendehantering/application/service/CaseService.java index 58265ff..16e7f35 100644 --- a/src/main/java/org/example/projektarendehantering/application/service/CaseService.java +++ b/src/main/java/org/example/projektarendehantering/application/service/CaseService.java @@ -71,13 +71,29 @@ public CaseDTO createCase(Actor actor, CaseDTO caseDTO) { if (!canCreate(actor)) { throw new NotAuthorizedException("Not allowed to create cases"); } - if (caseDTO.getPatientId() == null) { + UUID requestedPatientId = caseDTO.getPatientId(); + if (isPatient(actor)) { + if (requestedPatientId != null && !actor.userId().equals(requestedPatientId)) { + throw new NotAuthorizedException("Patients can only create cases for themselves"); + } + requestedPatientId = actor.userId(); + } + if (requestedPatientId == null) { throw new BadRequestException("patientId is required"); } CaseEntity entity = caseMapper.toEntity(caseDTO); entity.setId(UUID.randomUUID()); - PatientEntity patient = patientRepository.findById(caseDTO.getPatientId()) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Patient not found")); + PatientEntity patient = patientRepository.findById(requestedPatientId).orElse(null); + if (patient == null) { + if (isPatient(actor) && actor.userId().equals(requestedPatientId)) { + PatientEntity self = new PatientEntity(); + self.setId(requestedPatientId); + self.setCreatedAt(Instant.now()); + patient = patientRepository.save(self); + } else { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Patient not found"); + } + } entity.setPatient(patient); if (isDoctor(actor) || isManager(actor)) { entity.setOwnerId(actor.userId()); @@ -182,6 +198,11 @@ public List getAllCases(Actor actor) { .map(caseMapper::toDTO) .collect(Collectors.toList()); } + if (isPatient(actor)) { + return caseRepository.findAllByPatient_IdAndStatusNot(actor.userId(), CaseStatus.CLOSED).stream() + .map(caseMapper::toDTO) + .collect(Collectors.toList()); + } throw new NotAuthorizedException("Not allowed to list cases"); } @@ -254,6 +275,7 @@ private void requireCanRead(Actor actor, CaseEntity entity) { if (isManager(actor)) return; if (isDoctor(actor) && actor.userId().equals(entity.getOwnerId())) return; if (isNurse(actor) && actor.userId().equals(entity.getHandlerId())) return; + if (isPatient(actor) && entity.getPatient() != null && actor.userId().equals(entity.getPatient().getId())) return; throw new NotAuthorizedException("Not allowed to read this case"); } @@ -276,13 +298,14 @@ private void requireCanDelete(Actor actor, CaseEntity entity) { } private boolean canCreate(Actor actor) { - return isManager(actor) || isDoctor(actor); + return isManager(actor) || isDoctor(actor) || isPatient(actor); } private boolean canRead(Actor actor, CaseEntity entity) { if (isManager(actor)) return true; if (isDoctor(actor) && actor.userId().equals(entity.getOwnerId())) return true; if (isNurse(actor) && actor.userId().equals(entity.getHandlerId())) return true; + if (isPatient(actor) && entity.getPatient() != null && actor.userId().equals(entity.getPatient().getId())) return true; return false; } @@ -298,6 +321,10 @@ private boolean isNurse(Actor actor) { return actor.role() == Role.NURSE; } + private boolean isPatient(Actor actor) { + return actor.role() == Role.PATIENT; + } + private void recordStatusChange(Actor actor, UUID caseId, CaseStatus from, CaseStatus to) { String statusChange = (from != null ? from.name() : "NEW") + " -> " + to.name(); AuditEventEntity event = AuditEventEntity.builder() diff --git a/src/main/java/org/example/projektarendehantering/application/service/RegistrationService.java b/src/main/java/org/example/projektarendehantering/application/service/RegistrationService.java index 2365de2..d7a6c8e 100644 --- a/src/main/java/org/example/projektarendehantering/application/service/RegistrationService.java +++ b/src/main/java/org/example/projektarendehantering/application/service/RegistrationService.java @@ -4,6 +4,8 @@ import org.example.projektarendehantering.common.BadRequestException; import org.example.projektarendehantering.common.Role; import org.example.projektarendehantering.infrastructure.persistence.AuthProvider; +import org.example.projektarendehantering.infrastructure.persistence.PatientEntity; +import org.example.projektarendehantering.infrastructure.persistence.PatientRepository; import org.example.projektarendehantering.infrastructure.persistence.UserAccountEntity; import org.example.projektarendehantering.infrastructure.persistence.UserAccountRepository; import org.example.projektarendehantering.presentation.dto.PatientRegistrationDTO; @@ -12,11 +14,14 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Locale; + @Service @RequiredArgsConstructor public class RegistrationService { private final UserAccountRepository userAccountRepository; + private final PatientRepository patientRepository; private final PasswordEncoder passwordEncoder; @@ -27,11 +32,17 @@ public void registerPatient(PatientRegistrationDTO registrationDTO) { throw new BadRequestException("PASSWORDS_DO_NOT_MATCH", "Passwords do not match"); } - var normalizedEmail = registrationDTO.getEmail().trim().toLowerCase(); + var normalizedEmail = registrationDTO.getEmail().trim().toLowerCase(Locale.ROOT); + var firstName = registrationDTO.getFirstName().trim(); + var lastName = registrationDTO.getLastName().trim(); + var personalIdentityNumber = registrationDTO.getPersonalIdentityNumber().trim(); userAccountRepository.findByEmail(normalizedEmail).ifPresent(user -> { throw new BadRequestException("USER_EXISTS", "A user with this email already exists"); }); + patientRepository.findByPersonalIdentityNumber(personalIdentityNumber).ifPresent(existing -> { + throw new BadRequestException("PATIENT_EXISTS", "A patient with this personal identity number already exists"); + }); var newUserAccount = new UserAccountEntity(); newUserAccount.setEmail(normalizedEmail); @@ -40,10 +51,24 @@ public void registerPatient(PatientRegistrationDTO registrationDTO) { newUserAccount.setProvider(AuthProvider.LOCAL); newUserAccount.setEnabled(true); + UserAccountEntity savedAccount; try { - userAccountRepository.save(newUserAccount); + savedAccount = userAccountRepository.save(newUserAccount); } catch (DataIntegrityViolationException ex) { throw new BadRequestException("USER_EXISTS", "A user with this email already exists"); } + + PatientEntity patientEntity = new PatientEntity(); + patientEntity.setId(savedAccount.getId()); + patientEntity.setFirstName(firstName); + patientEntity.setLastName(lastName); + patientEntity.setPersonalIdentityNumber(personalIdentityNumber); + patientEntity.setCreatedAt(savedAccount.getCreatedAt()); + + try { + patientRepository.save(patientEntity); + } catch (DataIntegrityViolationException ex) { + throw new BadRequestException("PATIENT_EXISTS", "A patient with this personal identity number already exists"); + } } } diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapter.java b/src/main/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapter.java index 58c8df3..377df4a 100644 --- a/src/main/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapter.java +++ b/src/main/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapter.java @@ -4,12 +4,14 @@ import org.example.projektarendehantering.common.NotAuthorizedException; import org.example.projektarendehantering.common.Role; import org.example.projektarendehantering.infrastructure.persistence.EmployeeRepository; +import org.example.projektarendehantering.infrastructure.persistence.UserAccountRepository; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; +import java.util.Locale; import java.util.UUID; /** @@ -19,9 +21,11 @@ public class SecurityActorAdapter { private final EmployeeRepository employeeRepository; + private final UserAccountRepository userAccountRepository; - public SecurityActorAdapter(EmployeeRepository employeeRepository) { + public SecurityActorAdapter(EmployeeRepository employeeRepository, UserAccountRepository userAccountRepository) { this.employeeRepository = employeeRepository; + this.userAccountRepository = userAccountRepository; } public Actor currentUser() { @@ -43,14 +47,21 @@ public Actor currentUser() { // Create a deterministic UUID based on the username/name UUID userId = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8)); - // 1. Try finding an employee with this UUID + // 1. Try finding an employee with this UUID (OAuth/GitHub users) var employee = employeeRepository.findById(userId); if (employee.isPresent()) { var e = employee.get(); return new Actor(userId, e.getRole(), e.getDisplayName(), e.getGithubUsername()); } - // 2. Fallback to existing logic (checking Spring authorities) + // 2. Try local account by authentication name (email) and use persisted id + var userAccount = userAccountRepository.findByEmail(authentication.getName().trim().toLowerCase(Locale.ROOT)); + if (userAccount.isPresent()) { + var account = userAccount.get(); + return new Actor(account.getId(), account.getRole(), null, account.getEmail()); + } + + // 3. Fallback to Spring authorities for any remaining auth type Role role = Role.PENDING; if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_MANAGER"))) { role = Role.MANAGER; diff --git a/src/main/java/org/example/projektarendehantering/presentation/dto/CreateCaseForm.java b/src/main/java/org/example/projektarendehantering/presentation/dto/CreateCaseForm.java index 159681f..9f40374 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/dto/CreateCaseForm.java +++ b/src/main/java/org/example/projektarendehantering/presentation/dto/CreateCaseForm.java @@ -1,7 +1,6 @@ package org.example.projektarendehantering.presentation.dto; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Builder; @@ -24,7 +23,6 @@ public class CreateCaseForm { @Size(max = 4000, message = "Description must be under 4000 characters") private String description; - @NotNull(message = "Patient is required") private UUID patientId; } diff --git a/src/main/java/org/example/projektarendehantering/presentation/dto/PatientRegistrationDTO.java b/src/main/java/org/example/projektarendehantering/presentation/dto/PatientRegistrationDTO.java index 0d48c2a..ed50228 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/dto/PatientRegistrationDTO.java +++ b/src/main/java/org/example/projektarendehantering/presentation/dto/PatientRegistrationDTO.java @@ -1,6 +1,7 @@ package org.example.projektarendehantering.presentation.dto; import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; @@ -9,6 +10,16 @@ @Data public class PatientRegistrationDTO { + @NotBlank(message = "First name may not be empty") + private String firstName; + + @NotBlank(message = "Last name may not be empty") + private String lastName; + + @NotBlank(message = "Personal identity number may not be empty") + @Pattern(regexp = "^\\d{8}-\\d{4}$", message = "Use format YYYYMMDD-XXXX") + private String personalIdentityNumber; + @NotEmpty(message = "Email may not be empty") @Email(message = "Please provide a valid email") private String email; diff --git a/src/main/java/org/example/projektarendehantering/presentation/web/AuthController.java b/src/main/java/org/example/projektarendehantering/presentation/web/AuthController.java index 2c8b9dd..3e9a824 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/web/AuthController.java +++ b/src/main/java/org/example/projektarendehantering/presentation/web/AuthController.java @@ -17,6 +17,7 @@ @RequiredArgsConstructor public class AuthController { +// THis is a hidden message from an interdimensional potato council. Definitely not launch codes. private final RegistrationService registrationService; @GetMapping("/login") diff --git a/src/main/java/org/example/projektarendehantering/presentation/web/PatientUiController.java b/src/main/java/org/example/projektarendehantering/presentation/web/PatientUiController.java index 9b5e2e2..4db94b8 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/web/PatientUiController.java +++ b/src/main/java/org/example/projektarendehantering/presentation/web/PatientUiController.java @@ -11,6 +11,8 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; +// Something wierd + @Controller public class PatientUiController { diff --git a/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java b/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java index 3611853..a82fb9c 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java +++ b/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java @@ -8,6 +8,8 @@ import org.example.projektarendehantering.presentation.dto.CaseDTO; import org.example.projektarendehantering.presentation.dto.CreateCaseForm; import org.example.projektarendehantering.presentation.dto.UpdateCaseForm; +import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.Role; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -21,7 +23,6 @@ import jakarta.validation.Valid; import org.springframework.web.server.ResponseStatusException; -import java.security.Principal; import java.util.UUID; @Controller @@ -64,23 +65,33 @@ public String listClosedCases(Model model) { @GetMapping("/ui/cases/new") public String newCase(Model model) { + Actor actor = securityActorAdapter.currentUser(); model.addAttribute("createCaseForm", new CreateCaseForm()); - model.addAttribute("patients", patientService.getAllPatients()); + if (actor.role() != Role.PATIENT) { + model.addAttribute("patients", patientService.getAllPatients()); + } return "cases/new"; } @PostMapping("/ui/cases/new") - public String createCase(@Valid @ModelAttribute("createCaseForm") CreateCaseForm form, BindingResult result) { + public String createCase(@Valid @ModelAttribute("createCaseForm") CreateCaseForm form, BindingResult result, Model model) { + Actor actor = securityActorAdapter.currentUser(); + if (actor.role() != Role.PATIENT && form.getPatientId() == null) { + result.rejectValue("patientId", "required", "Patient is required"); + } if (result.hasErrors()) { + if (actor.role() != Role.PATIENT) { + model.addAttribute("patients", patientService.getAllPatients()); + } return "cases/new"; } CaseDTO caseDTO = new CaseDTO(); caseDTO.setTitle(form.getTitle()); caseDTO.setDescription(form.getDescription()); - caseDTO.setPatientId(form.getPatientId()); + caseDTO.setPatientId(actor.role() == Role.PATIENT ? actor.userId() : form.getPatientId()); - caseService.createCase(securityActorAdapter.currentUser(), caseDTO); + caseService.createCase(actor, caseDTO); return "redirect:/ui/cases"; } @@ -156,3 +167,5 @@ public String assignUsers(@PathVariable UUID caseId, @RequestParam(value = "owne } } + + \ No newline at end of file diff --git a/src/main/resources/templates/cases/new.html b/src/main/resources/templates/cases/new.html index 37a04ac..572680b 100644 --- a/src/main/resources/templates/cases/new.html +++ b/src/main/resources/templates/cases/new.html @@ -12,7 +12,7 @@

Create case

-