From 7fc8a4d40786eb0849f767d6ee2536d78b39ba9d Mon Sep 17 00:00:00 2001
From: hsiegeln <37154749+hsiegeln@users.noreply.github.com>
Date: Mon, 27 Apr 2026 22:36:21 +0200
Subject: [PATCH] fix: team invite role resolution, user cleanup, and settings
page redesign
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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)
---
.../saas/identity/LogtoManagementClient.java | 66 ++++++-
.../saas/portal/TenantPortalService.java | 27 ++-
ui/src/components/account/MfaSection.tsx | 21 +--
ui/src/components/account/PasskeySection.tsx | 12 +-
ui/src/pages/tenant/SettingsPage.tsx | 178 +++++-------------
5 files changed, 148 insertions(+), 156 deletions(-)
diff --git a/src/main/java/net/siegeln/cameleer/saas/identity/LogtoManagementClient.java b/src/main/java/net/siegeln/cameleer/saas/identity/LogtoManagementClient.java
index f870d80..76b8e5d 100644
--- a/src/main/java/net/siegeln/cameleer/saas/identity/LogtoManagementClient.java
+++ b/src/main/java/net/siegeln/cameleer/saas/identity/LogtoManagementClient.java
@@ -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 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
) : (
-
+
{credentials.map((pk) => (
@@ -131,6 +131,8 @@ export function PasskeySection() {
))}
)}
-
+ >
);
+
+ return bare ? content :
{content};
}
diff --git a/ui/src/pages/tenant/SettingsPage.tsx b/ui/src/pages/tenant/SettingsPage.tsx
index eecc411..2290ed0 100644
--- a/ui/src/pages/tenant/SettingsPage.tsx
+++ b/ui/src/pages/tenant/SettingsPage.tsx
@@ -13,14 +13,11 @@ import {
import {
useTenantSettings,
useResetServerAdminPassword,
- useUpdateTenantSettings,
useTenantAuthSettings, useUpdateTenantAuthSettings,
} from '../../api/tenant-hooks';
import { MfaSection } from '../../components/account/MfaSection';
-import { PasskeyNudgeBanner, PasskeySection } from '../../components/account/PasskeySection';
-import { PasswordChangeSection } from '../../components/account/PasswordChangeSection';
+import { PasskeySection } from '../../components/account/PasskeySection';
import { useScopes } from '../../auth/useScopes';
-import { tierColor } from '../../utils/tier';
import styles from '../../styles/platform.module.css';
function statusColor(status: string): 'success' | 'error' | 'warning' | 'auto' {
@@ -33,78 +30,6 @@ function statusColor(status: string): 'success' | 'error' | 'warning' | 'auto' {
}
}
-function MfaEnforcementToggle() {
- const scopes = useScopes();
- const { toast } = useToast();
- const { data: settings } = useTenantSettings();
- const updateSettings = useUpdateTenantSettings();
- const [confirmEnable, setConfirmEnable] = useState(false);
-
- if (!scopes.has('tenant:manage')) return null;
-
- const mfaRequired = settings?.mfaRequired ?? false;
-
- async function handleToggle() {
- if (!mfaRequired) {
- setConfirmEnable(true);
- return;
- }
- try {
- await updateSettings.mutateAsync({ mfaRequired: false });
- toast({ title: 'MFA requirement disabled for all members', variant: 'success' });
- } catch (err) {
- toast({ title: 'Failed to update MFA setting', description: errorMessage(err), variant: 'error' });
- }
- }
-
- async function handleConfirmEnable() {
- try {
- await updateSettings.mutateAsync({ mfaRequired: true });
- setConfirmEnable(false);
- toast({ title: 'MFA is now required for all members', variant: 'success' });
- } catch (err) {
- toast({ title: 'Failed to update MFA setting', description: errorMessage(err), variant: 'error' });
- }
- }
-
- return (
-
-
- When enabled, all team members will be required to set up multi-factor authentication before accessing this tenant.
-
-
- Require MFA for all members
-
-
- {confirmEnable ? (
-
-
- All team members who have not enrolled in MFA will need to set it up on their next login. Are you sure?
-
-
-
-
-
-
- ) : (
-
-
-
- )}
-
- );
-}
-
function AuthPolicySection() {
const scopes = useScopes();
const { toast } = useToast();
@@ -214,8 +139,8 @@ export function SettingsPage() {
return (
Settings
-
+ {/* Card 1: Tenant Details */}
@@ -226,10 +151,6 @@ export function SettingsPage() {
Slug
{data.slug}
-
- Tier
-
-
Status
@@ -244,56 +165,59 @@ export function SettingsPage() {
-
- To change your tier or other billing-related settings, please contact support.
-
-
-
-
-
-
-
- Reset the built-in admin password for your server dashboard (local login at /login?local).
-
-
-
-
-
+ {/* Card 2: Multi-Factor Authentication (MFA + Passkeys combined) */}
+
+
+
+
+
+ {/* Card 3: Authentication Policy (org-wide settings) */}
);