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
@@ -1,53 +1,38 @@
package org.example.projektarendehantering.infrastructure.security;

import jakarta.servlet.http.HttpServletRequest;
import org.example.projektarendehantering.common.Actor;
import org.example.projektarendehantering.common.NotAuthorizedException;
import org.example.projektarendehantering.common.Role;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.util.UUID;

/**
* Reads the current user identity from HTTP headers.
* <p>
* Expected headers:
* - {@code X-User-Id}: UUID string
* - {@code X-Role}: must match {@link Role} enum constant names exactly
* Bridges Spring Security authentication to the application's Actor model.
*/
@Component
public class HeaderCurrentUserAdapter {

private final HttpServletRequest request;

public HeaderCurrentUserAdapter(HttpServletRequest request) {
this.request = request;
}

public Actor currentUser() {
String userIdHeader = request.getHeader("X-User-Id");
String roleHeader = request.getHeader("X-Role");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (userIdHeader == null || userIdHeader.isBlank()) {
throw new NotAuthorizedException("Missing header: X-User-Id");
}
if (roleHeader == null || roleHeader.isBlank()) {
throw new NotAuthorizedException("Missing header: X-Role");
if (authentication == null || !authentication.isAuthenticated() || "anonymousUser".equals(authentication.getName())) {
throw new NotAuthorizedException("User not authenticated");
}

UUID userId;
try {
userId = UUID.fromString(userIdHeader);
} catch (IllegalArgumentException e) {
throw new NotAuthorizedException("Invalid header: X-User-Id");
}
// Create a deterministic UUID based on the username/name
UUID userId = UUID.nameUUIDFromBytes(authentication.getName().getBytes(StandardCharsets.UTF_8));

Role role;
try {
role = Role.valueOf(roleHeader);
} catch (IllegalArgumentException e) {
// Role.valueOf requires an exact match to enum constant names.
throw new NotAuthorizedException("Invalid header: X-Role");
Role role = Role.OTHER;
if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
role = Role.ADMIN;
} else if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_HANDLER"))) {
role = Role.HANDLER;
} else if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_CASE_OWNER"))) {
role = Role.CASE_OWNER;
}

return new Actor(userId, role);
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/cases/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ <h2>Notes</h2>
<p th:text="${note.content}">Note content</p>
<div class="muted" style="font-size: 0.8em;">
<span th:text="${note.author}">Author</span> -
<span th:text="${#instants.format(note.createdAt, 'yyyy-MM-dd HH:mm')}">Date</span>
<span th:if="${note.createdAt != null}" th:text="${note.createdAt}">Date</span>
</div>
</div>
<p th:if="${#lists.isEmpty(case.notes)}" class="muted">No notes yet.</p>
Expand Down