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:
@@ -7,6 +7,11 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { setAccessTokenCookies } from '$lib/server/access-token-cookie';
|
||||
import { getAccessTokenFromCookies } from '$lib/server/access-token-cookie';
|
||||
|
||||
// Disable client-side rendering to prevent SvelteKit from making a second
|
||||
// __data.json request that would consume the one-time relay token twice.
|
||||
export const csr = false;
|
||||
|
||||
export const load: PageServerLoad = async ({ url, cookies }) => {
|
||||
const relayToken = url.searchParams.get('relay');
|
||||
@@ -18,21 +23,22 @@ export const load: PageServerLoad = async ({ url, cookies }) => {
|
||||
|
||||
// If there is already a valid session, skip the exchange to avoid
|
||||
// re-using a one-time relay token (e.g. browser tab reload or prefetch).
|
||||
const existingToken = cookies.get('access_token') || cookies.get('access_token_0');
|
||||
const existingToken = getAccessTokenFromCookies(cookies);
|
||||
if (existingToken) {
|
||||
console.log('[SSO] sesión existente detectada, redirigiendo sin exchange');
|
||||
throw redirect(303, '/dashboard');
|
||||
}
|
||||
|
||||
// SSO exchange must call Hub backend, not Anexo76 backend.
|
||||
// Use INTERNAL_HUB_URL for server-to-server communication.
|
||||
let hubUrl = process.env.INTERNAL_HUB_URL;
|
||||
if (!hubUrl) {
|
||||
hubUrl = process.env.VITE_HUB_URL;
|
||||
// Fallback: replace localhost with hub-backend for Docker
|
||||
hubUrl = hubUrl?.replace('localhost', 'host.docker.internal').replace('127.0.0.1', 'host.docker.internal');
|
||||
}
|
||||
const baseUrl = hubUrl?.endsWith('/') ? hubUrl : `${hubUrl}/`;
|
||||
// SSO exchange must call the Hub that GENERATED the relay token.
|
||||
// HUB_URL is the canonical public Hub (workspace.aduanasoft.com) — where the
|
||||
// App Launcher runs and where relay tokens are stored.
|
||||
// INTERNAL_HUB_URL is a local mirror only used for token validation in the backend.
|
||||
const hubUrl = (
|
||||
process.env.HUB_URL ||
|
||||
process.env.VITE_HUB_URL ||
|
||||
'http://localhost:8001'
|
||||
).replace(/\/+$/, '');
|
||||
const baseUrl = `${hubUrl}/`;
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
@@ -47,7 +53,22 @@ export const load: PageServerLoad = async ({ url, cookies }) => {
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => ({}));
|
||||
const detail = body?.detail || 'sso_exchange_failed';
|
||||
const detail: string = body?.detail || 'sso_exchange_failed';
|
||||
console.error('[SSO] exchange falló:', response.status, detail);
|
||||
|
||||
// If the token is "invalid/used", a concurrent request may have already
|
||||
// succeeded and set cookies. Redirect to /dashboard — if the session is
|
||||
// valid it will load; if not, the dashboard layout will redirect to /login.
|
||||
const tokenAlreadyUsed =
|
||||
detail.toLowerCase().includes('inválido') ||
|
||||
detail.toLowerCase().includes('invalido') ||
|
||||
detail.toLowerCase().includes('invalid') ||
|
||||
detail.toLowerCase().includes('used') ||
|
||||
detail.toLowerCase().includes('expired');
|
||||
if (tokenAlreadyUsed) {
|
||||
throw redirect(303, '/dashboard');
|
||||
}
|
||||
|
||||
throw redirect(303, `/login?error=${encodeURIComponent(detail)}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,9 +54,12 @@
|
||||
updateCsvImportBanner();
|
||||
});
|
||||
|
||||
// Hacer disponible el usuario en el contexto para los componentes hijos
|
||||
// Hacer disponible el usuario en el contexto para los componentes hijos.
|
||||
// El sidebar ya lee de page.data directamente; este contexto lo usan otros componentes.
|
||||
setContext('user', data.user);
|
||||
setContext('userTenants', data.userTenants ?? []);
|
||||
// Actualizar el contexto reactive al cambiar data.user (post invalidateAll)
|
||||
$effect(() => { setContext('user', data.user); });
|
||||
|
||||
// ── Manejar expiración de sesión ────────────────────────────────────────
|
||||
function handleSessionExpired(e: Event) {
|
||||
|
||||
@@ -1,42 +1,18 @@
|
||||
/**
|
||||
* Server-side load y actions para gestión de perfil de usuario
|
||||
*/
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { authenticatedFetch } from '$lib/server/api';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, fetch }) => {
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
'v1/core/users/me/profile',
|
||||
{
|
||||
method: 'GET',
|
||||
},
|
||||
cookies,
|
||||
fetch,
|
||||
'/login' // Redirigir a login si no está autenticado
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
return {
|
||||
profile: null,
|
||||
error: errorData.detail || 'Error al cargar el perfil'
|
||||
};
|
||||
}
|
||||
|
||||
const profile = await response.json();
|
||||
return {
|
||||
profile,
|
||||
error: null
|
||||
};
|
||||
} catch (err: any) {
|
||||
console.error('Error loading profile:', err);
|
||||
return {
|
||||
profile: null,
|
||||
error: 'Error al cargar el perfil'
|
||||
};
|
||||
}
|
||||
export const load: PageServerLoad = async ({ parent }) => {
|
||||
// El layout /dashboard ya cargó los datos del usuario via validateAuth.
|
||||
// Reutilizamos esos datos en lugar de hacer una llamada duplicada.
|
||||
const { user } = await parent();
|
||||
return {
|
||||
profile: user ?? null,
|
||||
error: user ? null : 'Error al cargar el perfil'
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
@@ -86,10 +62,9 @@ export const actions: Actions = {
|
||||
}
|
||||
}
|
||||
|
||||
// Agregar avatar_url si se subió exitosamente
|
||||
if (avatarUrl) {
|
||||
updateData.avatar_url = avatarUrl;
|
||||
}
|
||||
// No incluir avatar_url en el PUT /me/profile: el POST /me/avatar ya persistió
|
||||
// el valor correcto (clave S3 o ruta local) en UserTenant. Enviar aquí la URL
|
||||
// pública sobrescribiría esa clave y rompería el endpoint de servicio de imágenes.
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '$lib/components/ui/avatar';
|
||||
import { getBackendAssetUrl } from '$lib/utils';
|
||||
@@ -120,7 +119,9 @@
|
||||
error = '';
|
||||
success = '';
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
// reset:false evita que SvelteKit limpie el formulario nativo.
|
||||
// update() ya llama a invalidateAll internamente.
|
||||
await update({ reset: false });
|
||||
saving = false;
|
||||
};
|
||||
}}
|
||||
@@ -200,22 +201,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2.5">
|
||||
<Label for="email" class="text-sm font-medium">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={profile.email || ''}
|
||||
placeholder="tu@email.com"
|
||||
class="transition-colors"
|
||||
required
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Cambiar el email puede requerir verificación
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2.5">
|
||||
<Label for="phone" class="text-sm font-medium">Teléfono</Label>
|
||||
<Input
|
||||
@@ -227,21 +212,6 @@
|
||||
class="transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2.5">
|
||||
<Label for="bio" class="text-sm font-medium">Biografía</Label>
|
||||
<Textarea
|
||||
id="bio"
|
||||
name="bio"
|
||||
value={profile.bio || ''}
|
||||
placeholder="Cuéntanos algo sobre ti..."
|
||||
rows={4}
|
||||
class="resize-none transition-colors"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Máximo 500 caracteres
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -263,6 +233,13 @@
|
||||
<Input value={profile.id} disabled class="font-mono text-xs bg-muted/50" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2.5">
|
||||
<Label class="text-sm font-medium">Email</Label>
|
||||
<Input value={profile.email || ''} disabled class="bg-muted/50" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Para cambiar tu email accede a tu perfil en el portal de Aduanasoft.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user