Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -182,6 +198,11 @@ public List<CaseDTO> 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");
}

Expand Down Expand Up @@ -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");
}

Expand All @@ -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;
}

Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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);
Expand All @@ -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");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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() {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

// Something wierd

Comment on lines +14 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove the stray debug comment.

// Something wierd appears to be an unintended artifact (also misspelled — "weird"). Please delete before merging; it adds noise and is unrelated to this PR's scope.

Proposed fix
-// Something wierd
-
 `@Controller`
 public class PatientUiController {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Something wierd
`@Controller`
public class PatientUiController {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/projektarendehantering/presentation/web/PatientUiController.java`
around lines 14 - 15, Remove the stray debug comment "// Something wierd" from
the PatientUiController file; locate the PatientUiController class (or its
top-of-file comments/imports) and delete that comment to avoid noise and correct
the typo (weird).

@Controller
public class PatientUiController {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,7 +23,6 @@
import jakarta.validation.Valid;
import org.springframework.web.server.ResponseStatusException;

import java.security.Principal;
import java.util.UUID;

@Controller
Expand Down Expand Up @@ -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";
}

Expand Down Expand Up @@ -156,3 +167,5 @@ public String assignUsers(@PathVariable UUID caseId, @RequestParam(value = "owne
}
}



5 changes: 4 additions & 1 deletion src/main/resources/templates/cases/new.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ <h1>Create case</h1>
</div>

<form class="form" th:action="@{/ui/cases/new}" th:object="${createCaseForm}" method="post">
<label class="field">
<label class="field" th:if="${currentActor == null || currentActor.role() == null || currentActor.role().toString() != 'PATIENT'}">
<span class="label">Patient</span>
<select class="input" th:field="*{patientId}">
<option value="">-- Select Patient --</option>
<option th:each="p : ${patients}" th:value="${p.id}" th:text="${p.firstName + ' ' + p.lastName}"></option>
</select>
<span class="muted" th:if="${#fields.hasErrors('patientId')}" th:errors="*{patientId}" style="color: var(--danger)">Error</span>
</label>
<p class="muted" th:if="${currentActor != null && currentActor.role() != null && currentActor.role().toString() == 'PATIENT'}">
Case will automatically be created for your patient profile.
</p>
<label class="field">
<span class="label">Title</span>
<input class="input" th:field="*{title}" maxlength="200"/>
Expand Down
17 changes: 16 additions & 1 deletion src/main/resources/templates/login/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,24 @@ <h4>Create a Patient Account</h4>
<p th:each="err : ${#fields.globalErrors()}" th:text="${err}"></p>
</div>

<div class="field">
<label for="firstName" class="label">First Name</label>
<input type="text" id="firstName" th:field="*{firstName}" class="input" required autofocus>
<span class="muted" style="color: var(--danger)" th:if="${#fields.hasErrors('firstName')}" th:errors="*{firstName}"></span>
</div>
<div class="field">
<label for="lastName" class="label">Last Name</label>
<input type="text" id="lastName" th:field="*{lastName}" class="input" required>
<span class="muted" style="color: var(--danger)" th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}"></span>
</div>
<div class="field">
<label for="personalIdentityNumber" class="label">Personal Identity Number</label>
<input type="text" id="personalIdentityNumber" th:field="*{personalIdentityNumber}" class="input" placeholder="YYYYMMDD-XXXX" required>
<span class="muted" style="color: var(--danger)" th:if="${#fields.hasErrors('personalIdentityNumber')}" th:errors="*{personalIdentityNumber}"></span>
</div>
<div class="field">
<label for="email" class="label">Email</label>
<input type="email" id="email" th:field="*{email}" class="input" required autofocus>
<input type="email" id="email" th:field="*{email}" class="input" required>
<span class="muted" style="color: var(--danger)" th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></span>
</div>
<div class="field">
Expand Down
Loading