feat(auth): revoke other sessions on password change; add force-logout endpoint

changePassword now calls authService.revokeOtherSessions() after the
password is updated and emits a LOGOUT audit with reason=password_change.

POST /api/users/{id}/force-logout (ADMIN_USER permission) revokes all
sessions for the target user and emits ADMIN_FORCE_LOGOUT audit. Returns
{"revokedCount": N} with 200.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-18 12:43:19 +02:00
committed by marcel
parent 7d10653c41
commit 1b178767ab
2 changed files with 95 additions and 0 deletions

View File

@@ -4,7 +4,11 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import org.raddatz.familienarchiv.audit.AuditKind;
import org.raddatz.familienarchiv.audit.AuditService;
import org.raddatz.familienarchiv.auth.AuthService;
import org.raddatz.familienarchiv.user.AdminUpdateUserRequest;
import org.raddatz.familienarchiv.user.ChangePasswordDTO;
import org.raddatz.familienarchiv.user.CreateUserRequest;
@@ -33,6 +37,8 @@ import lombok.AllArgsConstructor;
@AllArgsConstructor
public class UserController {
private UserService userService;
private AuthService authService;
private AuditService auditService;
@GetMapping("users/me")
public ResponseEntity<AppUser> getCurrentUser(Authentication authentication) {
@@ -56,9 +62,14 @@ public class UserController {
@PostMapping("users/me/password")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void changePassword(Authentication authentication,
HttpSession session,
@RequestBody ChangePasswordDTO dto) {
AppUser current = userService.findByEmail(authentication.getName());
userService.changePassword(current.getId(), dto);
int revoked = authService.revokeOtherSessions(session.getId(), authentication.getName());
auditService.log(AuditKind.LOGOUT, current.getId(), null, Map.of(
"reason", "password_change",
"revokedCount", revoked));
}
@GetMapping("users/{id}")
@@ -101,6 +112,18 @@ public class UserController {
return ResponseEntity.ok().build();
}
@PostMapping("/users/{id}/force-logout")
@RequirePermission(Permission.ADMIN_USER)
public ResponseEntity<Map<String, Object>> forceLogout(Authentication authentication,
@PathVariable UUID id) {
AppUser target = userService.getById(id);
int revoked = authService.revokeAllSessions(target.getEmail());
auditService.log(AuditKind.ADMIN_FORCE_LOGOUT, actorId(authentication), null, Map.of(
"targetUserId", target.getId().toString(),
"revokedCount", revoked));
return ResponseEntity.ok(Map.of("revokedCount", revoked));
}
private UUID actorId(Authentication auth) {
return userService.findByEmail(auth.getName()).getId();
}