fix: team invite role resolution, user cleanup, and settings page redesign
- 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:
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user