> pending(
+ @RequestParam OrgType orgType,
+ @RequestParam UUID orgId) {
+ return ResponseEntity.ok(accessRequestService.pendingForOrg(orgType, orgId));
+ }
+
+ private UUID getCurrentUserId() {
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ if (auth == null || auth.getName() == null) {
+ throw new IllegalStateException("No authenticated user in SecurityContext");
+ }
+ return UUID.fromString(auth.getName());
+ }
+}
diff --git a/plate-auth-starter/src/main/java/de/platesoft/auth/controller/AdminAuditController.java b/plate-auth-starter/src/main/java/de/platesoft/auth/controller/AdminAuditController.java
new file mode 100644
index 0000000..969f2d0
--- /dev/null
+++ b/plate-auth-starter/src/main/java/de/platesoft/auth/controller/AdminAuditController.java
@@ -0,0 +1,50 @@
+package de.platesoft.auth.controller;
+
+import de.platesoft.auth.entity.LoginEvent;
+import de.platesoft.auth.repository.LoginEventRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Admin-only audit endpoints.
+ *
+ * Endpoints under {@code /api/admin/**} — enforced by
+ * {@code .requestMatchers("/api/admin/**").hasAuthority("ROLE_ADMIN")} in {@link
+ * de.platesoft.auth.config.SecurityConfig SecurityConfig}.
+ *
+ *
+ * - {@code GET /api/admin/login-events} — paginated login event audit log
+ *
+ *
+ * Note: The Envers revision browser ({@code GET /api/admin/audit/revisions}) is
+ * added in W11 alongside the {@code RevInfo}/{@code RevInfoListener} implementation.
+ */
+@RestController
+@RequestMapping("/api/admin")
+@RequiredArgsConstructor
+public class AdminAuditController {
+
+ private final LoginEventRepository loginEventRepository;
+
+ /**
+ * Paginated login event audit log. Newest first.
+ *
+ * @param page zero-based page index (default 0)
+ * @param size page size (default 50, max 200)
+ */
+ @GetMapping("/login-events")
+ public ResponseEntity> loginEvents(
+ @RequestParam(defaultValue = "0") int page,
+ @RequestParam(defaultValue = "50") int size) {
+ size = Math.min(size, 200); // cap to prevent excessive queries
+ PageRequest pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "id"));
+ return ResponseEntity.ok(loginEventRepository.findAll(pageable));
+ }
+}
diff --git a/plate-auth-starter/src/main/java/de/platesoft/auth/controller/AuthController.java b/plate-auth-starter/src/main/java/de/platesoft/auth/controller/AuthController.java
new file mode 100644
index 0000000..dd3366a
--- /dev/null
+++ b/plate-auth-starter/src/main/java/de/platesoft/auth/controller/AuthController.java
@@ -0,0 +1,120 @@
+package de.platesoft.auth.controller;
+
+import de.platesoft.auth.PlateAuthProperties;
+import de.platesoft.auth.dto.*;
+import de.platesoft.auth.service.AuthService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * REST controller for password-based auth endpoints.
+ *
+ * Endpoints mounted under {@code /api/auth/**} (scoped by plate-auth's SecurityFilterChain):
+ *
+ * - {@code POST /api/auth/login} — password login (public)
+ * - {@code POST /api/auth/register} — password registration (public, if enabled)
+ * - {@code POST /api/auth/refresh} — refresh token rotation (public)
+ * - {@code GET /api/auth/me} — current user + memberships (authenticated)
+ * - {@code GET /api/auth/config} — provider list + registration flag (public)
+ *
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/auth")
+@RequiredArgsConstructor
+public class AuthController {
+
+ private final AuthService authService;
+ private final PlateAuthProperties props;
+
+ @PostMapping("/login")
+ public ResponseEntity login(@Valid @RequestBody LoginRequest req,
+ HttpServletRequest request) {
+ TokenResponse tokens = authService.login(
+ req.email(), req.password(),
+ request.getRemoteAddr(), request.getHeader("User-Agent"));
+ return ResponseEntity.ok(tokens);
+ }
+
+ @PostMapping("/register")
+ public ResponseEntity register(@Valid @RequestBody RegisterRequest req,
+ HttpServletRequest request) {
+ if (!props.getRegistration().isEnabled()) {
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
+ }
+ TokenResponse tokens = authService.register(
+ req.email(), req.password(), req.firstName(), req.lastName(),
+ request.getRemoteAddr(), request.getHeader("User-Agent"));
+ return ResponseEntity.status(HttpStatus.CREATED).body(tokens);
+ }
+
+ @PostMapping("/refresh")
+ public ResponseEntity refresh(@Valid @RequestBody RefreshRequest req) {
+ TokenResponse tokens = authService.refresh(req.refreshToken());
+ return ResponseEntity.ok(tokens);
+ }
+
+ @GetMapping("/me")
+ public ResponseEntity me() {
+ UUID userId = getCurrentUserId();
+ var info = authService.getCurrentUser(userId);
+ return ResponseEntity.ok(UserResponse.from(info.user(), info.memberships()));
+ }
+
+ @GetMapping("/config")
+ public ResponseEntity config() {
+ List providers = new ArrayList<>();
+ providers.add(AuthConfigResponse.ProviderInfo.google());
+ providers.add(AuthConfigResponse.ProviderInfo.microsoft(
+ props.getProviders().getMicrosoft().isEnabled()));
+ providers.add(AuthConfigResponse.ProviderInfo.emailMagicLink(
+ props.getProviders().getEmailMagicLink().isEnabled()));
+
+ return ResponseEntity.ok(new AuthConfigResponse(
+ props.getRegistration().isEnabled(),
+ providers
+ ));
+ }
+
+ // ── Helpers ───────────────────────────────────────────────────────────────
+
+ /**
+ * Extract the authenticated user's UUID from the SecurityContext.
+ * The {@link de.platesoft.auth.filter.JwtAuthenticationFilter} sets the principal
+ * to the userId as a string.
+ */
+ private UUID getCurrentUserId() {
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ if (auth == null || auth.getName() == null) {
+ throw new IllegalStateException("No authenticated user in SecurityContext");
+ }
+ return UUID.fromString(auth.getName());
+ }
+
+ // ── Exception handlers ────────────────────────────────────────────────────
+
+ @ExceptionHandler(AuthService.BadCredentialsException.class)
+ public ResponseEntity handleBadCredentials(AuthService.BadCredentialsException e) {
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
+ }
+
+ @ExceptionHandler(IllegalStateException.class)
+ public ResponseEntity handleIllegalState(IllegalStateException e) {
+ // Registration disabled or email already in use
+ if (e.getMessage().contains("Registration is disabled")) {
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage());
+ }
+ return ResponseEntity.status(HttpStatus.CONFLICT).body(e.getMessage());
+ }
+}
diff --git a/plate-auth-starter/src/main/java/de/platesoft/auth/controller/InvitationController.java b/plate-auth-starter/src/main/java/de/platesoft/auth/controller/InvitationController.java
new file mode 100644
index 0000000..7eb1df4
--- /dev/null
+++ b/plate-auth-starter/src/main/java/de/platesoft/auth/controller/InvitationController.java
@@ -0,0 +1,86 @@
+package de.platesoft.auth.controller;
+
+import de.platesoft.auth.dto.CreateInvitationRequest;
+import de.platesoft.auth.entity.Invitation;
+import de.platesoft.auth.entity.Membership;
+import de.platesoft.auth.entity.OrgType;
+import de.platesoft.auth.entity.User;
+import de.platesoft.auth.repository.UserRepository;
+import de.platesoft.auth.service.InvitationService;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * REST controller for invitation management.
+ *
+ * Endpoints under {@code /api/invitations/**}:
+ *
+ * - {@code POST /api/invitations} — create invitation (authenticated)
+ * - {@code POST /api/invitations/accept} — accept invitation (authenticated, with token)
+ * - {@code DELETE /api/invitations/{id}} — revoke invitation (authenticated)
+ * - {@code GET /api/invitations} — list pending invitations (authenticated)
+ *
+ */
+@RestController
+@RequestMapping("/api/invitations")
+@RequiredArgsConstructor
+public class InvitationController {
+
+ private final InvitationService invitationService;
+ private final UserRepository userRepository;
+
+ @PostMapping
+ public ResponseEntity create(@Valid @RequestBody CreateInvitationRequest req) {
+ UUID createdBy = getCurrentUserId();
+ Invitation invitation = invitationService.createInvitation(
+ req.email(), req.orgType(), req.orgId(), req.role(), createdBy);
+ return ResponseEntity.status(HttpStatus.CREATED).body(invitation);
+ }
+
+ @PostMapping("/accept")
+ public ResponseEntity