feat: create initial admin user + add vendor to new tenant orgs
All checks were successful
CI / build (push) Successful in 50s
CI / docker (push) Successful in 41s

When creating a tenant, the vendor can specify adminUsername +
adminPassword. The backend creates the user in Logto and assigns them
the owner org role. The vendor user is also auto-added to every new
org for support access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-10 07:35:17 +02:00
parent b7a0530466
commit 2dc75c4361
10 changed files with 114 additions and 22 deletions

View File

@@ -189,6 +189,42 @@ public class LogtoManagementClient {
}
}
/** Create a user with username/password and add to org with role. */
@SuppressWarnings("unchecked")
public String createUserWithPassword(String username, String password, String orgId, String roleId) {
if (!isAvailable()) return null;
try {
var userResp = (Map<String, Object>) restClient.post()
.uri(config.getLogtoEndpoint() + "/api/users")
.header("Authorization", "Bearer " + getAccessToken())
.contentType(MediaType.APPLICATION_JSON)
.body(Map.of("username", username, "password", password, "name", username))
.retrieve()
.body(Map.class);
String userId = String.valueOf(userResp.get("id"));
addUserToOrganization(orgId, userId);
if (roleId != null) {
assignOrganizationRole(orgId, userId, roleId);
}
log.info("Created user '{}' and added to org {} with role {}", username, orgId, roleId);
return userId;
} catch (Exception e) {
log.error("Failed to create user '{}': {}", username, e.getMessage());
throw new RuntimeException("User creation failed: " + e.getMessage(), e);
}
}
/** Find org role ID by name (e.g., "owner", "operator", "viewer"). */
@SuppressWarnings("unchecked")
public String findOrgRoleIdByName(String roleName) {
var roles = listOrganizationRoles();
return roles.stream()
.filter(r -> roleName.equals(r.get("name")))
.map(r -> String.valueOf(r.get("id")))
.findFirst()
.orElse(null);
}
/** List available organization roles. */
@SuppressWarnings("unchecked")
public List<Map<String, Object>> listOrganizationRoles() {

View File

@@ -7,5 +7,7 @@ import jakarta.validation.constraints.Size;
public record CreateTenantRequest(
@NotBlank @Size(max = 255) String name,
@NotBlank @Size(max = 100) @Pattern(regexp = "^[a-z0-9][a-z0-9-]*[a-z0-9]$", message = "Slug must be lowercase alphanumeric with hyphens") String slug,
String tier
String tier,
String adminUsername,
String adminPassword
) {}

View File

@@ -58,13 +58,40 @@ public class VendorTenantService {
@Transactional
public TenantEntity createAndProvision(CreateTenantRequest request, UUID actorId) {
// 1. Create tenant record (sets status = PROVISIONING)
// 1. Create tenant record (sets status = PROVISIONING) + Logto org
TenantEntity tenant = tenantService.create(request, actorId);
// 2. Generate license
// 2. Create initial admin user in Logto org (if credentials provided)
if (tenant.getLogtoOrgId() != null && logtoClient.isAvailable()) {
String ownerRoleId = logtoClient.findOrgRoleIdByName("owner");
// Create tenant admin
if (request.adminUsername() != null && request.adminPassword() != null) {
try {
logtoClient.createUserWithPassword(
request.adminUsername(), request.adminPassword(),
tenant.getLogtoOrgId(), ownerRoleId);
} catch (Exception e) {
log.warn("Failed to create admin user for tenant {}: {}", tenant.getSlug(), e.getMessage());
}
}
// Add the current vendor user to the new org for support access
try {
String vendorUserId = actorId.toString();
logtoClient.addUserToOrganization(tenant.getLogtoOrgId(), vendorUserId);
if (ownerRoleId != null) {
logtoClient.assignOrganizationRole(tenant.getLogtoOrgId(), vendorUserId, ownerRoleId);
}
} catch (Exception e) {
log.warn("Failed to add vendor to org for tenant {}: {}", tenant.getSlug(), e.getMessage());
}
}
// 3. Generate license
LicenseEntity license = licenseService.generateLicense(tenant, DEFAULT_LICENSE_VALIDITY, actorId);
// 3. Provision server if provisioner is available
// 4. Provision server if provisioner is available
if (tenantProvisioner.isAvailable()) {
var provisionRequest = new TenantProvisionRequest(
tenant.getId(), tenant.getSlug(),