feat(timeline): add TimelineEventController CRUD endpoints
POST→201, PUT→200, DELETE→204, GET→200; @RequirePermission(WRITE_ALL) on the three writes, GET via global auth baseline (no annotation, documented). @Valid request body; all bodies are TimelineEventView. Injects UserService + private requireUserId wrapper. Controller slice tests cover 401/403/exact-status per verb, GET 404, service PERSON_NOT_FOUND→404, Bean-Validation 400s carrying code=VALIDATION_ERROR, and ArgumentCaptor proof that actorId is the resolved session principal (not a forged body field) on both write paths. Per #775. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package org.raddatz.familienarchiv.timeline;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.raddatz.familienarchiv.security.Permission;
|
||||
import org.raddatz.familienarchiv.security.RequirePermission;
|
||||
import org.raddatz.familienarchiv.security.SecurityUtils;
|
||||
import org.raddatz.familienarchiv.user.UserService;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/timeline/events")
|
||||
@RequiredArgsConstructor
|
||||
public class TimelineEventController {
|
||||
|
||||
private final TimelineEventService timelineEventService;
|
||||
private final UserService userService;
|
||||
|
||||
/**
|
||||
* No {@code @RequirePermission} on GET by design: the global {@code anyRequest().authenticated()}
|
||||
* rule is the READ_ALL baseline, consistent with {@code DocumentController.getDocument}. Do not
|
||||
* "fix" the missing annotation.
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public TimelineEventView getEvent(@PathVariable UUID id) {
|
||||
return timelineEventService.getEvent(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@RequirePermission(Permission.WRITE_ALL)
|
||||
public TimelineEventView create(@Valid @RequestBody TimelineEventRequest request, Authentication authentication) {
|
||||
return timelineEventService.create(request, requireUserId(authentication));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@RequirePermission(Permission.WRITE_ALL)
|
||||
public TimelineEventView update(
|
||||
@PathVariable UUID id,
|
||||
@Valid @RequestBody TimelineEventRequest request,
|
||||
Authentication authentication) {
|
||||
return timelineEventService.update(id, request, requireUserId(authentication));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@RequirePermission(Permission.WRITE_ALL)
|
||||
public ResponseEntity<Void> delete(@PathVariable UUID id) {
|
||||
timelineEventService.delete(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
private UUID requireUserId(Authentication authentication) {
|
||||
return SecurityUtils.requireUserId(authentication, userService);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user