Merge pull request 'feat: add first_name and last_name fields to UserTenant model and sync with Keycloak' (#366) from fix/user-profile-edit-keycloak-sync into development

Reviewed-on: ADUANASOFT/anexo76#366
This commit is contained in:
2026-05-06 17:11:07 +00:00
12 changed files with 432 additions and 264 deletions

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { getContext, onMount } from "svelte";
import { onMount } from "svelte";
import { page } from "$app/state";
import { useSidebar } from "$lib/components/ui/sidebar/context.svelte.js";
import { getSidebarData } from "$lib/components/sidebar/modules";
import NavMain from "./nav-main.svelte";
@@ -15,9 +16,8 @@
...restProps
}: ComponentProps<typeof Sidebar.Root> = $props();
// Obtener datos del usuario desde el contexto (viene de Keycloak vía +layout.server.ts)
const userData = getContext<any>('user');
const userTenants = getContext<{ id: number; name: string; slug: string }[]>('userTenants') ?? [];
// Leer siempre de page.data para que el sidebar reaccione tras invalidateAll()
const userTenants = $derived((page.data.userTenants as { id: number; name: string; slug: string }[]) ?? []);
// Obtener datos del sidebar con traducciones
const sidebarData = getSidebarData();
@@ -25,15 +25,21 @@
// Combinar los datos estáticos del sidebar con los datos del usuario de Keycloak
const data = $derived({
...sidebarData,
user: userData
user: page.data.user
? {
name: userData.name || userData.preferred_username || "",
email: userData.email || "",
avatar: userData.avatar_url || "/avatars/default.jpg",
name: _displayName(page.data.user),
email: page.data.user.email || "",
avatar: page.data.user.avatar_url || "/avatars/default.jpg",
}
: sidebarData.user,
});
function _displayName(u: any): string {
const first = u.first_name || u.given_name || "";
const last = u.last_name || u.family_name || "";
return (first + " " + last).trim() || u.name || u.preferred_username || "";
}
const sidebar = useSidebar();
onMount(() => {

View File

@@ -285,20 +285,34 @@ export async function validateAuth(
if (profileResponse.ok) {
const profileData = await profileResponse.json();
// Combinar datos de Keycloak con datos del perfil
// Combinar datos de Keycloak con datos del perfil.
// Prioridad para nombre: caché local del perfil > JWT claims.
return {
...keycloakData,
avatar_url: profileData.avatar_url,
phone: profileData.phone,
bio: profileData.bio,
preferences: profileData.preferences
id: profileData.id || keycloakData.id || keycloakData.sub,
username: profileData.username || keycloakData.username || keycloakData.preferred_username || '',
email: profileData.email || keycloakData.email || '',
first_name: profileData.first_name || keycloakData.first_name || keycloakData.given_name || '',
last_name: profileData.last_name || keycloakData.last_name || keycloakData.family_name || '',
avatar_url: profileData.avatar_url || null,
phone: profileData.phone || null,
bio: profileData.bio || null,
preferences: profileData.preferences || {}
};
}
} catch (profileError) {
console.warn('⚠️ [API] No se pudo cargar el perfil del usuario, usando solo datos de Keycloak');
}
return keycloakData;
// Fallback: map raw JWT claim names to the expected field names
const nameParts = (keycloakData.name || '').split(' ');
return {
...keycloakData,
id: keycloakData.id || keycloakData.sub,
username: keycloakData.username || keycloakData.preferred_username || '',
first_name: keycloakData.first_name || keycloakData.given_name || nameParts[0] || '',
last_name: keycloakData.last_name || keycloakData.family_name || nameParts.slice(1).join(' ') || '',
};
} catch (error) {
// Si es un redirect, re-lanzarlo
if (error && typeof error === 'object' && 'status' in error && 'location' in error) {