From ae1cf83491e430a1408ff0808ca3ca418e7704dc Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Fri, 26 Dec 2025 09:48:39 -0600 Subject: [PATCH 1/8] Add production setup and update Docker configurations - Introduced a new docker-compose.prod.yml for production deployment. - Updated docker-compose.yml to change backend port mapping. - Added svelte-sonner dependency to frontend package.json. - Created Dockerfile.prod for building the frontend application. - Enhanced README.md with production deployment instructions. --- README.md | 15 +++ docker-compose.prod.yml | 260 +++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 2 +- frontend/Dockerfile.prod | 56 +++++++++ frontend/package.json | 3 +- frontend/pnpm-lock.yaml | 23 ++++ 6 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 docker-compose.prod.yml create mode 100644 frontend/Dockerfile.prod diff --git a/README.md b/README.md index 8c54b962..956de442 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,21 @@ python -c "from core.database import init_db; init_db()" - **Frontend**: http://localhost:5173 - **Keycloak Admin**: http://localhost:8080 +## 🛠️ Produccion + +``` +docker build -t dev.aduanasoft.com/anexo76/backend:latest -f ./backend/Dockerfile ./backend +docker build -t dev.aduanasoft.com/anexo76/frontend:latest -f ./frontend/Dockerfile.prod ./frontend +``` + +Publica en el registro (ajusta el registry si corresponde): + +``` +docker login dev.aduanasoft.com +docker push dev.aduanasoft.com/anexo76/backend:latest +docker push dev.aduanasoft.com/anexo76/frontend:latest +``` + ## 🛠️ Desarrollo Local ### Backend diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 00000000..8a6b8f8e --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,260 @@ +services: + # PostgreSQL - Base de datos core (app) + postgres-a76: + image: postgres:18-alpine + container_name: anexo76-postgres-a76 + environment: + POSTGRES_DB: anexo76_core + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_APP_PASSWORD:-postgres} + POSTGRES_INITDB_ARGS: "--encoding=UTF8" + ports: + - "5432:5432" + volumes: + - postgres_app_data:/var/lib/postgresql/data + - ./scripts/postgres-app-entrypoint.sh:/docker-entrypoint-initdb.d/init-app.sh:ro + networks: + - backend-net + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d anexo76_core || exit 1"] + interval: 5s + timeout: 3s + retries: 10 + start_period: 20s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + shm_size: 128mb + + # PostgreSQL - Base de datos Keycloak + postgres-keycloak: + image: postgres:18-alpine + container_name: anexo76-postgres-keycloak + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_KEYCLOAK_PASSWORD:-postgres} + POSTGRES_INITDB_ARGS: "--encoding=UTF8" + ports: + - "5433:5432" + volumes: + - postgres_keycloak_data:/var/lib/postgresql/data + - ./scripts/postgres-keycloak-entrypoint.sh:/docker-entrypoint-initdb.d/init-keycloak.sh:ro + networks: + - auth-net + - backend-net + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d keycloak || exit 1"] + interval: 5s + timeout: 3s + retries: 10 + start_period: 20s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + shm_size: 128mb + + # Keycloak - Servidor de autenticación + keycloak: + image: quay.io/keycloak/keycloak:26.4 + container_name: anexo76-keycloak + environment: + KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin} + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin} + KC_DB: postgres + KC_DB_URL_HOST: postgres-keycloak + KC_DB_URL_PORT: "5432" + KC_DB_URL_DATABASE: keycloak + KC_DB_URL: jdbc:postgresql://postgres-keycloak:5432/keycloak + KC_DB_USERNAME: postgres + KC_DB_PASSWORD: ${POSTGRES_KEYCLOAK_PASSWORD:-postgres} + KC_DB_SCHEMA: public + KC_HOSTNAME: localhost + KC_HTTP_ENABLED: "true" + KC_HOSTNAME_STRICT: "false" + KC_HOSTNAME_STRICT_HTTPS: "false" + KC_PROXY_HEADERS: "xforwarded" + KC_HEALTH_ENABLED: "true" + KC_METRICS_ENABLED: "true" + KC_LOG_LEVEL: INFO + JAVA_OPTS_APPEND: "-Xms256m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true" + command: + - start-dev + - --db=postgres + - --db-url-host=postgres-keycloak + - --db-url-port=5432 + - --db-url-database=keycloak + - --db-username=postgres + - --db-password=${POSTGRES_KEYCLOAK_PASSWORD:-postgres} + - --http-enabled=true + - --hostname-strict=false + - --proxy-headers=xforwarded + ports: + - "8080:8080" + - "9000:9000" + depends_on: + postgres-keycloak: + condition: service_healthy + volumes: + - keycloak_data:/opt/keycloak/data + networks: + - auth-net + - backend-net + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /health/ready HTTP/1.1\r\nhost: 127.0.0.1\r\nConnection: close\r\n\r\n' >&3; grep -q 'HTTP/1.1 200' <&3 || exit 1"] + interval: 10s + timeout: 5s + retries: 30 + start_period: 90s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 768M + reservations: + memory: 512M + + # Backend - FastAPI + backend: + image: dev.aduanasoft.com/anexo76/backend:latest + container_name: anexo76-backend + environment: + - DEBUG=${DEBUG:-True} + - ENVIRONMENT=${ENVIRONMENT:-development} + - PYTHONUNBUFFERED=1 + - PYTHONDONTWRITEBYTECODE=1 + - CORE_DB_HOST=${CORE_DB_HOST:-postgres-a76} + - CORE_DB_PORT=${CORE_DB_PORT:-5432} + - CORE_DB_NAME=${CORE_DB_NAME:-anexo76_core} + - CORE_DB_USER=${CORE_DB_USER:-postgres} + - CORE_DB_PASSWORD=${POSTGRES_APP_PASSWORD:-postgres} + - KEYCLOAK_SERVER_URL=${KEYCLOAK_SERVER_URL:-http://keycloak:8080} + - KEYCLOAK_REALM=${KEYCLOAK_REALM:-master} + - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend} + - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret} + - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5173,http://localhost:3000} + ports: + - "8000:8000" + depends_on: + postgres-a76: + condition: service_healthy + keycloak: + condition: service_healthy + volumes: + - ./backend:/app + - backend_cache:/app/__pycache__ + - ./scripts/backend-entrypoint.sh:/entrypoint.sh:ro + networks: + - backend-net + - frontend-net + restart: unless-stopped + entrypoint: ["/entrypoint.sh"] + command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload", "--log-level", "info"] + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8000/api/health || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 60s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + + # Frontend - SvelteKit + frontend: + image: dev.aduanasoft.com/anexo76/frontend:latest + container_name: anexo76-frontend + environment: + - NODE_ENV=${NODE_ENV:-development} + - VITE_API_URL=${VITE_API_URL:-http://localhost:8000/api/} + - INTERNAL_API_URL=${INTERNAL_API_URL:-http://backend:8000/api/} + - VITE_KEYCLOAK_URL=${VITE_KEYCLOAK_URL:-http://localhost:8080} + - VITE_KEYCLOAK_REALM=${VITE_KEYCLOAK_REALM:-master} + - VITE_KEYCLOAK_CLIENT_ID=${VITE_KEYCLOAK_CLIENT_ID:-anexo76-frontend} + - KEYCLOAK_URL=${KEYCLOAK_URL:-http://keycloak:8080} + - KEYCLOAK_REALM=${KEYCLOAK_REALM:-master} + - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend} + - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-zRU5NuvUFtBSOuh7Kdc372AItoWGLgz9} + ports: + - "5173:5173" + depends_on: + backend: + condition: service_healthy + entrypoint: ["/frontend-entrypoint.sh"] + volumes: + - ./frontend:/app + - frontend_node_modules:/app/node_modules + - ./scripts/frontend-entrypoint.sh:/frontend-entrypoint.sh:ro + networks: + - frontend-net + - auth-net + restart: unless-stopped + command: ["pnpm", "run", "dev", "--", "--host", "0.0.0.0"] + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:5173/ || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 45s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 1G + reservations: + memory: 512M + +volumes: + postgres_app_data: + driver: local + postgres_keycloak_data: + driver: local + keycloak_data: + driver: local + frontend_node_modules: + driver: local + backend_cache: + driver: local + +networks: + backend-net: + driver: bridge + auth-net: + driver: bridge + frontend-net: + driver: bridge \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index c13ae35e..18215907 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -162,7 +162,7 @@ services: - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret} - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5173,http://localhost:3000} ports: - - "5050:8000" + - "8000:8000" depends_on: postgres-a76: condition: service_healthy diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod new file mode 100644 index 00000000..1c5a55e6 --- /dev/null +++ b/frontend/Dockerfile.prod @@ -0,0 +1,56 @@ +# ========================== +# Etapa de build +# ========================== +FROM node:22-alpine AS build + +# Directorio de trabajo +WORKDIR /app + +# Configurar npm para trabajar con certificados autofirmados e instalar pnpm +RUN npm config set strict-ssl false && \ + npm install -g pnpm + +# Copiar archivos de dependencias +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ + +# Instalar dependencias con pnpm +RUN pnpm install --frozen-lockfile + +ARG VITE_API_URL +ENV VITE_API_URL=${VITE_API_URL} + +# Copiar el resto del código +COPY . . + +# Construir el proyecto +RUN pnpm run build + + +# ========================== +# Etapa de ejecución con Node.js +# ========================== +FROM node:22-alpine AS runtime + +WORKDIR /app + +# Crear usuario no-root para seguridad antes de copiar con --chown +RUN addgroup -g 1001 -S nodejs +RUN adduser -S svelte -u 1001 + +# Copiar solo archivos necesarios para producción y aplicar propietario en la copia +COPY --from=build --chown=svelte:nodejs /app/build ./build +COPY --from=build --chown=svelte:nodejs /app/package.json ./ +COPY --from=build --chown=svelte:nodejs /app/node_modules ./node_modules + +USER svelte + +# Puerto para SvelteKit con adapter-node +EXPOSE 3000 + +# Variables de entorno +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOST=0.0.0.0 + +# Ejecutar aplicación con Node.js +CMD ["node", "build"] \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 5845afea..af6b62ab 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -56,6 +56,7 @@ }, "dependencies": { "keycloak-js": "^26.2.1", - "lucide-svelte": "^0.553.0" + "lucide-svelte": "^0.553.0", + "svelte-sonner": "^1.0.7" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 35a56541..bfafc5e5 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: lucide-svelte: specifier: ^0.553.0 version: 0.553.0(svelte@5.40.2) + svelte-sonner: + specifier: ^1.0.7 + version: 1.0.7(svelte@5.40.2) devDependencies: '@eslint/compat': specifier: ^1.4.0 @@ -1675,6 +1678,11 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + runed@0.28.0: + resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==} + peerDependencies: + svelte: ^5.7.0 + runed@0.35.1: resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==} peerDependencies: @@ -1761,6 +1769,11 @@ packages: svelte: optional: true + svelte-sonner@1.0.7: + resolution: {integrity: sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A==} + peerDependencies: + svelte: ^5.0.0 + svelte-toolbelt@0.10.6: resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} engines: {node: '>=18', pnpm: '>=8.7.0'} @@ -3379,6 +3392,11 @@ snapshots: dependencies: queue-microtask: 1.2.3 + runed@0.28.0(svelte@5.40.2): + dependencies: + esm-env: 1.2.2 + svelte: 5.40.2 + runed@0.35.1(@sveltejs/kit@2.47.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2): dependencies: dequal: 2.0.3 @@ -3460,6 +3478,11 @@ snapshots: optionalDependencies: svelte: 5.40.2 + svelte-sonner@1.0.7(svelte@5.40.2): + dependencies: + runed: 0.28.0(svelte@5.40.2) + svelte: 5.40.2 + svelte-toolbelt@0.10.6(@sveltejs/kit@2.47.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2): dependencies: clsx: 2.1.1 From 731ac168b5ce5045a6860a43c630612c132d2a01 Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Fri, 26 Dec 2025 09:50:49 -0600 Subject: [PATCH 2/8] Remove unused database check scripts for equivalencies and units of measure --- backend/check_db.py | 30 ------------------------------ check_db.py | 30 ------------------------------ check_uom.py | 23 ----------------------- 3 files changed, 83 deletions(-) delete mode 100644 backend/check_db.py delete mode 100644 check_db.py delete mode 100644 check_uom.py diff --git a/backend/check_db.py b/backend/check_db.py deleted file mode 100644 index 74209b31..00000000 --- a/backend/check_db.py +++ /dev/null @@ -1,30 +0,0 @@ -import sys -import os -from sqlalchemy import create_engine, text, inspect -from sqlalchemy.orm import sessionmaker - -# Add the backend directory to the python path -sys.path.append(os.path.join(os.getcwd(), 'backend')) - -from core.config import settings - -def check_equivalencies_columns(): - engine = create_engine(str(settings.core_database_url)) - inspector = inspect(engine) - - try: - print("Checking equivalencies table columns...") - columns = inspector.get_columns('equivalencies', schema='a76') - for column in columns: - print(f"Column: {column['name']} - Type: {column['type']}") - - print("\nChecking equivalency_items table columns...") - columns = inspector.get_columns('equivalency_items', schema='a76') - for column in columns: - print(f"Column: {column['name']} - Type: {column['type']}") - - except Exception as e: - print(f"Error: {e}") - -if __name__ == "__main__": - check_equivalencies_columns() diff --git a/check_db.py b/check_db.py deleted file mode 100644 index 74209b31..00000000 --- a/check_db.py +++ /dev/null @@ -1,30 +0,0 @@ -import sys -import os -from sqlalchemy import create_engine, text, inspect -from sqlalchemy.orm import sessionmaker - -# Add the backend directory to the python path -sys.path.append(os.path.join(os.getcwd(), 'backend')) - -from core.config import settings - -def check_equivalencies_columns(): - engine = create_engine(str(settings.core_database_url)) - inspector = inspect(engine) - - try: - print("Checking equivalencies table columns...") - columns = inspector.get_columns('equivalencies', schema='a76') - for column in columns: - print(f"Column: {column['name']} - Type: {column['type']}") - - print("\nChecking equivalency_items table columns...") - columns = inspector.get_columns('equivalency_items', schema='a76') - for column in columns: - print(f"Column: {column['name']} - Type: {column['type']}") - - except Exception as e: - print(f"Error: {e}") - -if __name__ == "__main__": - check_equivalencies_columns() diff --git a/check_uom.py b/check_uom.py deleted file mode 100644 index 43776e89..00000000 --- a/check_uom.py +++ /dev/null @@ -1,23 +0,0 @@ -import sys -import os - -# Add backend to path -sys.path.append(os.path.join(os.getcwd(), 'backend')) - -from core.database import CoreSessionLocal -from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure - -def check_units(): - db = CoreSessionLocal() - try: - units = db.query(UnitOfMeasure).limit(10).all() - print(f"Found {len(units)} units:") - for u in units: - print(f"Code: {u.code}, Description: {u.description}") - except Exception as e: - print(f"Error: {e}") - finally: - db.close() - -if __name__ == "__main__": - check_units() From 3409d0a1635db72a76b2c30a27b36dbda3a9ffc2 Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Fri, 26 Dec 2025 12:03:47 -0600 Subject: [PATCH 3/8] Update Keycloak server URL to include /kcauth in configuration files --- backend/.env.example | 2 +- backend/api/v1/modules/core/auth/service.py | 8 ++++---- backend/core/config.py | 4 ++-- backend/core/security.py | 2 +- docker-compose.prod.yml | 6 +----- docker-compose.yml | 3 ++- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index 61b7c01f..23cd9f7b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -12,7 +12,7 @@ CORE_DB_USER=postgres CORE_DB_PASSWORD=postgres # Keycloak -KEYCLOAK_SERVER_URL=http://localhost:8080 +KEYCLOAK_SERVER_URL=http://localhost:8080/kcauth KEYCLOAK_REALM=master KEYCLOAK_CLIENT_ID=anexo76-backend KEYCLOAK_CLIENT_SECRET=your-client-secret diff --git a/backend/api/v1/modules/core/auth/service.py b/backend/api/v1/modules/core/auth/service.py index 57293605..96153be7 100644 --- a/backend/api/v1/modules/core/auth/service.py +++ b/backend/api/v1/modules/core/auth/service.py @@ -31,7 +31,7 @@ class AuthService: def __init__(self, db: Session): self.db = db self.keycloak_openid = KeycloakOpenID( - server_url=settings.KEYCLOAK_SERVER_URL, + server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth", client_id=settings.KEYCLOAK_CLIENT_ID, realm_name=settings.KEYCLOAK_REALM, client_secret_key=settings.KEYCLOAK_CLIENT_SECRET, @@ -64,7 +64,7 @@ class AuthService: # Crear nueva instancia de KeycloakOpenID con el realm del tenant keycloak_client = KeycloakOpenID( - server_url=settings.KEYCLOAK_SERVER_URL, + server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth", client_id=settings.KEYCLOAK_CLIENT_ID, realm_name=tenant.keycloak_realm, client_secret_key=settings.KEYCLOAK_CLIENT_SECRET, @@ -78,7 +78,7 @@ class AuthService: # o buscar el usuario por username try: keycloak_admin = KeycloakAdmin( - server_url=settings.KEYCLOAK_SERVER_URL, + server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth", username=settings.KEYCLOAK_ADMIN_USERNAME, password=settings.KEYCLOAK_ADMIN_PASSWORD, realm_name=tenant.keycloak_realm, @@ -279,7 +279,7 @@ class AuthService: # Crear instancia de KeycloakAdmin para gestión de usuarios keycloak_admin = KeycloakAdmin( - server_url=settings.KEYCLOAK_SERVER_URL, + server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth", username=settings.KEYCLOAK_ADMIN_USERNAME, password=settings.KEYCLOAK_ADMIN_PASSWORD, realm_name=tenant.keycloak_realm, diff --git a/backend/core/config.py b/backend/core/config.py index fb19d6ef..c27954eb 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -24,7 +24,7 @@ class Settings(BaseSettings): CORE_DB_PASSWORD: str = "postgres" # Keycloak - KEYCLOAK_SERVER_URL: str = "http://localhost:8080" + KEYCLOAK_SERVER_URL: str = "http://localhost:8080/kcauth" KEYCLOAK_REALM: str = "master" KEYCLOAK_CLIENT_ID: str = "anexo76-backend" KEYCLOAK_CLIENT_SECRET: str = "" @@ -43,7 +43,7 @@ class Settings(BaseSettings): LICENSE_CHECK_ENABLED: bool = True model_config = SettingsConfigDict( - env_file=".env", case_sensitive=True, extra="ignore" + env_file=".env", case_sensitive=True, extra="ignore", env_file_encoding="utf-8" ) @property diff --git a/backend/core/security.py b/backend/core/security.py index 60834f26..9bb89b8b 100644 --- a/backend/core/security.py +++ b/backend/core/security.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) # Configuración de Keycloak keycloak_openid = KeycloakOpenID( - server_url=settings.KEYCLOAK_SERVER_URL, + server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth", client_id=settings.KEYCLOAK_CLIENT_ID, realm_name=settings.KEYCLOAK_REALM, client_secret_key=settings.KEYCLOAK_CLIENT_SECRET, diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 8a6b8f8e..c8f60a7c 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -151,7 +151,7 @@ services: - CORE_DB_NAME=${CORE_DB_NAME:-anexo76_core} - CORE_DB_USER=${CORE_DB_USER:-postgres} - CORE_DB_PASSWORD=${POSTGRES_APP_PASSWORD:-postgres} - - KEYCLOAK_SERVER_URL=${KEYCLOAK_SERVER_URL:-http://keycloak:8080} + - KEYCLOAK_SERVER_URL=${KEYCLOAK_SERVER_URL:-http://keycloak:8080/kcauth} - KEYCLOAK_REALM=${KEYCLOAK_REALM:-master} - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend} - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret} @@ -164,8 +164,6 @@ services: keycloak: condition: service_healthy volumes: - - ./backend:/app - - backend_cache:/app/__pycache__ - ./scripts/backend-entrypoint.sh:/entrypoint.sh:ro networks: - backend-net @@ -213,8 +211,6 @@ services: condition: service_healthy entrypoint: ["/frontend-entrypoint.sh"] volumes: - - ./frontend:/app - - frontend_node_modules:/app/node_modules - ./scripts/frontend-entrypoint.sh:/frontend-entrypoint.sh:ro networks: - frontend-net diff --git a/docker-compose.yml b/docker-compose.yml index 18215907..3e73fff6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -94,6 +94,7 @@ services: KC_PROXY_HEADERS: "xforwarded" KC_HEALTH_ENABLED: "true" KC_METRICS_ENABLED: "true" + KC_HOSTNAME_PATH: /kcauth KC_LOG_LEVEL: INFO JAVA_OPTS_APPEND: "-Xms256m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true" command: @@ -156,7 +157,7 @@ services: - CORE_DB_NAME=${CORE_DB_NAME:-anexo76_core} - CORE_DB_USER=${CORE_DB_USER:-postgres} - CORE_DB_PASSWORD=${POSTGRES_APP_PASSWORD:-postgres} - - KEYCLOAK_SERVER_URL=${KEYCLOAK_SERVER_URL:-http://keycloak:8080} + - KEYCLOAK_SERVER_URL=${KEYCLOAK_SERVER_URL:-http://keycloak:8080/kcauth} - KEYCLOAK_REALM=${KEYCLOAK_REALM:-master} - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend} - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret} From 69bf0c472bd5688fce1968f50db2aa39757770b8 Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Fri, 26 Dec 2025 14:21:00 -0600 Subject: [PATCH 4/8] Refactor middleware import path, update Docker configurations, and normalize API base URL --- backend/core/middleware.py | 2 +- docker-compose.prod.yml | 38 ++++++++++++++++++++++++-------------- frontend/Dockerfile.prod | 9 ++++++--- frontend/src/lib/api.ts | 3 ++- frontend/vite.config.ts | 6 +++++- 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/backend/core/middleware.py b/backend/core/middleware.py index 02731b37..f5d0df8a 100644 --- a/backend/core/middleware.py +++ b/backend/core/middleware.py @@ -124,7 +124,7 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware): db = CoreSessionLocal() try: # Importar aquí para evitar imports circulares - from api.v1.modules.a76.licenses.service import LicenseService + from api.v1.modules.core.licenses.service import LicenseService license_service = LicenseService(db) license_info = license_service.validate_license(tenant_id) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index c8f60a7c..4339e0a8 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -9,7 +9,7 @@ services: POSTGRES_PASSWORD: ${POSTGRES_APP_PASSWORD:-postgres} POSTGRES_INITDB_ARGS: "--encoding=UTF8" ports: - - "5432:5432" + - "5939:5432" volumes: - postgres_app_data:/var/lib/postgresql/data - ./scripts/postgres-app-entrypoint.sh:/docker-entrypoint-initdb.d/init-app.sh:ro @@ -45,7 +45,7 @@ services: POSTGRES_PASSWORD: ${POSTGRES_KEYCLOAK_PASSWORD:-postgres} POSTGRES_INITDB_ARGS: "--encoding=UTF8" ports: - - "5433:5432" + - "5233:5432" volumes: - postgres_keycloak_data:/var/lib/postgresql/data - ./scripts/postgres-keycloak-entrypoint.sh:/docker-entrypoint-initdb.d/init-keycloak.sh:ro @@ -94,12 +94,14 @@ services: KC_PROXY_HEADERS: "xforwarded" KC_HEALTH_ENABLED: "true" KC_METRICS_ENABLED: "true" + KC_HOSTNAME_PATH: /kcauth KC_LOG_LEVEL: INFO JAVA_OPTS_APPEND: "-Xms256m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true" command: - start-dev - --db=postgres - --db-url-host=postgres-keycloak + - --http-relative-path=/kcauth - --db-url-port=5432 - --db-url-database=keycloak - --db-username=postgres @@ -108,7 +110,7 @@ services: - --hostname-strict=false - --proxy-headers=xforwarded ports: - - "8080:8080" + - "8880:8080" - "9000:9000" depends_on: postgres-keycloak: @@ -120,11 +122,11 @@ services: - backend-net restart: unless-stopped healthcheck: - test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /health/ready HTTP/1.1\r\nhost: 127.0.0.1\r\nConnection: close\r\n\r\n' >&3; grep -q 'HTTP/1.1 200' <&3 || exit 1"] + test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /kcauth/health/ready HTTP/1.1\r\nhost: 127.0.0.1\r\nConnection: close\r\n\r\n' >&3; grep -q 'HTTP/1.1 200' <&3 || exit 1"] interval: 10s timeout: 5s retries: 30 - start_period: 90s + start_period: 30s logging: driver: "json-file" options: @@ -155,15 +157,17 @@ services: - KEYCLOAK_REALM=${KEYCLOAK_REALM:-master} - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend} - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret} - - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5173,http://localhost:3000} + - CORS_ORIGINS=${CORS_ORIGINS:-https://anexo76-dev.aduanasoft.com,http://localhost:3000} ports: - - "8000:8000" + - "3467:8000" depends_on: postgres-a76: condition: service_healthy keycloak: condition: service_healthy volumes: + - ./backend:/app + - backend_cache:/app/__pycache__ - ./scripts/backend-entrypoint.sh:/entrypoint.sh:ro networks: - backend-net @@ -191,21 +195,27 @@ services: # Frontend - SvelteKit frontend: - image: dev.aduanasoft.com/anexo76/frontend:latest + build: + context: ./frontend + dockerfile: Dockerfile.prod + args: + - BUILDKIT_INLINE_CACHE=1 + - VITE_API_URL=https://anexo76-dev.aduanasoft.com/api/ + - VITE_KEYCLOAK_URL=https://anexo76-dev.aduanasoft.com/kcauth/ container_name: anexo76-frontend environment: - NODE_ENV=${NODE_ENV:-development} - - VITE_API_URL=${VITE_API_URL:-http://localhost:8000/api/} + - VITE_API_URL=${VITE_API_URL:-https://anexo76-dev.aduanasoft.com/api} - INTERNAL_API_URL=${INTERNAL_API_URL:-http://backend:8000/api/} - - VITE_KEYCLOAK_URL=${VITE_KEYCLOAK_URL:-http://localhost:8080} + - VITE_KEYCLOAK_URL=${VITE_KEYCLOAK_URL:-https://anexo76-dev.aduanasoft.com/kcauth/} - VITE_KEYCLOAK_REALM=${VITE_KEYCLOAK_REALM:-master} - VITE_KEYCLOAK_CLIENT_ID=${VITE_KEYCLOAK_CLIENT_ID:-anexo76-frontend} - - KEYCLOAK_URL=${KEYCLOAK_URL:-http://keycloak:8080} + - KEYCLOAK_URL=${KEYCLOAK_URL:-http://keycloak:8080/kcauth} - KEYCLOAK_REALM=${KEYCLOAK_REALM:-master} - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend} - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-zRU5NuvUFtBSOuh7Kdc372AItoWGLgz9} ports: - - "5173:5173" + - "5111:5173" depends_on: backend: condition: service_healthy @@ -216,7 +226,7 @@ services: - frontend-net - auth-net restart: unless-stopped - command: ["pnpm", "run", "dev", "--", "--host", "0.0.0.0"] + command: ["node", "build"] healthcheck: test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:5173/ || exit 1"] interval: 15s @@ -253,4 +263,4 @@ networks: auth-net: driver: bridge frontend-net: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod index 1c5a55e6..754f6fed 100644 --- a/frontend/Dockerfile.prod +++ b/frontend/Dockerfile.prod @@ -33,6 +33,9 @@ FROM node:22-alpine AS runtime WORKDIR /app +RUN npm config set strict-ssl false && \ + npm install -g pnpm + # Crear usuario no-root para seguridad antes de copiar con --chown RUN addgroup -g 1001 -S nodejs RUN adduser -S svelte -u 1001 @@ -45,12 +48,12 @@ COPY --from=build --chown=svelte:nodejs /app/node_modules ./node_modules USER svelte # Puerto para SvelteKit con adapter-node -EXPOSE 3000 +EXPOSE 5173 # Variables de entorno ENV NODE_ENV=production -ENV PORT=3000 +ENV PORT=5173 ENV HOST=0.0.0.0 # Ejecutar aplicación con Node.js -CMD ["node", "build"] \ No newline at end of file +CMD ["node", "build"] diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 5221c628..5959b930 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -4,7 +4,8 @@ import { getToken } from './auth'; import { browser } from '$app/environment'; -const API_BASE_URL = import.meta.env.VITE_API_URL; +// Normalize API_BASE_URL to remove trailing slash +const API_BASE_URL = (import.meta.env.VITE_API_URL || '').replace(/\/+$/, ''); export interface ApiResponse { data?: T; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index c7bff032..346c0f22 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -6,7 +6,11 @@ import { sveltekit } from '@sveltejs/kit/vite'; export default defineConfig({ server: { port: 5173, // fija el puerto - host: true // escucha en 0.0.0.0 + host: true, // escucha en 0.0.0.0 + allowedHosts: [ + 'anexo76-dev.aduanasoft.com', + // 'otro-host.com' si necesitas más + ], }, plugins: [ tailwindcss(), From b939526302efc5f4786a51904bb3d7efecfd82c0 Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Fri, 26 Dec 2025 17:48:46 -0600 Subject: [PATCH 5/8] Update Keycloak configuration and normalize API endpoint paths in invoices API --- docker-compose.yml | 7 ++-- .../src/lib/api/dashboard/a76/invoices.ts | 22 ++++++------- .../invoices/edit/[id]/+page.server.ts | 33 +++++-------------- 3 files changed, 24 insertions(+), 38 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3e73fff6..655340d1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -99,6 +99,7 @@ services: JAVA_OPTS_APPEND: "-Xms256m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true" command: - start-dev + - --http-relative-path=/kcauth - --db=postgres - --db-url-host=postgres-keycloak - --db-url-port=5432 @@ -121,7 +122,7 @@ services: - backend-net restart: unless-stopped healthcheck: - test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /health/ready HTTP/1.1\r\nhost: 127.0.0.1\r\nConnection: close\r\n\r\n' >&3; grep -q 'HTTP/1.1 200' <&3 || exit 1"] + test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /kcauth/health/ready HTTP/1.1\r\nhost: 127.0.0.1\r\nConnection: close\r\n\r\n' >&3; grep -q 'HTTP/1.1 200' <&3 || exit 1"] interval: 10s timeout: 5s retries: 30 @@ -210,10 +211,10 @@ services: - NODE_ENV=${NODE_ENV:-development} - VITE_API_URL=${VITE_API_URL:-http://localhost:8000/api/} - INTERNAL_API_URL=${INTERNAL_API_URL:-http://backend:8000/api/} - - VITE_KEYCLOAK_URL=${VITE_KEYCLOAK_URL:-http://localhost:8080} + - VITE_KEYCLOAK_URL=${VITE_KEYCLOAK_URL:-http://localhost:8080/kcauth} - VITE_KEYCLOAK_REALM=${VITE_KEYCLOAK_REALM:-master} - VITE_KEYCLOAK_CLIENT_ID=${VITE_KEYCLOAK_CLIENT_ID:-anexo76-frontend} - - KEYCLOAK_URL=${KEYCLOAK_URL:-http://keycloak:8080} + - KEYCLOAK_URL=${KEYCLOAK_URL:-http://keycloak:8080/kcauth} - KEYCLOAK_REALM=${KEYCLOAK_REALM:-master} - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend} - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-zRU5NuvUFtBSOuh7Kdc372AItoWGLgz9} diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index 78043d2e..e245f335 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -325,7 +325,7 @@ export const invoicesApi = { }); } - return api.get(`/v1/a76/invoices?${params.toString()}`); + return api.get(`/v1/a76/invoices/?${params.toString()}`); }, /** @@ -345,7 +345,7 @@ export const invoicesApi = { const params = new URLSearchParams({ company_id: companyId.toString() }); - return api.post(`/v1/a76/invoices?${params.toString()}`, data); + return api.post(`/v1/a76/invoices/?${params.toString()}`, data); }, /** @@ -378,21 +378,21 @@ export const invoicesApi = { const params = new URLSearchParams({ company_id: companyId.toString() }); - return api.get(`/v1/a76/invoices/${invoiceId}/logistics?${params.toString()}`); + return api.get(`/v1/a76/invoices/${invoiceId}/logistics/?${params.toString()}`); }, create: (invoiceId: number, companyId: number, data: Omit) => { const params = new URLSearchParams({ company_id: companyId.toString() }); - return api.post(`/v1/a76/invoices/${invoiceId}/logistics?${params.toString()}`, data); + return api.post(`/v1/a76/invoices/${invoiceId}/logistics/?${params.toString()}`, data); }, delete: (invoiceId: number, logisticsId: number, companyId: number) => { const params = new URLSearchParams({ company_id: companyId.toString() }); - return api.delete(`/v1/a76/invoices/${invoiceId}/logistics/${logisticsId}?${params.toString()}`); + return api.delete(`/v1/a76/invoices/${invoiceId}/logistics/${logisticsId}/?${params.toString()}`); } }, @@ -404,21 +404,21 @@ export const invoicesApi = { const params = new URLSearchParams({ company_id: companyId.toString() }); - return api.get(`/v1/a76/invoices/${invoiceId}/details?${params.toString()}`); + return api.get(`/v1/a76/invoices/${invoiceId}/details/?${params.toString()}`); }, create: (invoiceId: number, companyId: number, data: Omit) => { const params = new URLSearchParams({ company_id: companyId.toString() }); - return api.post(`/v1/a76/invoices/${invoiceId}/details?${params.toString()}`, data); + return api.post(`/v1/a76/invoices/${invoiceId}/details/?${params.toString()}`, data); }, delete: (invoiceId: number, detailId: number, companyId: number) => { const params = new URLSearchParams({ company_id: companyId.toString() }); - return api.delete(`/v1/a76/invoices/${invoiceId}/details/${detailId}?${params.toString()}`); + return api.delete(`/v1/a76/invoices/${invoiceId}/details/${detailId}/?${params.toString()}`); } }, @@ -430,21 +430,21 @@ export const invoicesApi = { const params = new URLSearchParams({ company_id: companyId.toString() }); - return api.get(`/v1/a76/invoices/${invoiceId}/collections?${params.toString()}`); + return api.get(`/v1/a76/invoices/${invoiceId}/collections/?${params.toString()}`); }, create: (invoiceId: number, companyId: number, data: Omit) => { const params = new URLSearchParams({ company_id: companyId.toString() }); - return api.post(`/v1/a76/invoices/${invoiceId}/collections?${params.toString()}`, data); + return api.post(`/v1/a76/invoices/${invoiceId}/collections/?${params.toString()}`, data); }, delete: (invoiceId: number, collectionId: number, companyId: number) => { const params = new URLSearchParams({ company_id: companyId.toString() }); - return api.delete(`/v1/a76/invoices/${invoiceId}/collections/${collectionId}?${params.toString()}`); + return api.delete(`/v1/a76/invoices/${invoiceId}/collections/${collectionId}/?${params.toString()}`); } } }; diff --git a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts index 11d7de3a..eb48c4d6 100644 --- a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts +++ b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts @@ -31,14 +31,14 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { // Cargar datos de referencia necesarios const invoiceTypesPromise = authenticatedFetch( - 'v1/public/refrence_data/invoice-types?page=1&page_size=100', + 'v1/public/refrence_data/invoice-types/?page=1&page_size=100', {}, cookies, fetch ); const customsBrokersPromise = authenticatedFetch( - `v1/a76/customs-brokers?company_id=${companyId}&page=1&page_size=1000`, + `v1/a76/customs-brokers/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch @@ -46,14 +46,14 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { // Cargar clientes y proveedores const clientsPromise = authenticatedFetch( - `v1/a76/clients-providers?company_id=${companyId}&type=client&page=1&page_size=1000`, + `v1/a76/clients-providers/?company_id=${companyId}&type=client&page=1&page_size=1000`, {}, cookies, fetch ); const providersPromise = authenticatedFetch( - `v1/a76/clients-providers?company_id=${companyId}&type=provider&page=1&page_size=1000`, + `v1/a76/clients-providers/?company_id=${companyId}&type=provider&page=1&page_size=1000`, {}, cookies, fetch @@ -61,42 +61,35 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { // Cargar tipos de moneda, transporte, etc. const currencyTypesPromise = authenticatedFetch( - 'v1/public/refrence_data/currency-types?page=1&page_size=100', + 'v1/public/refrence_data/currency-types/?page=1&page_size=100', {}, cookies, fetch ); const transportTypesPromise = authenticatedFetch( - 'v1/public/refrence_data/transport-types?page=1&page_size=100', + 'v1/public/refrence_data/transport-types/?page=1&page_size=100', {}, cookies, fetch ); const sealsPromise = authenticatedFetch( - 'v1/public/refrence_data/seals?page=1&page_size=100', + `v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch ); const incotermsPromise = authenticatedFetch( - 'v1/public/refrence_data/incoterms?page=1&page_size=100', - {}, - cookies, - fetch - ); - - const enclosurePromise = authenticatedFetch( - 'v1/public/refrence_data/enclosure?page=1&page_size=100', + 'v1/public/refrence_data/incoterms/?page=1&page_size=100', {}, cookies, fetch ); const pedimentosPromise = authenticatedFetch( - `v1/a76/pedimentos?company_id=${companyId}&page=1&page_size=100`, + `v1/a76/pedimentos/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch @@ -113,7 +106,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { transportTypesResponse, sealsResponse, incotermsResponse, - enclosureResponse, pedimentosResponse ] = await Promise.all([ invoiceTypesPromise, @@ -124,7 +116,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { transportTypesPromise, sealsPromise, incotermsPromise, - enclosurePromise, pedimentosPromise ]); @@ -136,7 +127,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] }; const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] }; const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] }; - const enclosure = enclosureResponse.ok ? await enclosureResponse.json() : { items: [] }; const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] }; return { @@ -151,7 +141,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { transportTypes: transportTypes.items || [], seals: seals.items || [], incoterms: incoterms.items || [], - enclosure: enclosure.items || [], pedimentos: pedimentos.items || [], // Filtros desde query parameters para preselección filters: { @@ -191,7 +180,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { transportTypesResponse, sealsResponse, incotermsResponse, - enclosureResponse, pedimentosResponse ] = await Promise.all([ invoiceTypesPromise, @@ -202,7 +190,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { transportTypesPromise, sealsPromise, incotermsPromise, - enclosurePromise, pedimentosPromise ]); @@ -214,7 +201,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] }; const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] }; const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] }; - const enclosure = enclosureResponse.ok ? await enclosureResponse.json() : { items: [] }; const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] }; return { @@ -229,7 +215,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { transportTypes: transportTypes.items || [], seals: seals.items || [], incoterms: incoterms.items || [], - enclosure: enclosure.items || [], pedimentos: pedimentos.items || [], // Filtros desde query parameters para preselección filters: { From 8d0a895c8d4a0cb28ffb41d96fff4144d4a94392 Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Mon, 29 Dec 2025 12:53:06 -0600 Subject: [PATCH 6/8] Refactor invoice edit flow: enhance error handling, load reference data, and improve token management --- frontend/src/lib/server/api.ts | 87 +++++++----- .../api/invoices/[id]/edit-data/+server.ts | 80 +++++++++++ .../api/invoices/reference-data/+server.ts | 67 +++++++++ .../invoices/edit/[id]/+page.server.ts | 121 +++++++++------- .../dashboard/invoices/edit/[id]/+page.svelte | 27 ++-- .../dashboard/invoices/edit/[id]/+page.ts | 132 ++++++++++++++++++ 6 files changed, 421 insertions(+), 93 deletions(-) create mode 100644 frontend/src/routes/api/invoices/[id]/edit-data/+server.ts create mode 100644 frontend/src/routes/api/invoices/reference-data/+server.ts create mode 100644 frontend/src/routes/dashboard/invoices/edit/[id]/+page.ts diff --git a/frontend/src/lib/server/api.ts b/frontend/src/lib/server/api.ts index 4c607d15..41ec018b 100644 --- a/frontend/src/lib/server/api.ts +++ b/frontend/src/lib/server/api.ts @@ -134,48 +134,63 @@ export async function authenticatedFetch( fetch: typeof globalThis.fetch, redirectUrl?: string ): Promise { - const baseUrl = getServerApiUrl(); - let { accessToken } = getAuthTokens(cookies); + try { + const baseUrl = getServerApiUrl(); + let { accessToken } = getAuthTokens(cookies); - // Si no hay token, redirigir o lanzar error - if (!accessToken) { - if (redirectUrl) { - throw redirect(303, redirectUrl); - } - throw new Error('No access token available'); - } - - // Construir URL completa - const url = endpoint.startsWith('http') ? endpoint : `${baseUrl}${endpoint}`; - - // Realizar la petición inicial - const headers = createAuthHeaders(accessToken, options.headers as Record); - let response = await fetch(url, { - ...options, - headers - }); - - // Si es 401, intentar refrescar el token - if (response.status === 401) { - const newToken = await refreshAccessToken(cookies, fetch); - - if (newToken) { - // Reintentar la petición con el nuevo token - const newHeaders = createAuthHeaders(newToken, options.headers as Record); - response = await fetch(url, { - ...options, - headers: newHeaders - }); - } else { - // No se pudo refrescar, limpiar y redirigir - clearAuthTokens(cookies); + // Si no hay token, redirigir o lanzar error + if (!accessToken) { if (redirectUrl) { throw redirect(303, redirectUrl); } + throw new Error('No access token available'); } - } - return response; + // Construir URL completa + const url = endpoint.startsWith('http') ? endpoint : `${baseUrl}${endpoint}`; + + // Realizar la petición inicial + const headers = createAuthHeaders(accessToken, options.headers as Record); + let response = await fetch(url, { + ...options, + headers + }); + + // Si es 401, intentar refrescar el token + if (response.status === 401) { + const newToken = await refreshAccessToken(cookies, fetch); + + if (newToken) { + // Reintentar la petición con el nuevo token + const newHeaders = createAuthHeaders(newToken, options.headers as Record); + response = await fetch(url, { + ...options, + headers: newHeaders + }); + } else { + // No se pudo refrescar, limpiar y redirigir + clearAuthTokens(cookies); + if (redirectUrl) { + throw redirect(303, redirectUrl); + } + } + } + + return response; + } catch (error) { + // Si es un redirect, re-lanzarlo + if (error && typeof error === 'object' && 'status' in error && 'location' in error) { + throw error; + } + + console.error('🔴 [API] Error en authenticatedFetch:', endpoint, error); + + // Retornar una respuesta de error simulada en lugar de lanzar + return new Response(JSON.stringify({ error: 'Network error', details: String(error) }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } } /** diff --git a/frontend/src/routes/api/invoices/[id]/edit-data/+server.ts b/frontend/src/routes/api/invoices/[id]/edit-data/+server.ts new file mode 100644 index 00000000..48c5cd3f --- /dev/null +++ b/frontend/src/routes/api/invoices/[id]/edit-data/+server.ts @@ -0,0 +1,80 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { getAuthTokens, getActiveCompanyId, authenticatedFetch } from '$lib/server/api'; + +export const GET: RequestHandler = async ({ params, cookies, fetch }) => { + const { accessToken } = getAuthTokens(cookies); + + if (!accessToken) { + throw error(401, 'Not authenticated'); + } + + const companyId = await getActiveCompanyId(cookies, fetch); + + if (!companyId) { + throw error(400, 'No company selected'); + } + + const invoiceId = parseInt(params.id); + if (isNaN(invoiceId)) { + throw error(400, 'Invalid invoice ID'); + } + + try { + // Cargar la factura y datos de referencia en paralelo + const [ + invoiceResponse, + invoiceTypesResponse, + customsBrokersResponse, + clientsResponse, + providersResponse, + currencyTypesResponse, + transportTypesResponse, + sealsResponse, + incotermsResponse, + pedimentosResponse + ] = await Promise.all([ + authenticatedFetch(`v1/a76/invoices/${invoiceId}?company_id=${companyId}`, {}, cookies, fetch), + authenticatedFetch('v1/public/refrence_data/invoice-types/?page=1&page_size=100', {}, cookies, fetch), + authenticatedFetch(`v1/a76/customs-brokers/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch), + authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=client&page=1&page_size=1000`, {}, cookies, fetch), + authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=provider&page=1&page_size=1000`, {}, cookies, fetch), + authenticatedFetch('v1/public/refrence_data/currency-types/?page=1&page_size=100', {}, cookies, fetch), + authenticatedFetch('v1/public/refrence_data/transport-types/?page=1&page_size=100', {}, cookies, fetch), + authenticatedFetch(`v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch), + authenticatedFetch('v1/public/refrence_data/incoterms/?page=1&page_size=100', {}, cookies, fetch), + authenticatedFetch(`v1/a76/pedimentos/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch) + ]); + + if (!invoiceResponse.ok) { + throw error(invoiceResponse.status, 'Error loading invoice'); + } + + const invoice = await invoiceResponse.json(); + const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] }; + const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] }; + const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] }; + const providers = providersResponse.ok ? await providersResponse.json() : { items: [] }; + const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] }; + const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] }; + const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] }; + const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] }; + const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] }; + + return json({ + invoice, + invoiceTypes: invoiceTypes.items || [], + customsBrokers: customsBrokers.items || [], + clients: clients.items || [], + providers: providers.items || [], + currencyTypes: currencyTypes.items || [], + transportTypes: transportTypes.items || [], + seals: seals.items || [], + incoterms: incoterms.items || [], + pedimentos: pedimentos.items || [] + }); + } catch (err) { + console.error('Error loading invoice edit data:', err); + throw error(500, 'Error loading invoice'); + } +}; diff --git a/frontend/src/routes/api/invoices/reference-data/+server.ts b/frontend/src/routes/api/invoices/reference-data/+server.ts new file mode 100644 index 00000000..97c0e9ab --- /dev/null +++ b/frontend/src/routes/api/invoices/reference-data/+server.ts @@ -0,0 +1,67 @@ +import type { RequestHandler } from './$types'; +import { json } from '@sveltejs/kit'; +import { getAuthTokens, getActiveCompanyId, authenticatedFetch } from '$lib/server/api'; + +export const GET: RequestHandler = async ({ cookies, fetch }) => { + const { accessToken } = getAuthTokens(cookies); + + if (!accessToken) { + return json({ error: 'Not authenticated' }, { status: 401 }); + } + + const companyId = await getActiveCompanyId(cookies, fetch); + + if (!companyId) { + return json({ error: 'No company selected' }, { status: 400 }); + } + + try { + // Cargar todos los datos de referencia en paralelo + const [ + invoiceTypesResponse, + customsBrokersResponse, + clientsResponse, + providersResponse, + currencyTypesResponse, + transportTypesResponse, + sealsResponse, + incotermsResponse, + pedimentosResponse + ] = await Promise.all([ + authenticatedFetch('v1/public/refrence_data/invoice-types/?page=1&page_size=100', {}, cookies, fetch), + authenticatedFetch(`v1/a76/customs-brokers/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch), + authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=client&page=1&page_size=1000`, {}, cookies, fetch), + authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=provider&page=1&page_size=1000`, {}, cookies, fetch), + authenticatedFetch('v1/public/refrence_data/currency-types/?page=1&page_size=100', {}, cookies, fetch), + authenticatedFetch('v1/public/refrence_data/transport-types/?page=1&page_size=100', {}, cookies, fetch), + authenticatedFetch(`v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch), + authenticatedFetch('v1/public/refrence_data/incoterms/?page=1&page_size=100', {}, cookies, fetch), + authenticatedFetch(`v1/a76/pedimentos/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch) + ]); + + const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] }; + const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] }; + const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] }; + const providers = providersResponse.ok ? await providersResponse.json() : { items: [] }; + const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] }; + const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] }; + const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] }; + const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] }; + const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] }; + + return json({ + invoiceTypes: invoiceTypes.items || [], + customsBrokers: customsBrokers.items || [], + clients: clients.items || [], + providers: providers.items || [], + currencyTypes: currencyTypes.items || [], + transportTypes: transportTypes.items || [], + seals: seals.items || [], + incoterms: incoterms.items || [], + pedimentos: pedimentos.items || [] + }); + } catch (err) { + console.error('Error loading reference data:', err); + return json({ error: 'Error loading reference data' }, { status: 500 }); + } +}; diff --git a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts index eb48c4d6..4321a3c7 100644 --- a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts +++ b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts @@ -97,57 +97,80 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { // Si el ID es "new", es una creación if (params.id === 'new') { - const [ - invoiceTypesResponse, - customsBrokersResponse, - clientsResponse, - providersResponse, - currencyTypesResponse, - transportTypesResponse, - sealsResponse, - incotermsResponse, - pedimentosResponse - ] = await Promise.all([ - invoiceTypesPromise, - customsBrokersPromise, - clientsPromise, - providersPromise, - currencyTypesPromise, - transportTypesPromise, - sealsPromise, - incotermsPromise, - pedimentosPromise - ]); + try { + const [ + invoiceTypesResponse, + customsBrokersResponse, + clientsResponse, + providersResponse, + currencyTypesResponse, + transportTypesResponse, + sealsResponse, + incotermsResponse, + pedimentosResponse + ] = await Promise.all([ + invoiceTypesPromise, + customsBrokersPromise, + clientsPromise, + providersPromise, + currencyTypesPromise, + transportTypesPromise, + sealsPromise, + incotermsPromise, + pedimentosPromise + ]); - const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] }; - const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] }; - const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] }; - const providers = providersResponse.ok ? await providersResponse.json() : { items: [] }; - const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] }; - const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] }; - const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] }; - const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] }; - const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] }; + const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] }; + const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] }; + const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] }; + const providers = providersResponse.ok ? await providersResponse.json() : { items: [] }; + const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] }; + const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] }; + const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] }; + const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] }; + const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] }; - return { - invoice: null, - invoiceId: null, - isCreate: true, - invoiceTypes: invoiceTypes.items || [], - customsBrokers: customsBrokers.items || [], - clients: clients.items || [], - providers: providers.items || [], - currencyTypes: currencyTypes.items || [], - transportTypes: transportTypes.items || [], - seals: seals.items || [], - incoterms: incoterms.items || [], - pedimentos: pedimentos.items || [], - // Filtros desde query parameters para preselección - filters: { - operation_type: parsedOperationType, - invoice_type: invoiceTypeParam || null - } - }; + return { + invoice: null, + invoiceId: null, + isCreate: true, + invoiceTypes: invoiceTypes.items || [], + customsBrokers: customsBrokers.items || [], + clients: clients.items || [], + providers: providers.items || [], + currencyTypes: currencyTypes.items || [], + transportTypes: transportTypes.items || [], + seals: seals.items || [], + incoterms: incoterms.items || [], + pedimentos: pedimentos.items || [], + // Filtros desde query parameters para preselección + filters: { + operation_type: parsedOperationType, + invoice_type: invoiceTypeParam || null + } + }; + } catch (err) { + console.error('Error loading data for new invoice:', err); + // En caso de error, devolver estructura mínima para que la página pueda cargar + return { + invoice: null, + invoiceId: null, + isCreate: true, + invoiceTypes: [], + customsBrokers: [], + clients: [], + providers: [], + currencyTypes: [], + transportTypes: [], + seals: [], + incoterms: [], + pedimentos: [], + filters: { + operation_type: parsedOperationType, + invoice_type: invoiceTypeParam || null + } + }; + } } const invoiceId = parseInt(params.id); diff --git a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte index 3bc2c203..3a35472a 100644 --- a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte @@ -1,4 +1,6 @@