feat: add vendor admin management (list, create/invite, remove, reset password/MFA)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
53
src/main/java/net/siegeln/cameleer/saas/vendor/VendorAdminController.java
vendored
Normal file
53
src/main/java/net/siegeln/cameleer/saas/vendor/VendorAdminController.java
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package net.siegeln.cameleer.saas.vendor;
|
||||||
|
|
||||||
|
import net.siegeln.cameleer.saas.vendor.VendorAdminService.CreateAdminRequest;
|
||||||
|
import net.siegeln.cameleer.saas.vendor.VendorAdminService.CreateAdminResponse;
|
||||||
|
import net.siegeln.cameleer.saas.vendor.VendorAdminService.VendorAdmin;
|
||||||
|
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/vendor/admins")
|
||||||
|
public class VendorAdminController {
|
||||||
|
|
||||||
|
private final VendorAdminService vendorAdminService;
|
||||||
|
|
||||||
|
public VendorAdminController(VendorAdminService vendorAdminService) {
|
||||||
|
this.vendorAdminService = vendorAdminService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<VendorAdmin> listAdmins() {
|
||||||
|
return vendorAdminService.listAdmins();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public CreateAdminResponse createAdmin(@RequestBody CreateAdminRequest request) {
|
||||||
|
return vendorAdminService.createAdmin(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{userId}")
|
||||||
|
public ResponseEntity<Void> removeAdmin(@AuthenticationPrincipal Jwt jwt,
|
||||||
|
@PathVariable String userId) {
|
||||||
|
vendorAdminService.removeAdmin(userId, jwt.getSubject());
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{userId}/reset-password")
|
||||||
|
public ResponseEntity<Void> resetPassword(@PathVariable String userId,
|
||||||
|
@RequestBody Map<String, String> body) {
|
||||||
|
vendorAdminService.resetAdminPassword(userId, body.get("password"));
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{userId}/mfa")
|
||||||
|
public ResponseEntity<Void> resetMfa(@PathVariable String userId) {
|
||||||
|
vendorAdminService.resetAdminMfa(userId);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
119
src/main/java/net/siegeln/cameleer/saas/vendor/VendorAdminService.java
vendored
Normal file
119
src/main/java/net/siegeln/cameleer/saas/vendor/VendorAdminService.java
vendored
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package net.siegeln.cameleer.saas.vendor;
|
||||||
|
|
||||||
|
import net.siegeln.cameleer.saas.account.AccountService;
|
||||||
|
import net.siegeln.cameleer.saas.identity.LogtoManagementClient;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class VendorAdminService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(VendorAdminService.class);
|
||||||
|
private static final String VENDOR_ROLE_NAME = "saas-vendor";
|
||||||
|
|
||||||
|
private final LogtoManagementClient logtoClient;
|
||||||
|
private final AccountService accountService;
|
||||||
|
private final EmailConnectorService emailConnectorService;
|
||||||
|
|
||||||
|
public VendorAdminService(LogtoManagementClient logtoClient,
|
||||||
|
AccountService accountService,
|
||||||
|
EmailConnectorService emailConnectorService) {
|
||||||
|
this.logtoClient = logtoClient;
|
||||||
|
this.accountService = accountService;
|
||||||
|
this.emailConnectorService = emailConnectorService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Records ---
|
||||||
|
|
||||||
|
public record VendorAdmin(String userId, String name, String email) {}
|
||||||
|
public record CreateAdminRequest(String email, String tempPassword) {}
|
||||||
|
public record CreateAdminResponse(boolean invited, String tempPassword) {}
|
||||||
|
|
||||||
|
// --- Methods ---
|
||||||
|
|
||||||
|
private String getVendorRoleId() {
|
||||||
|
var role = logtoClient.getRoleByName(VENDOR_ROLE_NAME);
|
||||||
|
if (role == null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
"Vendor role '" + VENDOR_ROLE_NAME + "' not found in Logto");
|
||||||
|
}
|
||||||
|
return String.valueOf(role.get("id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<VendorAdmin> listAdmins() {
|
||||||
|
String roleId = getVendorRoleId();
|
||||||
|
var users = logtoClient.listRoleUsers(roleId);
|
||||||
|
return users.stream()
|
||||||
|
.map(u -> new VendorAdmin(
|
||||||
|
String.valueOf(u.get("id")),
|
||||||
|
u.get("name") != null ? String.valueOf(u.get("name")) : "",
|
||||||
|
u.get("primaryEmail") != null ? String.valueOf(u.get("primaryEmail")) : ""
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CreateAdminResponse createAdmin(CreateAdminRequest request) {
|
||||||
|
if (request.email() == null || request.email().isBlank() || !request.email().contains("@")) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Valid email address required");
|
||||||
|
}
|
||||||
|
|
||||||
|
String roleId = getVendorRoleId();
|
||||||
|
boolean emailConfigured = emailConnectorService.getEmailConnector() != null;
|
||||||
|
|
||||||
|
String userId;
|
||||||
|
boolean invited;
|
||||||
|
String tempPassword = null;
|
||||||
|
|
||||||
|
if (emailConfigured && (request.tempPassword() == null || request.tempPassword().isBlank())) {
|
||||||
|
// Invite via email — no org needed for vendor (global role only)
|
||||||
|
// Create user with primaryEmail only; no org assignment
|
||||||
|
userId = logtoClient.createAndInviteUser(request.email(), null, null);
|
||||||
|
invited = true;
|
||||||
|
log.info("Invited vendor admin: {}", request.email());
|
||||||
|
} else {
|
||||||
|
// Create with temporary password
|
||||||
|
tempPassword = request.tempPassword();
|
||||||
|
if (tempPassword == null || tempPassword.isBlank()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||||
|
"Temporary password required when email connector is not configured");
|
||||||
|
}
|
||||||
|
accountService.validatePassword(tempPassword);
|
||||||
|
String username = request.email().substring(0, request.email().indexOf('@'));
|
||||||
|
userId = logtoClient.createUserWithPassword(username, tempPassword, null, null);
|
||||||
|
logtoClient.updateUserProfile(userId, Map.of("primaryEmail", request.email()));
|
||||||
|
invited = false;
|
||||||
|
log.info("Created vendor admin with credentials: {}", request.email());
|
||||||
|
}
|
||||||
|
|
||||||
|
logtoClient.assignGlobalRole(userId, roleId);
|
||||||
|
log.info("Assigned vendor role to user {}", userId);
|
||||||
|
|
||||||
|
return new CreateAdminResponse(invited, invited ? null : tempPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeAdmin(String userId, String requesterId) {
|
||||||
|
if (userId.equals(requesterId)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot remove yourself as administrator");
|
||||||
|
}
|
||||||
|
String roleId = getVendorRoleId();
|
||||||
|
logtoClient.revokeGlobalRole(userId, roleId);
|
||||||
|
log.info("Revoked vendor role from user {}", userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetAdminPassword(String userId, String newPassword) {
|
||||||
|
accountService.validatePassword(newPassword);
|
||||||
|
logtoClient.updateUserPassword(userId, newPassword);
|
||||||
|
log.info("Reset password for vendor admin {}", userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetAdminMfa(String userId) {
|
||||||
|
logtoClient.deleteAllMfaVerifications(userId);
|
||||||
|
log.info("Reset MFA for vendor admin {}", userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user