Refactor backend and frontend code for improved structure and functionality

- Rearranged imports in multiple files for consistency and clarity.
- Updated logging middleware to exclude specific paths from logging.
- Enhanced security module by cleaning up token handling and improving tenant validation.
- Added tenant and company scoped mixins for better database model management.
- Implemented generic CRUD routes for tenant-scoped resources.
- Improved error handling and response management in API routes.
- Cleaned up login and logout processes to ensure proper session management.
- Introduced mechanisms to clear local storage and cookies on tenant change.
- Enhanced company store to detect tenant changes and clear data accordingly.
- Added new DTO mixins for currency and value affect flags.
This commit is contained in:
2025-11-11 17:20:47 -06:00
parent ac7f8d19d8
commit b68c4316ff
247 changed files with 2248 additions and 2504 deletions

View File

@@ -48,8 +48,7 @@ async function refreshToken(): Promise<string | null> {
};
refreshTokenValue = getCookie('refresh_token');
if (refreshTokenValue) {
console.log('📝 [API] Refresh token encontrado en cookies, sincronizando a localStorage');
if (refreshTokenValue) {
localStorage.setItem('refresh_token', refreshTokenValue);
}
}
@@ -57,9 +56,7 @@ async function refreshToken(): Promise<string | null> {
if (!refreshTokenValue) {
console.error('❌ [API] No hay refresh token disponible');
return null;
}
console.log('🔄 [API] Intentando refrescar token...');
}
try {
const response = await fetch(`${API_BASE_URL}/v1/auth/refresh`, {
@@ -88,8 +85,7 @@ async function refreshToken(): Promise<string | null> {
return null;
}
const data = await response.json();
console.log('✅ [API] Token refrescado correctamente');
const data = await response.json();
// Guardar los nuevos tokens
if (data.access_token) {
@@ -136,8 +132,7 @@ async function fetchApi<T = any>(
retryCount = 0
): Promise<ApiResponse<T>> {
// Si ya estamos refrescando el token, esperar
if (isRefreshing && retryCount === 0) {
console.log('⏳ [API] Esperando refresh del token...');
if (isRefreshing && retryCount === 0) {
return new Promise((resolve) => {
subscribeTokenRefresh((newToken) => {
resolve(fetchApi<T>(endpoint, options, 1));
@@ -168,16 +163,14 @@ async function fetchApi<T = any>(
});
// Si recibimos 401 o 403 y no es el endpoint de refresh, intentar refrescar el token
if ((response.status === 401 || response.status === 403) && !endpoint.includes('/auth/refresh') && retryCount === 0) {
console.log('🔄 [API] Recibido 401/403, intentando refrescar token...');
if ((response.status === 401 || response.status === 403) && !endpoint.includes('/auth/refresh') && retryCount === 0) {
isRefreshing = true;
try {
const newToken = await refreshToken();
if (newToken) {
// Token refrescado exitosamente
console.log('✅ [API] Token refrescado exitosamente');
// Token refrescado exitosamente
onTokenRefreshed(newToken);
isRefreshing = false;
// Reintentar la petición original con el nuevo token

View File

@@ -11,9 +11,10 @@
import { Button } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { page } from '$app/stores';
import { page } from '$app/state';
import { enhance } from '$app/forms';
import { loginWithProvider } from '$lib/sso';
import { onMount } from 'svelte';
let { class: className, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();
@@ -25,23 +26,40 @@
let loading = $state(false);
// Obtener el error del servidor si existe
const error = $derived($page.form?.error || '');
const error = $derived(page.form?.error || '');
// Limpiar todo el localStorage y cookies al montar el componente de login
// Esto asegura que no queden datos del tenant anterior
onMount(() => {
clearAllData();
});
// Función para limpiar cookies del cliente
function clearClientCookies() {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
}
if (typeof document !== 'undefined') {
const isSecure = window.location.protocol === 'https:';
const secureFlag = isSecure ? '; Secure' : '';
document.cookie = `access_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Lax${secureFlag}`;
document.cookie = `refresh_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Lax${secureFlag}`;
document.cookie = `active_company_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Lax${secureFlag}`;
}
}
// Función para limpiar todo el localStorage y cookies
function clearAllData() {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('activeCompanyId');
}
clearClientCookies();
}
function handleMicrosoftLogin() {
// Limpiar datos antes de iniciar SSO
clearAllData();
// Guardar el tenant_slug en localStorage para recuperarlo después del callback
if (tenantSlug) {
localStorage.setItem('pending_tenant_slug', tenantSlug);
@@ -50,6 +68,9 @@
}
function handleGoogleLogin() {
// Limpiar datos antes de iniciar SSO
clearAllData();
// Guardar el tenant_slug en localStorage para recuperarlo después del callback
if (tenantSlug) {
localStorage.setItem('pending_tenant_slug', tenantSlug);

View File

@@ -15,6 +15,7 @@ class CompanyStore {
private _activeCompany = $state<Company | null>(null);
private _companies = $state<Company[]>([]);
private _loading = $state(false);
private _currentTenantId = $state<number | null>(null);
get activeCompany() {
return this._activeCompany;
@@ -36,7 +37,22 @@ class CompanyStore {
try {
const response = await fetch('/api/company/my-companies');
if (response.ok) {
this._companies = await response.json();
const newCompanies = await response.json();
// Detectar si el tenant ha cambiado
if (newCompanies.length > 0) {
const newTenantId = newCompanies[0].tenant_id;
// Si el tenant cambió, limpiar el store primero
if (this._currentTenantId !== null && this._currentTenantId !== newTenantId) {
this.clear();
}
this._currentTenantId = newTenantId;
}
this._companies = newCompanies;
// Si hay compañías y no hay una activa, seleccionar la primera
if (this._companies.length > 0 && !this._activeCompany) {
@@ -44,6 +60,10 @@ class CompanyStore {
}
} else {
console.error('Error loading companies:', response.statusText);
// Si falla la carga (ej: 401), limpiar el store
if (response.status === 401) {
this.clear();
}
}
} catch (error) {
console.error('Error loading companies:', error);
@@ -98,6 +118,7 @@ class CompanyStore {
this._activeCompany = null;
this._companies = [];
this._loading = false;
this._currentTenantId = null;
// Limpiar localStorage
if (typeof window !== 'undefined') {

View File

@@ -8,6 +8,8 @@ export const GET: RequestHandler = async ({ cookies, fetch }) => {
const token = cookies.get('access_token');
if (!token) {
// Limpiar cualquier cookie de compañía si no hay autenticación
cookies.delete('active_company_id', { path: '/' });
return json({ error: 'No authenticated' }, { status: 401 });
}
@@ -31,9 +33,13 @@ export const GET: RequestHandler = async ({ cookies, fetch }) => {
});
if (!response.ok) {
// Si la autenticación falló, limpiar la cookie de compañía
if (response.status === 401) {
cookies.delete('active_company_id', { path: '/' });
}
return json({ error: 'Failed to fetch companies' }, { status: response.status });
}
console.log('✅ Token', token);
const companies = await response.json();
return json(companies);
} catch (error) {

View File

@@ -6,9 +6,16 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
if (url.searchParams.has('logout')) {
cookies.delete('access_token', { path: '/' });
cookies.delete('refresh_token', { path: '/' });
cookies.delete('active_company_id', { path: '/' });
return {};
}
// Limpiar siempre las cookies de sesión anterior al cargar login
// Esto evita que se queden datos del tenant anterior
cookies.delete('access_token', { path: '/' });
cookies.delete('refresh_token', { path: '/' });
cookies.delete('active_company_id', { path: '/' });
// Permitir acceso al login sin redirigir automáticamente
// Esto evita bucles de redirección cuando el token existe pero puede estar expirado
return {};

View File

@@ -6,6 +6,9 @@ export const POST: RequestHandler = async ({ cookies }) => {
cookies.delete('access_token', { path: '/' });
cookies.delete('refresh_token', { path: '/' });
// Eliminar la cookie de la compañía activa
cookies.delete('active_company_id', { path: '/' });
// Redirigir al login
throw redirect(303, '/login');
};