fix: team invite role resolution, user cleanup, and settings page redesign
All checks were successful
CI / build (push) Successful in 2m9s
CI / docker (push) Successful in 1m33s

- Resolve org role names to Logto role IDs in invite and role change flows
  (fixes entity.relation_foreign_key_not_found on invite)
- Handle existing Logto users on re-invite instead of failing with
  email_already_in_use
- Delete users from Logto when removed from last org membership
- Consolidate tenant settings page into 3 cards: Tenant Details, MFA,
  Authentication Policy — remove duplicate MFA Enforcement and Change
  Password (now in Account Settings)
- Make passkey list scrollable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-27 22:36:21 +02:00
parent e21a9d6046
commit 7fc8a4d407
5 changed files with 148 additions and 156 deletions

View File

@@ -209,19 +209,67 @@ public class LogtoManagementClient {
}
}
/** Create a user in Logto and add to organization with role. */
/** Delete a user from Logto entirely. */
public void deleteUser(String userId) {
if (!isAvailable() || userId == null) return;
try {
restClient.delete()
.uri(config.getLogtoEndpoint() + "/api/users/" + userId)
.header("Authorization", "Bearer " + getAccessToken())
.retrieve()
.toBodilessEntity();
log.info("Deleted user {} from Logto", userId);
} catch (Exception e) {
log.warn("Failed to delete user {}: {}", userId, e.getMessage());
}
}
/** Find a user by email. Returns user map or null if not found. */
@SuppressWarnings("unchecked")
public Map<String, Object> findUserByEmail(String email) {
if (!isAvailable() || email == null) return null;
try {
var resp = restClient.get()
.uri(config.getLogtoEndpoint() + "/api/users?search="
+ java.net.URLEncoder.encode(email, java.nio.charset.StandardCharsets.UTF_8)
+ "&search.primaryEmail="
+ java.net.URLEncoder.encode(email, java.nio.charset.StandardCharsets.UTF_8)
+ "&page_size=5")
.header("Authorization", "Bearer " + getAccessToken())
.retrieve()
.body(List.class);
if (resp == null) return null;
return ((List<Map<String, Object>>) resp).stream()
.filter(u -> email.equalsIgnoreCase(String.valueOf(u.get("primaryEmail"))))
.findFirst()
.orElse(null);
} catch (Exception e) {
log.warn("Failed to find user by email: {}", e.getMessage());
return null;
}
}
/** Create a user in Logto (or find existing by email) and add to organization with role. */
@SuppressWarnings("unchecked")
public String createAndInviteUser(String email, 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("primaryEmail", email, "name", email.split("@")[0]))
.retrieve()
.body(Map.class);
String userId = String.valueOf(userResp.get("id"));
String userId;
// Check if user already exists in Logto
var existing = findUserByEmail(email);
if (existing != null) {
userId = String.valueOf(existing.get("id"));
log.info("User '{}' already exists in Logto ({}), adding to org", email, userId);
} else {
var userResp = (Map<String, Object>) restClient.post()
.uri(config.getLogtoEndpoint() + "/api/users")
.header("Authorization", "Bearer " + getAccessToken())
.contentType(MediaType.APPLICATION_JSON)
.body(Map.of("primaryEmail", email, "name", email.split("@")[0]))
.retrieve()
.body(Map.class);
userId = String.valueOf(userResp.get("id"));
}
if (orgId != null) {
addUserToOrganization(orgId, userId);
if (roleId != null) {

View File

@@ -181,13 +181,14 @@ public class TenantPortalService {
return logtoClient.listOrganizationMembers(orgId);
}
public String inviteTeamMember(String email, String roleId) {
public String inviteTeamMember(String email, String roleName) {
TenantEntity tenant = resolveTenant();
String orgId = tenant.getLogtoOrgId();
if (orgId == null || orgId.isBlank()) {
throw new IllegalStateException("Tenant has no Logto organization configured");
}
return logtoClient.createAndInviteUser(email, orgId, roleId);
String resolvedRoleId = resolveOrgRoleId(roleName);
return logtoClient.createAndInviteUser(email, orgId, resolvedRoleId);
}
public void removeTeamMember(String userId) {
@@ -197,15 +198,33 @@ public class TenantPortalService {
throw new IllegalStateException("Tenant has no Logto organization configured");
}
logtoClient.removeUserFromOrganization(orgId, userId);
// If the user has no remaining org memberships, delete from Logto entirely
var remainingOrgs = logtoClient.getUserOrganizations(userId);
if (remainingOrgs.isEmpty()) {
log.info("User {} has no remaining org memberships — deleting from Logto", userId);
logtoClient.deleteUser(userId);
}
}
public void changeTeamMemberRole(String userId, String roleId) {
public void changeTeamMemberRole(String userId, String roleName) {
TenantEntity tenant = resolveTenant();
String orgId = tenant.getLogtoOrgId();
if (orgId == null || orgId.isBlank()) {
throw new IllegalStateException("Tenant has no Logto organization configured");
}
logtoClient.assignOrganizationRole(orgId, userId, roleId);
String resolvedRoleId = resolveOrgRoleId(roleName);
logtoClient.assignOrganizationRole(orgId, userId, resolvedRoleId);
}
/** Resolve a role name (e.g. "viewer") to a Logto organization role ID. */
private String resolveOrgRoleId(String roleName) {
if (roleName == null || roleName.isBlank()) return null;
String resolved = logtoClient.findOrgRoleIdByName(roleName);
if (resolved == null) {
throw new IllegalArgumentException("Unknown organization role: " + roleName);
}
return resolved;
}
public void resetServerAdminPassword(String newPassword) {