diff --git a/adminfront/src/features/users/UserDetailPage.tsx b/adminfront/src/features/users/UserDetailPage.tsx index 6db94a92..e526c619 100644 --- a/adminfront/src/features/users/UserDetailPage.tsx +++ b/adminfront/src/features/users/UserDetailPage.tsx @@ -139,7 +139,9 @@ function TenantMetadataFields({ ); } -type UserFormValues = UserUpdateRequest & { metadata: Record }; +type UserFormValues = Omit & { + metadata: Record>; +}; function UserDetailPage() { const params = useParams<{ id: string }>(); @@ -299,7 +301,20 @@ function UserDetailPage() { const onSubmit = (data: UserFormValues) => { setError(null); setSuccessMsg(null); - mutation.mutate(data); + + // Filter out undefined/null/empty strings from metadata + const cleanMetadata = Object.fromEntries( + Object.entries(data.metadata).map(([tenantId, fields]) => { + const cleanFields = Object.fromEntries( + Object.entries(fields).filter( + ([_, v]) => v !== undefined && v !== null && v !== "", + ), + ); + return [tenantId, cleanFields]; + }), + ); + + mutation.mutate({ ...data, metadata: cleanMetadata }); }; const handleDelete = () => { diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go index 40d101f1..112b95bb 100644 --- a/backend/internal/handler/user_handler.go +++ b/backend/internal/handler/user_handler.go @@ -1194,15 +1194,28 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error { } // For namespaced metadata, we don't delete everything, we merge. - // But we should remove legacy flat traits that are not in the new req.Metadata if we want strict sync. - // For now, let's just merge. for k, v := range req.Metadata { if !coreTraits[k] { - traits[k] = v + // Ensure we are merging maps (tenant namespaces) correctly, not overwriting with slices + if incomingMap, ok := v.(map[string]any); ok { + if existingMap, ok := traits[k].(map[string]interface{}); ok { + for subK, subV := range incomingMap { + existingMap[subK] = subV + } + traits[k] = existingMap + } else { + traits[k] = incomingMap // New namespace + } + } else { + traits[k] = v // Fallback for flat metadata + } } } state := normalizeKratosState(req.Status) + + slog.Info("[UpdateUser] Calling Kratos UpdateIdentity", "userID", userID, "traits", traits, "state", state) + updated, err := h.KratosAdmin.UpdateIdentity(c.Context(), userID, traits, state) if err != nil { return errorJSON(c, fiber.StatusInternalServerError, err.Error())