feat: vendor admin management and shared account settings #59

Merged
hsiegeln merged 19 commits from feature/vendor-admin-account-settings into main 2026-04-27 15:20:23 +02:00
2 changed files with 172 additions and 0 deletions
Showing only changes of commit 022b6d9722 - Show all commits

View 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();
}
}

View 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);
}
}