feat: vendor admin management and shared account settings #59
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