Merge pull request 'feature/production' (#33) from feature/production into development
Reviewed-on: ADUANASOFT/anexo76#33
This commit is contained in:
15
README.md
15
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
30
check_db.py
30
check_db.py
@@ -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()
|
||||
23
check_uom.py
23
check_uom.py
@@ -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()
|
||||
266
docker-compose.prod.yml
Normal file
266
docker-compose.prod.yml
Normal file
@@ -0,0 +1,266 @@
|
||||
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:
|
||||
- "5939: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:
|
||||
- "5233: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_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
|
||||
- --db-password=${POSTGRES_KEYCLOAK_PASSWORD:-postgres}
|
||||
- --http-enabled=true
|
||||
- --hostname-strict=false
|
||||
- --proxy-headers=xforwarded
|
||||
ports:
|
||||
- "8880: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 /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: 30s
|
||||
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/kcauth}
|
||||
- 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:-https://anexo76-dev.aduanasoft.com,http://localhost:3000}
|
||||
ports:
|
||||
- "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
|
||||
- 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:
|
||||
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:-https://anexo76-dev.aduanasoft.com/api}
|
||||
- INTERNAL_API_URL=${INTERNAL_API_URL:-http://backend:8000/api/}
|
||||
- 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/kcauth}
|
||||
- KEYCLOAK_REALM=${KEYCLOAK_REALM:-master}
|
||||
- KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend}
|
||||
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-zRU5NuvUFtBSOuh7Kdc372AItoWGLgz9}
|
||||
ports:
|
||||
- "5111:5173"
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
entrypoint: ["/frontend-entrypoint.sh"]
|
||||
volumes:
|
||||
- ./scripts/frontend-entrypoint.sh:/frontend-entrypoint.sh:ro
|
||||
networks:
|
||||
- frontend-net
|
||||
- auth-net
|
||||
restart: unless-stopped
|
||||
command: ["node", "build"]
|
||||
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
|
||||
@@ -94,10 +94,12 @@ 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
|
||||
- --http-relative-path=/kcauth
|
||||
- --db=postgres
|
||||
- --db-url-host=postgres-keycloak
|
||||
- --db-url-port=5432
|
||||
@@ -120,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
|
||||
@@ -156,13 +158,13 @@ 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}
|
||||
- CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5173,http://localhost:3000}
|
||||
ports:
|
||||
- "5050:8000"
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
postgres-a76:
|
||||
condition: service_healthy
|
||||
@@ -209,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}
|
||||
|
||||
59
frontend/Dockerfile.prod
Normal file
59
frontend/Dockerfile.prod
Normal file
@@ -0,0 +1,59 @@
|
||||
# ==========================
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
# 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 5173
|
||||
|
||||
# Variables de entorno
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=5173
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
# Ejecutar aplicación con Node.js
|
||||
CMD ["node", "build"]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
23
frontend/pnpm-lock.yaml
generated
23
frontend/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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<T = any> {
|
||||
data?: T;
|
||||
|
||||
@@ -325,7 +325,7 @@ export const invoicesApi = {
|
||||
});
|
||||
}
|
||||
|
||||
return api.get<InvoiceListResponse>(`/v1/a76/invoices?${params.toString()}`);
|
||||
return api.get<InvoiceListResponse>(`/v1/a76/invoices/?${params.toString()}`);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -345,7 +345,7 @@ export const invoicesApi = {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<Invoice>(`/v1/a76/invoices?${params.toString()}`, data);
|
||||
return api.post<Invoice>(`/v1/a76/invoices/?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -378,21 +378,21 @@ export const invoicesApi = {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<InvoiceLogistics[]>(`/v1/a76/invoices/${invoiceId}/logistics?${params.toString()}`);
|
||||
return api.get<InvoiceLogistics[]>(`/v1/a76/invoices/${invoiceId}/logistics/?${params.toString()}`);
|
||||
},
|
||||
|
||||
create: (invoiceId: number, companyId: number, data: Omit<InvoiceLogistics, 'id' | 'invoice_id'>) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<InvoiceLogistics>(`/v1/a76/invoices/${invoiceId}/logistics?${params.toString()}`, data);
|
||||
return api.post<InvoiceLogistics>(`/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<InvoiceSalesDetails[]>(`/v1/a76/invoices/${invoiceId}/details?${params.toString()}`);
|
||||
return api.get<InvoiceSalesDetails[]>(`/v1/a76/invoices/${invoiceId}/details/?${params.toString()}`);
|
||||
},
|
||||
|
||||
create: (invoiceId: number, companyId: number, data: Omit<InvoiceSalesDetails, 'id' | 'invoice_id'>) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<InvoiceSalesDetails>(`/v1/a76/invoices/${invoiceId}/details?${params.toString()}`, data);
|
||||
return api.post<InvoiceSalesDetails>(`/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<InvoiceCollections[]>(`/v1/a76/invoices/${invoiceId}/collections?${params.toString()}`);
|
||||
return api.get<InvoiceCollections[]>(`/v1/a76/invoices/${invoiceId}/collections/?${params.toString()}`);
|
||||
},
|
||||
|
||||
create: (invoiceId: number, companyId: number, data: Omit<InvoiceCollections, 'id' | 'invoice_id'>) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<InvoiceCollections>(`/v1/a76/invoices/${invoiceId}/collections?${params.toString()}`, data);
|
||||
return api.post<InvoiceCollections>(`/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()}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -134,48 +134,63 @@ export async function authenticatedFetch(
|
||||
fetch: typeof globalThis.fetch,
|
||||
redirectUrl?: string
|
||||
): Promise<Response> {
|
||||
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<string, string>);
|
||||
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<string, string>);
|
||||
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<string, string>);
|
||||
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<string, string>);
|
||||
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' }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
@@ -104,61 +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,
|
||||
enclosureResponse,
|
||||
pedimentosResponse
|
||||
] = await Promise.all([
|
||||
invoiceTypesPromise,
|
||||
customsBrokersPromise,
|
||||
clientsPromise,
|
||||
providersPromise,
|
||||
currencyTypesPromise,
|
||||
transportTypesPromise,
|
||||
sealsPromise,
|
||||
incotermsPromise,
|
||||
enclosurePromise,
|
||||
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 enclosure = enclosureResponse.ok ? await enclosureResponse.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 || [],
|
||||
enclosure: enclosure.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);
|
||||
@@ -191,7 +203,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
||||
transportTypesResponse,
|
||||
sealsResponse,
|
||||
incotermsResponse,
|
||||
enclosureResponse,
|
||||
pedimentosResponse
|
||||
] = await Promise.all([
|
||||
invoiceTypesPromise,
|
||||
@@ -202,7 +213,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
||||
transportTypesPromise,
|
||||
sealsPromise,
|
||||
incotermsPromise,
|
||||
enclosurePromise,
|
||||
pedimentosPromise
|
||||
]);
|
||||
|
||||
@@ -214,7 +224,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 +238,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: {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
@@ -17,8 +19,6 @@
|
||||
LoaderCircle,
|
||||
Save
|
||||
} from 'lucide-svelte';
|
||||
import { useSidebar } from '$lib/components/ui/sidebar/context.svelte';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
// Importar los componentes de cada pestaña
|
||||
import GeneralTabForm from '$lib/components/dashboard/invoices/edit/general-tab-form.svelte';
|
||||
@@ -34,8 +34,20 @@
|
||||
import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
|
||||
import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
|
||||
|
||||
// Get sidebar context
|
||||
const sidebar = useSidebar();
|
||||
// Cargar companyStore solo en el cliente - no usamos sidebar en esta página
|
||||
let companyStore: any = $state(undefined);
|
||||
let mounted = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const companyStoreModule = await import('$lib/stores/company.svelte');
|
||||
companyStore = companyStoreModule.companyStore;
|
||||
mounted = true;
|
||||
} catch (err) {
|
||||
console.error('Error loading client modules:', err);
|
||||
mounted = true;
|
||||
}
|
||||
});
|
||||
|
||||
interface ExtendedPageData {
|
||||
invoiceId?: number | null;
|
||||
@@ -471,7 +483,7 @@
|
||||
|
||||
if (data.isCreate) {
|
||||
// Crear nueva factura con todos sus sub-recursos
|
||||
const response = await invoicesApi.create(companyStore.activeCompany?.id || 0, payload as CreateInvoiceData);
|
||||
const response = await invoicesApi.create(companyStore?.activeCompany?.id || 0, payload as CreateInvoiceData);
|
||||
if (response.error) {
|
||||
const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear la factura';
|
||||
throw new Error(errorMsg);
|
||||
@@ -484,7 +496,7 @@
|
||||
return;
|
||||
} else {
|
||||
// Actualizar factura existente con todos sus sub-recursos
|
||||
const response = await invoicesApi.update(invoiceId!, companyStore.activeCompany?.id || 0, payload as UpdateInvoiceData);
|
||||
const response = await invoicesApi.update(invoiceId!, companyStore?.activeCompany?.id || 0, payload as UpdateInvoiceData);
|
||||
if (response.error) throw new Error(response.error);
|
||||
}
|
||||
|
||||
@@ -635,8 +647,7 @@
|
||||
|
||||
<!-- Footer fijo en la parte inferior -->
|
||||
<div
|
||||
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5] transition-[left] duration-200 ease-linear"
|
||||
style:left={sidebar.isMobile ? '0' : (sidebar.open ? 'var(--sidebar-width)' : '0')}
|
||||
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5]"
|
||||
>
|
||||
<div class="px-4 py-4 space-y-4 max-w-[1400px] mx-auto">
|
||||
<!-- Tabs Navigation -->
|
||||
|
||||
132
frontend/src/routes/dashboard/invoices/edit/[id]/+page.ts
Normal file
132
frontend/src/routes/dashboard/invoices/edit/[id]/+page.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
// Deshabilitar SSR para esta página debido al layout de dashboard que usa stores del cliente
|
||||
export const ssr = false;
|
||||
|
||||
export const load: PageLoad = async ({ params, url, parent }) => {
|
||||
// Obtener datos del layout padre
|
||||
const parentData = await parent();
|
||||
|
||||
const operationTypeParam = url.searchParams.get('operation_type');
|
||||
const invoiceTypeParam = url.searchParams.get('invoice_type');
|
||||
|
||||
let parsedOperationType: number | null = null;
|
||||
if (operationTypeParam) {
|
||||
const parsed = parseInt(operationTypeParam, 10);
|
||||
if (!isNaN(parsed)) {
|
||||
parsedOperationType = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
// Si el ID es "new", cargar datos de referencia
|
||||
if (params.id === 'new') {
|
||||
try {
|
||||
// Llamar a la ruta de servidor que ya existe
|
||||
const response = await fetch(`/api-sveltekit/invoices/reference-data`);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Error loading reference data:', response.status);
|
||||
// Retornar estructura vacía en caso de error
|
||||
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 data = await response.json();
|
||||
|
||||
return {
|
||||
invoice: null,
|
||||
invoiceId: null,
|
||||
isCreate: true,
|
||||
invoiceTypes: data.invoiceTypes || [],
|
||||
customsBrokers: data.customsBrokers || [],
|
||||
clients: data.clients || [],
|
||||
providers: data.providers || [],
|
||||
currencyTypes: data.currencyTypes || [],
|
||||
transportTypes: data.transportTypes || [],
|
||||
seals: data.seals || [],
|
||||
incoterms: data.incoterms || [],
|
||||
pedimentos: data.pedimentos || [],
|
||||
filters: {
|
||||
operation_type: parsedOperationType,
|
||||
invoice_type: invoiceTypeParam || null
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Error loading data for new invoice:', err);
|
||||
return {
|
||||
invoice: null,
|
||||
invoiceId: null,
|
||||
isCreate: true,
|
||||
invoiceTypes: [],
|
||||
customsBrokers: [],
|
||||
clients: [],
|
||||
providers: [],
|
||||
currencyTypes: [],
|
||||
transportTypes: [],
|
||||
seals: [],
|
||||
incoterms: [],
|
||||
pedimentos: [],
|
||||
filters: {
|
||||
operation_type: parsedOperationType,
|
||||
invoice_type: invoiceTypeParam || null
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Si es un ID numérico, cargar la factura
|
||||
const invoiceId = parseInt(params.id);
|
||||
if (isNaN(invoiceId)) {
|
||||
throw error(400, 'ID de factura inválido');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api-sveltekit/invoices/${invoiceId}/edit-data`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw error(response.status, 'Error al cargar la factura');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
invoice: data.invoice,
|
||||
invoiceId,
|
||||
isCreate: false,
|
||||
invoiceTypes: data.invoiceTypes || [],
|
||||
customsBrokers: data.customsBrokers || [],
|
||||
clients: data.clients || [],
|
||||
providers: data.providers || [],
|
||||
currencyTypes: data.currencyTypes || [],
|
||||
transportTypes: data.transportTypes || [],
|
||||
seals: data.seals || [],
|
||||
incoterms: data.incoterms || [],
|
||||
pedimentos: data.pedimentos || [],
|
||||
filters: {
|
||||
operation_type: parsedOperationType,
|
||||
invoice_type: invoiceTypeParam || null
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Error loading invoice:', err);
|
||||
throw error(500, 'Error al cargar la factura');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -31,11 +31,11 @@ YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Variables de configuración
|
||||
KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8080}"
|
||||
KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8080/kcauth}"
|
||||
KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN:-admin}"
|
||||
KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD:-admin}"
|
||||
KEYCLOAK_REALM="${KEYCLOAK_REALM:-master}"
|
||||
KEYCLOACK_ADMIN_URL="${KEYCLOACK_ADMIN_URL:-http://localhost:9000}"
|
||||
KEYCLOACK_ADMIN_URL="${KEYCLOACK_ADMIN_URL:-http://localhost:9000/kcauth}"
|
||||
|
||||
POSTGRES_HOST="${POSTGRES_HOST:-localhost}"
|
||||
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
|
||||
|
||||
Reference in New Issue
Block a user