diff --git a/src/main/java/net/siegeln/cameleer/saas/account/AccountController.java b/src/main/java/net/siegeln/cameleer/saas/account/AccountController.java new file mode 100644 index 0000000..957a377 --- /dev/null +++ b/src/main/java/net/siegeln/cameleer/saas/account/AccountController.java @@ -0,0 +1,114 @@ +package net.siegeln.cameleer.saas.account; + +import net.siegeln.cameleer.saas.account.AccountService.*; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/account") +public class AccountController { + + private final AccountService accountService; + + public AccountController(AccountService accountService) { + this.accountService = accountService; + } + + // --- Profile --- + + @GetMapping("/profile") + public ProfileData getProfile(@AuthenticationPrincipal Jwt jwt) { + return accountService.getProfile(jwt.getSubject()); + } + + @PatchMapping("/profile") + public ResponseEntity updateProfile(@AuthenticationPrincipal Jwt jwt, + @RequestBody Map body) { + String name = body.get("name"); + accountService.updateDisplayName(jwt.getSubject(), name); + return ResponseEntity.noContent().build(); + } + + // --- Password --- + + record PasswordChangeRequest(String currentPassword, String newPassword) {} + + @PostMapping("/password") + public ResponseEntity changePassword(@AuthenticationPrincipal Jwt jwt, + @RequestBody PasswordChangeRequest request) { + accountService.changePassword(jwt.getSubject(), request.currentPassword(), request.newPassword()); + return ResponseEntity.noContent().build(); + } + + // --- MFA --- + + @GetMapping("/mfa/status") + public MfaStatusData getMfaStatus(@AuthenticationPrincipal Jwt jwt) { + return accountService.getMfaStatus(jwt.getSubject()); + } + + @PostMapping("/mfa/totp/setup") + public MfaSetupData setupTotp(@AuthenticationPrincipal Jwt jwt) { + return accountService.setupTotp(jwt.getSubject()); + } + + record TotpVerifyRequest(String secret, String code) {} + + @PostMapping("/mfa/totp/verify") + public Map verifyTotp(@AuthenticationPrincipal Jwt jwt, + @RequestBody TotpVerifyRequest request) { + boolean ok = accountService.verifyTotpCode(request.secret(), request.code()); + return Map.of("verified", ok); + } + + @PostMapping("/mfa/backup-codes") + public BackupCodesData generateBackupCodes(@AuthenticationPrincipal Jwt jwt) { + return accountService.generateBackupCodes(jwt.getSubject()); + } + + @DeleteMapping("/mfa/totp") + public ResponseEntity removeTotp(@AuthenticationPrincipal Jwt jwt) { + accountService.removeMfa(jwt.getSubject()); + return ResponseEntity.noContent().build(); + } + + // --- Passkeys --- + + @GetMapping("/mfa/webauthn") + public List listPasskeys(@AuthenticationPrincipal Jwt jwt) { + return accountService.listPasskeys(jwt.getSubject()); + } + + @PatchMapping("/mfa/webauthn/{id}/name") + public ResponseEntity renamePasskey(@AuthenticationPrincipal Jwt jwt, + @PathVariable String id, + @RequestBody Map body) { + String name = body.get("name"); + if (name == null || name.isBlank()) { + return ResponseEntity.badRequest().build(); + } + accountService.renamePasskey(jwt.getSubject(), id, name.trim()); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/mfa/webauthn/{id}") + public ResponseEntity deletePasskey(@AuthenticationPrincipal Jwt jwt, + @PathVariable String id) { + accountService.deletePasskey(jwt.getSubject(), id); + return ResponseEntity.noContent().build(); + } + + // --- MFA Preference --- + + @PostMapping("/mfa/method-preference") + public ResponseEntity setMfaPreference(@AuthenticationPrincipal Jwt jwt, + @RequestBody Map body) { + accountService.setMfaMethodPreference(jwt.getSubject(), body.get("preference")); + return ResponseEntity.noContent().build(); + } +}