diff --git a/docker-compose.yml b/docker-compose.yml index 9814035..d412b4e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,9 +13,9 @@ services: POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" volumes: - postgres_data:/var/lib/postgresql/data - - ./db/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro + #- ./db/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro ports: - - "5432:5432" + - "5433:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-servicemanager}"] interval: 10s @@ -215,7 +215,7 @@ services: - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro - uploads_data:/var/www/uploads:ro ports: - - "80:80" + - "8088:80" depends_on: - backend - frontend-client diff --git a/frontend-client/src/hooks.server.ts b/frontend-client/src/hooks.server.ts index 19e5436..69cdc94 100644 --- a/frontend-client/src/hooks.server.ts +++ b/frontend-client/src/hooks.server.ts @@ -1,19 +1,37 @@ import type { Handle } from '@sveltejs/kit'; - export const handle: Handle = async ({ event, resolve }) => { - const cookie = event.request.headers.get('cookie') ?? ''; - if (cookie) { - try { - const apiUrl = process.env.PUBLIC_API_URL ?? 'http://backend:8000'; - const response = await fetch(`${apiUrl}/v1/auth/me`, { - headers: { cookie, 'X-App': 'client' } - }); - event.locals.user = response.ok ? await response.json() : null; - } catch { - event.locals.user = null; - } - } else { - event.locals.user = null; - } - return resolve(event); -}; + // No restaurar sesión en la página de login + if (event.url.pathname === '/login') { + event.locals.user = null; + return resolve(event); + } + + const cookieHeader = event.request.headers.get('cookie') ?? ''; + const cookieMatch = cookieHeader.match(/(?:client_access_token|internal_access_token)=([^;]+)/); + const token = cookieMatch?.[1]; + + if (token) { + try { + const apiUrl = process.env.PUBLIC_API_URL ?? 'http://backend:8000'; + const response = await fetch(`${apiUrl}/v1/auth/me`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'X-App': 'client', + 'X-Tenant-Slug': 'aduanasoft' + } + }); + if (response.ok) { + event.locals.user = await response.json(); + } else { + event.locals.user = null; + event.cookies.delete('client_access_token', { path: '/' }); + event.cookies.delete('internal_access_token', { path: '/' }); + } + } catch { + event.locals.user = null; + } + } else { + event.locals.user = null; + } + return resolve(event); +}; \ No newline at end of file diff --git a/frontend-client/src/lib/stores/auth.ts b/frontend-client/src/lib/stores/auth.ts index 0316596..4acf562 100644 --- a/frontend-client/src/lib/stores/auth.ts +++ b/frontend-client/src/lib/stores/auth.ts @@ -1,7 +1,5 @@ import type { Writable } from 'svelte/store'; import { writable } from 'svelte/store'; - -// Types export interface User { id: string; email: string; @@ -13,131 +11,94 @@ export interface User { is_two_factor_enabled: boolean; created_at: string; } - export interface AuthState { user: User | null; token: string | null; isAuthenticated: boolean; isLoading: boolean; } - export interface LoginRequest { email: string; password: string; tenant_slug: string; totp_code?: string; } - export interface LoginResponse { access_token: string; token_type: string; expires_in: number; user: User; } - -// Initial state const initialState: AuthState = { user: null, token: null, isAuthenticated: false, isLoading: false }; - -// Create auth store function createAuthStore() { const { subscribe, set, update }: Writable = writable(initialState); - + let _state = initialState; + subscribe(s => { _state = s; }); return { subscribe, - - // Rehidrata sesión desde cookie HttpOnly (no toca localStorage) init: async () => { if (typeof window !== 'undefined') { try { const response = await fetch('/api/v1/auth/me', { credentials: 'include', - headers: { 'X-App': 'client' } + headers: { 'X-App': 'client', 'X-Tenant-Slug': 'aduanasoft' } }); if (response.ok) { const user = await response.json(); - set({ - user, - token: null, - isAuthenticated: true, - isLoading: false - }); + set({ user, token: null, isAuthenticated: true, isLoading: false }); } - // 401/400 es esperado cuando no hay sesión activa — no es un error - } catch (error) { - // Ignorar errores de red en init - } + } catch (error) {} } }, - - // Login login: async (credentials: LoginRequest): Promise => { update(state => ({ ...state, isLoading: true })); - try { const response = await fetch('/api/v1/auth/login', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', + 'X-Tenant-Slug': credentials.tenant_slug, }, body: JSON.stringify(credentials) }); - if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Login failed'); } - const data: LoginResponse = await response.json(); - - set({ - user: data.user, - token: data.access_token, - isAuthenticated: true, - isLoading: false - }); + set({ user: data.user, token: data.access_token, isAuthenticated: true, isLoading: false }); } catch (error) { update(state => ({ ...state, isLoading: false })); throw error; } }, - - // Logout logout: async () => { - // Llamar al backend para que borre la cookie HttpOnly try { + const token = _state.token; await fetch('/api/v1/auth/logout', { method: 'POST', credentials: 'include', - headers: { 'X-App': 'client' } + headers: { + 'X-App': 'client', + 'X-Tenant-Slug': 'aduanasoft', + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + } }); - } catch { /* ignorar errores de red */ } + } catch {} set(initialState); if (typeof window !== 'undefined') { window.location.href = '/login'; } }, - - // Update user data - updateUser: (user: User) => { - update(state => ({ ...state, user })); - }, - - // Set user from SSR pre-load (no fetch required) - setUser: (user: User) => { - set({ user, token: null, isAuthenticated: true, isLoading: false }); - }, - - // Set loading state - setLoading: (isLoading: boolean) => { - update(state => ({ ...state, isLoading })); - } + updateUser: (user: User) => { update(state => ({ ...state, user })); }, + setUser: (user: User) => { set({ user, token: null, isAuthenticated: true, isLoading: false }); }, + setLoading: (isLoading: boolean) => { update(state => ({ ...state, isLoading })); } }; } - export const auth = createAuthStore(); \ No newline at end of file diff --git a/frontend-client/src/lib/utils/api.ts b/frontend-client/src/lib/utils/api.ts index a1ae2c5..2aeca74 100644 --- a/frontend-client/src/lib/utils/api.ts +++ b/frontend-client/src/lib/utils/api.ts @@ -1,12 +1,8 @@ -/** - * Cliente HTTP centralizado para frontend-client. - * Usa cookies HttpOnly (client_access_token) como fuente primaria de auth, - * con Bearer token como complemento cuando está disponible en memoria. - */ import { auth } from '$lib/stores/auth'; import { get } from 'svelte/store'; const API_BASE = '/api/v1'; +const TENANT_SLUG = 'aduanasoft'; interface RequestOptions extends RequestInit { params?: Record; @@ -20,7 +16,6 @@ async function request(endpoint: string, options: RequestOptions = {}): Promi const filteredParams = Object.entries(params) .filter(([, value]) => value !== undefined && value !== null && value !== '') .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); - if (Object.keys(filteredParams).length > 0) { url += `?${new URLSearchParams(filteredParams).toString()}`; } @@ -29,7 +24,6 @@ async function request(endpoint: string, options: RequestOptions = {}): Promi const authState = get(auth); const headers = new Headers(init.headers); - // Bearer header cuando el token está en memoria (sesión activa sin reload) if (authState.token) { headers.set('Authorization', `Bearer ${authState.token}`); } @@ -39,8 +33,8 @@ async function request(endpoint: string, options: RequestOptions = {}): Promi if (!headers.has('Content-Type')) { headers.set('Content-Type', 'application/json'); } - // Identifica este frontend para que el backend use client_access_token headers.set('X-App', 'client'); + headers.set('X-Tenant-Slug', TENANT_SLUG); const response = await fetch(url, { ...init, @@ -78,6 +72,7 @@ async function downloadFile(endpoint: string, filename: string): Promise { headers.set('X-Tenant-ID', authState.user.tenant_id); } headers.set('X-App', 'client'); + headers.set('X-Tenant-Slug', TENANT_SLUG); const response = await fetch(`${API_BASE}${endpoint}`, { method: 'GET', @@ -108,19 +103,14 @@ async function downloadFile(endpoint: string, filename: string): Promise { export const api = { get: (endpoint: string, params?: Record) => request(endpoint, { method: 'GET', params }), - post: (endpoint: string, body?: any) => request(endpoint, { method: 'POST', body: body !== undefined ? JSON.stringify(body) : undefined }), - put: (endpoint: string, body?: any) => request(endpoint, { method: 'PUT', body: body !== undefined ? JSON.stringify(body) : undefined }), - patch: (endpoint: string, body?: any) => request(endpoint, { method: 'PATCH', body: body !== undefined ? JSON.stringify(body) : undefined }), - delete: (endpoint: string) => request(endpoint, { method: 'DELETE' }), - downloadFile: (endpoint: string, filename: string) => downloadFile(endpoint, filename) -}; +}; \ No newline at end of file diff --git a/frontend-client/src/routes/login/+page.svelte b/frontend-client/src/routes/login/+page.svelte index 27e0516..4e1c976 100644 --- a/frontend-client/src/routes/login/+page.svelte +++ b/frontend-client/src/routes/login/+page.svelte @@ -7,7 +7,7 @@ let email = ''; let password = ''; - let tenantSlug = 'ventas'; + let tenantSlug = 'aduanasoft-demo'; let totpCode = ''; let isLoading = false; let showTwoFactor = false; @@ -34,7 +34,7 @@ await auth.login({ email, password, - tenant_slug: tenantSlug.trim() || 'ventas', + tenant_slug: tenantSlug.trim() || 'aduanasoft-demo', totp_code: totpCode || undefined }); diff --git a/frontend-client/vite.config.js b/frontend-client/vite.config.js index 1713f1d..7eb9761 100644 --- a/frontend-client/vite.config.js +++ b/frontend-client/vite.config.js @@ -1,38 +1,35 @@ import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; - export default defineConfig({ - plugins: [sveltekit()], - server: { - port: 3000, - host: '0.0.0.0', - watch: { - usePolling: true, - interval: 500 - }, - // HMR: el browser llega al contenedor en el mismo puerto 3000 - hmr: { - host: 'localhost', - clientPort: parseInt(process.env.HMR_CLIENT_PORT || '3000') - }, - // Permitir que Vite sirva archivos del filesystem del contenedor - fs: { - allow: ['/app', '.'], - strict: false - }, - proxy: { - '/api': { - target: process.env.PUBLIC_API_URL || 'http://localhost:8000', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, '') - } - } - }, - preview: { - port: 3000, - host: '0.0.0.0' - }, - build: { - target: 'esnext' - } -}); \ No newline at end of file + plugins: [sveltekit()], + server: { + port: 3000, + host: '0.0.0.0', + watch: { + usePolling: true, + interval: 500 + }, + hmr: { + host: 'localhost', + clientPort: parseInt(process.env.HMR_CLIENT_PORT || '3000') + }, + fs: { + allow: ['/app', '.'], + strict: false + }, + proxy: { + '/api': { + target: process.env.PUBLIC_API_URL || 'http://backend:8000', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, '') + } + } + }, + preview: { + port: 3000, + host: '0.0.0.0' + }, + build: { + target: 'esnext' + } +}); diff --git a/frontend-internal/src/lib/stores/auth.ts b/frontend-internal/src/lib/stores/auth.ts index 188b11e..7c7632c 100644 --- a/frontend-internal/src/lib/stores/auth.ts +++ b/frontend-internal/src/lib/stores/auth.ts @@ -102,6 +102,7 @@ function createAuthStore() { credentials: 'include', headers: { 'Content-Type': 'application/json', + 'X-Tenant-Slug': credentials.tenant_slug, }, body: JSON.stringify(credentials) }); diff --git a/frontend-internal/src/routes/login/+page.svelte b/frontend-internal/src/routes/login/+page.svelte index 5f2cb74..7b8cef3 100644 --- a/frontend-internal/src/routes/login/+page.svelte +++ b/frontend-internal/src/routes/login/+page.svelte @@ -191,7 +191,7 @@ Verificación de Seguridad
path.replace(/^\/api/, '') - } - } - }, - preview: { - port: parseInt(process.env.PORT || '3001'), - host: '0.0.0.0' - }, - build: { - target: 'esnext' - } + plugins: [sveltekit()], + server: { + port: parseInt(process.env.PORT || '3001'), + host: '0.0.0.0', + watch: { + usePolling: true, + interval: 500 + }, + hmr: { + host: 'localhost', + clientPort: parseInt(process.env.HMR_CLIENT_PORT || '3001') + }, + fs: { + allow: ['/app', '.'], + strict: false + }, + proxy: { + '/api': { + target: process.env.PUBLIC_API_URL || 'http://backend:8000', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, '') + } + } + }, + preview: { + port: parseInt(process.env.PORT || '3001'), + host: '0.0.0.0' + }, + build: { + target: 'esnext' + } }); diff --git a/migrations.sql b/migrations.sql new file mode 100644 index 0000000..6f1c6af Binary files /dev/null and b/migrations.sql differ