From c9fe5847dab713ba8d8caaef0e776d89133676aa Mon Sep 17 00:00:00 2001 From: david Date: Mon, 15 Dec 2025 19:21:09 +0100 Subject: [PATCH] . --- Dockerfile | 2 +- apps/server/.env.acana | 1 - apps/server/.env.development | 3 +- apps/server/.env.example | 3 +- apps/server/.env.production | 1 - apps/server/.env.rodax | 45 ++++++++++ apps/server/package.json | 2 +- apps/server/src/app.ts | 2 +- apps/server/src/config/index.ts | 6 +- apps/server/src/index.ts | 13 ++- apps/web/package.json | 2 +- modules/auth/package.json | 2 +- .../api/lib/express/mock-user.middleware.ts | 4 +- modules/core/package.json | 2 +- .../express/api-error-mapper.ts | 13 +-- .../infrastructure/express/express-guards.ts | 14 +++- modules/customer-invoices/package.json | 2 +- .../templates/rodax/logo2.jpg | Bin 0 -> 22220 bytes modules/customers/package.json | 2 +- modules/doc-numbering/package.json | 2 +- packages/rdx-criteria/package.json | 2 +- packages/rdx-ddd/package.json | 2 +- packages/rdx-logger/package.json | 2 +- packages/rdx-utils/package.json | 2 +- scripts/build-factuges.sh | 34 ++++++-- scripts/docker-compose.caddymanager.yml | 78 ------------------ scripts/docker-compose.old | 4 +- scripts/docker-compose.yml | 6 +- scripts/stack.env | 2 +- scripts/stacks/rodax/env.rodax | 52 ++++++++++++ 30 files changed, 170 insertions(+), 135 deletions(-) create mode 100644 apps/server/.env.rodax create mode 100644 modules/customer-invoices/templates/rodax/logo2.jpg delete mode 100644 scripts/docker-compose.caddymanager.yml diff --git a/Dockerfile b/Dockerfile index e8c31264..839ce364 100644 --- a/Dockerfile +++ b/Dockerfile @@ -111,4 +111,4 @@ COPY --from=builder /repo/apps/server/package.json ./apps/server/package.json EXPOSE 3002 #CMD ["pnpm","exec", "node", "--env-file=apps/server/dist/.env", "apps/server/dist/index.js"] -CMD ["pnpm","exec", "node", "apps/server/dist/index.js"] +CMD ["pnpm", "exec", "node", "apps/server/dist/index.js"] diff --git a/apps/server/.env.acana b/apps/server/.env.acana index 61efaa69..9285d0b6 100644 --- a/apps/server/.env.acana +++ b/apps/server/.env.acana @@ -8,7 +8,6 @@ API_PORT=3002 NODE_ENV=production -HOST=0.0.0.0 PORT=3002 FRONTEND_URL=https://aana.factuges.app diff --git a/apps/server/.env.development b/apps/server/.env.development index 9ac99ca9..eb2cb0e9 100644 --- a/apps/server/.env.development +++ b/apps/server/.env.development @@ -1,6 +1,5 @@ NODE_ENV=development -HOST=0.0.0.0 -SERVER_PORT=3002 +API_PORT=3002 FRONTEND_URL=http://localhost:5173 diff --git a/apps/server/.env.example b/apps/server/.env.example index 1c4076da..05c2ed90 100644 --- a/apps/server/.env.example +++ b/apps/server/.env.example @@ -2,8 +2,7 @@ # Core del servidor HTTP # ─────────────────────────────── NODE_ENV=development -HOST=0.0.0.0 -SERVER_PORT=3002 +API_PORT=3002 # URL pública del frontend (CORS). # En dev se puede permitir todo con '*' diff --git a/apps/server/.env.production b/apps/server/.env.production index a2d05074..16dc18d7 100644 --- a/apps/server/.env.production +++ b/apps/server/.env.production @@ -13,7 +13,6 @@ DB_ROOT_PASS=verysecret TRAEFIK_ENTRYPOINT=web NODE_ENV=production -HOST=0.0.0.0 PORT=3002 FRONTEND_URL=http://factuges.rodax-software.local diff --git a/apps/server/.env.rodax b/apps/server/.env.rodax new file mode 100644 index 00000000..239f987a --- /dev/null +++ b/apps/server/.env.rodax @@ -0,0 +1,45 @@ +# ─────────────────────────────── +# Identidad de la compañía +# ─────────────────────────────── +COMPANY=rodax +CTE_COMPANY_ID=5e4dc5b3-96b9-4968-9490-14bd032fec5f + +# Dominios +DOMAIN=factuges.rodax-software.local + +# ─────────────────────────────── +# Base de datos (Sequelize / MySQL-MariaDB) +# ─────────────────────────────── +DB_ROOT_PASS=verysecret +DB_USER=rodax_usr +DB_PASS=supersecret +DB_NAME=rodax_db +DB_PORT=3306 + +# Log de Sequelize (true|false) +DB_LOGGING=false + +# Alterar estructura BD +DB_SYNC_MODE=none # none | alter | force + + +# ─────────────────────────────── +# API +# ─────────────────────────────── +API_PORT=3002 +API_IMAGE=factuges-server:rodax-latest + +# Plantillas +TEMPLATES_PATH=/repo/apps/server/templates + +# Chrome executable path (Puppeteer) +PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome + +# URL pública del frontend (CORS) +FRONTEND_URL=factuges.rodax-software.local + +# Tiempo máximo para cada warmup() de un módulo, en milisegundos. +WARMUP_TIMEOUT_MS=10000 + +# Si es true, un fallo de warmup aborta el arranque. Si es false, continúa con warning. +WARMUP_STRICT=false diff --git a/apps/server/package.json b/apps/server/package.json index c4c21f61..ff64e428 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "@erp/factuges-server", - "version": "0.1.0", + "version": "0.1.1", "private": true, "scripts": { "build": "tsup src/index.ts --config tsup.config.ts", diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 03c1c74c..f225f3f8 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -47,7 +47,7 @@ export function createApp(): Application { app.use(cors(ENV.NODE_ENV === "development" ? devCors : prodCors)); app.options("*", cors(ENV.NODE_ENV === "development" ? devCors : prodCors)); - app.set("port", process.env.SERVER_PORT ?? 3002); + app.set("port", process.env.API_PORT ?? 3002); // Oculta la cabecera x-powered-by app.disable("x-powered-by"); diff --git a/apps/server/src/config/index.ts b/apps/server/src/config/index.ts index 5ff878ab..9339160b 100644 --- a/apps/server/src/config/index.ts +++ b/apps/server/src/config/index.ts @@ -13,8 +13,7 @@ const NODE_ENV = (process.env.NODE_ENV as NodeEnv) ?? "development"; const isProd = NODE_ENV === "production"; const isDev = NODE_ENV === "development"; -const HOST = process.env.HOST ?? "0.0.0.0"; -const SERVER_PORT = asNumber(process.env.SERVER_PORT, 3002); +const API_PORT = asNumber(process.env.API_PORT, 3002); // En producción exigimos FRONTEND_URL definido (según requisitos actuales). const FRONTEND_URL = isProd @@ -48,8 +47,7 @@ const TRUST_PROXY = asNumber(process.env.TRUST_PROXY, 0); export const ENV = { NODE_ENV, - HOST, - SERVER_PORT, + API_PORT, FRONTEND_URL, DATABASE_URL, DB_DIALECT, diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index a6b1cf7c..ff88fb9b 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -20,8 +20,8 @@ z.config(z.locales.es()); export const currentState = { launchedAt: DateTime.now(), appPath: process.cwd(), - host: ENV.HOST, - port: ENV.SERVER_PORT, + hosts: "0.0.0.0", + port: ENV.API_PORT, environment: ENV.NODE_ENV, connections: {} as Record, }; @@ -91,12 +91,11 @@ const serverError = (error: NodeJS.ErrnoException) => { logger.error("⛔️ Server wasn't able to start properly.", { label: "serverError0", error, - host: ENV.HOST, - port: ENV.SERVER_PORT, + port: ENV.API_PORT, }); if (error.code === "EADDRINUSE") { - logger.error(`Port ${ENV.SERVER_PORT} already in use`, { error, label: "serverError1" }); + logger.error(`Port ${ENV.API_PORT} already in use`, { error, label: "serverError1" }); } else { logger.error(error.message, { error, label: "serverError2" }); } @@ -211,8 +210,7 @@ process.on("uncaughtException", async (error: Error) => { // Mostrar variables de entorno logger.info(`Environment: ${currentState.environment}`); - logger.info(`HOST: ${ENV.HOST}`); - logger.info(`SERVER_PORT: ${ENV.SERVER_PORT}`); + logger.info(`API_PORT: ${ENV.API_PORT}`); logger.info(`API_BASE_PATH: ${API_BASE_PATH}`); logger.info(`FRONTEND_URL: ${ENV.FRONTEND_URL}`); @@ -268,7 +266,6 @@ process.on("uncaughtException", async (error: Error) => { // URLs de acceso útiles logger.info(`⚡️ Server accessible at: http://localhost:${currentState.port}`); - logger.info(`⚡️ Server accessible at: http://${currentState.host}:${currentState.port}`); for (const address of addresses) { logger.info(`⚡️ Server accessible at: http://${address}:${currentState.port}`); } diff --git a/apps/web/package.json b/apps/web/package.json index 7f0e8583..e0c428b8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@erp/factuges-web", "private": true, - "version": "0.1.0", + "version": "0.1.1", "type": "module", "scripts": { "dev": "vite --host --clearScreen false", diff --git a/modules/auth/package.json b/modules/auth/package.json index 69d3fa9f..3bbc19d2 100644 --- a/modules/auth/package.json +++ b/modules/auth/package.json @@ -1,6 +1,6 @@ { "name": "@erp/auth", - "version": "0.1.0", + "version": "0.1.1", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/auth/src/api/lib/express/mock-user.middleware.ts b/modules/auth/src/api/lib/express/mock-user.middleware.ts index d9b98a15..f44164de 100644 --- a/modules/auth/src/api/lib/express/mock-user.middleware.ts +++ b/modules/auth/src/api/lib/express/mock-user.middleware.ts @@ -7,8 +7,8 @@ export function mockUser(req: RequestWithAuth, _res: Response, next: NextFunctio req.user = { userId: UniqueID.create("9e4dc5b3-96b9-4968-9490-14bd032fec5f").data, email: EmailAddress.create("dev@example.com").data, - companyId: UniqueID.create("019a9667-6a65-767a-a737-48234ee50a3a").data, - companySlug: "alonsoysal", + companyId: UniqueID.create("5e4dc5b3-96b9-4968-9490-14bd032fec5f").data, + companySlug: "rodax", roles: ["admin"], }; diff --git a/modules/core/package.json b/modules/core/package.json index dcbf424a..2b4c6e70 100644 --- a/modules/core/package.json +++ b/modules/core/package.json @@ -1,6 +1,6 @@ { "name": "@erp/core", - "version": "0.1.0", + "version": "0.1.1", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/core/src/api/infrastructure/express/api-error-mapper.ts b/modules/core/src/api/infrastructure/express/api-error-mapper.ts index 5ca97b4c..37f0b870 100644 --- a/modules/core/src/api/infrastructure/express/api-error-mapper.ts +++ b/modules/core/src/api/infrastructure/express/api-error-mapper.ts @@ -12,24 +12,25 @@ // Nota: Todos los nombres de tipos/clases/archivos en inglés; comentarios en castellano. import { - DomainValidationError, + type DomainValidationError, + type ValidationErrorCollection, isDomainValidationError, isValidationErrorCollection, - ValidationErrorCollection, } from "@repo/rdx-ddd"; import { - DuplicateEntityError, - EntityNotFoundError, + type DuplicateEntityError, + type EntityNotFoundError, isDuplicateEntityError, isEntityNotFoundError, } from "../../domain"; import { - InfrastructureRepositoryError, - InfrastructureUnavailableError, + type InfrastructureRepositoryError, + type InfrastructureUnavailableError, isInfrastructureRepositoryError, isInfrastructureUnavailableError, } from "../errors"; + import { ApiError, ConflictApiError, diff --git a/modules/core/src/api/infrastructure/express/express-guards.ts b/modules/core/src/api/infrastructure/express/express-guards.ts index 14989303..e3e483d1 100644 --- a/modules/core/src/api/infrastructure/express/express-guards.ts +++ b/modules/core/src/api/infrastructure/express/express-guards.ts @@ -1,7 +1,13 @@ -import { Criteria } from "@repo/rdx-criteria/server"; -import { NextFunction, Request, Response } from "express"; -import { ApiError, ForbiddenApiError, UnauthorizedApiError, ValidationApiError } from "./errors"; -import { ExpressController } from "./express-controller"; +import type { Criteria } from "@repo/rdx-criteria/server"; +import type { NextFunction, Request, Response } from "express"; + +import { + type ApiError, + ForbiddenApiError, + UnauthorizedApiError, + ValidationApiError, +} from "./errors"; +import type { ExpressController } from "./express-controller"; export type GuardResultLike = { isFailure: boolean; error?: ApiError }; diff --git a/modules/customer-invoices/package.json b/modules/customer-invoices/package.json index 2e3aa220..e29c1802 100644 --- a/modules/customer-invoices/package.json +++ b/modules/customer-invoices/package.json @@ -1,6 +1,6 @@ { "name": "@erp/customer-invoices", - "version": "0.1.0", + "version": "0.1.1", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/customer-invoices/templates/rodax/logo2.jpg b/modules/customer-invoices/templates/rodax/logo2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7ab3d183158c8fcb3fa008c52ac03f8d330f9092 GIT binary patch literal 22220 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 literal 0 HcmV?d00001 diff --git a/modules/customers/package.json b/modules/customers/package.json index fffbb9cf..12113e04 100644 --- a/modules/customers/package.json +++ b/modules/customers/package.json @@ -1,6 +1,6 @@ { "name": "@erp/customers", - "version": "0.1.0", + "version": "0.1.1", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/doc-numbering/package.json b/modules/doc-numbering/package.json index 8a4397fa..28e1df54 100644 --- a/modules/doc-numbering/package.json +++ b/modules/doc-numbering/package.json @@ -1,6 +1,6 @@ { "name": "@erp/doc-numbering", - "version": "0.1.0", + "version": "0.1.1", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-criteria/package.json b/packages/rdx-criteria/package.json index d7830255..480c41ab 100644 --- a/packages/rdx-criteria/package.json +++ b/packages/rdx-criteria/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-criteria", - "version": "0.1.0", + "version": "0.1.1", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-ddd/package.json b/packages/rdx-ddd/package.json index ec52cc64..ca3e2d34 100644 --- a/packages/rdx-ddd/package.json +++ b/packages/rdx-ddd/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-ddd", - "version": "0.1.0", + "version": "0.1.1", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-logger/package.json b/packages/rdx-logger/package.json index cd5a1efd..01246d7d 100644 --- a/packages/rdx-logger/package.json +++ b/packages/rdx-logger/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-logger", - "version": "0.1.0", + "version": "0.1.1", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-utils/package.json b/packages/rdx-utils/package.json index c79e8931..4cae5361 100644 --- a/packages/rdx-utils/package.json +++ b/packages/rdx-utils/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-utils", - "version": "0.1.0", + "version": "0.1.1", "private": true, "type": "module", "sideEffects": false, diff --git a/scripts/build-factuges.sh b/scripts/build-factuges.sh index ec4ce5be..0fefbb17 100755 --- a/scripts/build-factuges.sh +++ b/scripts/build-factuges.sh @@ -42,6 +42,17 @@ fi COMPANY="$1" +SSH_USER="rodax" +SSH_HOST="vps-2.rodax-software.com" +SSH_PORT="49152" + +# Override por compañía específica +if [[ "$COMPANY" == "rodax" ]]; then + SSH_USER="rodax" + SSH_HOST="factuges.rodax-software.local" + SSH_PORT="22" +fi + # --- Parseo de flags --- shift # quitamos el , ahora solo quedan flags @@ -221,19 +232,26 @@ EOF echo "📦 API manifest generado en ${OUT_API_DIR}/manifest-v${API_VERSION}-${DATE}.json" fi -if [[ "$LOAD" == true ]]; then - echo "📥 Cargando imagen en producción vps-2.rodax-software.com..." - [[ "$MODE" == "web" || "$MODE" == "all" ]] && scp -r -P 49152 ${OUT_WEB_DIR} rodax@vps-2.rodax-software.com:/opt/factuges/${COMPANY}/ - [[ "$MODE" == "api" || "$MODE" == "all" ]] && scp -r -P 49152 ${OUT_API_DIR} rodax@vps-2.rodax-software.com:/opt/factuges/${COMPANY}/ - [[ "$MODE" == "api" || "$MODE" == "all" ]] && scp -r -P 49152 ${TEMPLATES_DIR} rodax@vps-2.rodax-software.com:/opt/factuges/${COMPANY}/ - [[ "$MODE" == "api" || "$MODE" == "all" ]] && RESULT=$(ssh -p 49152 rodax@vps-2.rodax-software.com "docker load -i /opt/factuges/${COMPANY}/api/$(basename ${TAR_FILE_LATEST})") - [[ "$MODE" == "api" || "$MODE" == "all" ]] && echo $RESULT - #docker load -i "${TAR_FILE_V}" +if [[ "$LOAD" == true ]]; then + echo "📥 Cargando imagen en producción ${SSH_HOST}..." + + [[ "$MODE" == "web" || "$MODE" == "all" ]] && scp -r -P "${SSH_PORT}" "${OUT_WEB_DIR}" "${SSH_USER}@${SSH_HOST}:/opt/factuges/${COMPANY}/" + [[ "$MODE" == "api" || "$MODE" == "all" ]] && scp -r -P "${SSH_PORT}" "${OUT_API_DIR}" "${SSH_USER}@${SSH_HOST}:/opt/factuges/${COMPANY}/" + [[ "$MODE" == "api" || "$MODE" == "all" ]] && scp -r -P "${SSH_PORT}" "${TEMPLATES_DIR}" "${SSH_USER}@${SSH_HOST}:/opt/factuges/${COMPANY}/" + + if [[ "$MODE" == "api" || "$MODE" == "all" ]]; then + RESULT=$(ssh -p "${SSH_PORT}" "${SSH_USER}@${SSH_HOST}" \ + "docker load -i /opt/factuges/${COMPANY}/api/$(basename "${TAR_FILE_LATEST}")") + echo "${RESULT}" + fi + echo "✅ Imagen cargada en producción" fi + + # ===================================================== # 3️⃣ Resumen # ===================================================== diff --git a/scripts/docker-compose.caddymanager.yml b/scripts/docker-compose.caddymanager.yml deleted file mode 100644 index 0fc80b1e..00000000 --- a/scripts/docker-compose.caddymanager.yml +++ /dev/null @@ -1,78 +0,0 @@ -services: - # MongoDB database for persistent storage (optional - SQLite is used by default) - mongodb: - image: mongo:8.0 - container_name: caddymanager-mongodb - restart: unless-stopped - environment: - - MONGO_INITDB_ROOT_USERNAME=mongoadmin - - MONGO_INITDB_ROOT_PASSWORD=someSecretPassword # Change for production! - ports: - - "27017:27017" # Expose for local dev, remove for production - volumes: - - mongodb_data:/data/db - networks: - - caddymanager - profiles: - - mongodb # Use 'docker-compose --profile mongodb up' to include MongoDB - - # Backend API server - backend: - image: caddymanager/caddymanager-backend:latest - container_name: caddymanager-backend - restart: unless-stopped - environment: - - PORT=3000 - # Database Engine Configuration (defaults to SQLite) - - DB_ENGINE=sqlite # Options: 'sqlite' or 'mongodb' - # SQLite Configuration (used when DB_ENGINE=sqlite) - - SQLITE_DB_PATH=/app/data/caddymanager.sqlite - # MongoDB Configuration (used when DB_ENGINE=mongodb) - - MONGODB_URI=mongodb://mongoadmin:someSecretPassword@mongodb:27017/caddymanager?authSource=admin - - CORS_ORIGIN=http://localhost:80 - - LOG_LEVEL=debug - - CADDY_SANDBOX_URL=http://localhost:2019 - - PING_INTERVAL=30000 - - PING_TIMEOUT=2000 - - AUDIT_LOG_MAX_SIZE_MB=100 - - AUDIT_LOG_RETENTION_DAYS=90 - - METRICS_HISTORY_MAX=1000 # Optional: max number of in-memory metric history snapshots to keep - - JWT_SECRET=your_jwt_secret_key_here # Change for production! - - JWT_EXPIRATION=24h - # Backend is now only accessible through frontend proxy - volumes: - - sqlite_data:/app/data # SQLite database storage - networks: - - caddymanager - - # Frontend web UI - frontend: - image: caddymanager/caddymanager-frontend:latest - container_name: caddymanager-frontend - restart: unless-stopped - depends_on: - - backend - environment: - - BACKEND_HOST=backend:3000 - - APP_NAME=Caddy Manager - - DARK_MODE=true - ports: - - "80:80" # Expose web UI - networks: - - caddymanager - -networks: - caddymanager: - driver: bridge - -volumes: - mongodb_data: # Only used when MongoDB profile is active - sqlite_data: # SQLite database storage - -# Notes: -# - SQLite is the default database engine - no additional setup required! -# - To use MongoDB instead, set DB_ENGINE=mongodb and start with: docker-compose --profile mongodb up -# - For production, use strong passwords and consider secrets management. -# - The backend uses SQLite by default, storing data in a persistent volume. -# - The frontend proxies all /api/* requests to the backend service. -# - Backend is not directly exposed - all API access goes through the frontend proxy. diff --git a/scripts/docker-compose.old b/scripts/docker-compose.old index e65959ce..743fbcf9 100644 --- a/scripts/docker-compose.old +++ b/scripts/docker-compose.old @@ -67,7 +67,7 @@ services: environment: NODE_ENV: production COMPANY: ${COMPANY} - PORT: ${SERVER_PORT:-3002} + PORT: ${API_PORT:-3002} DB_DIALECT: "mysql" DB_HOST: "db" DB_PORT: ${DB_PORT} @@ -79,7 +79,7 @@ services: - internal - edge ports: - - ${SERVER_PORT:-3002}:${SERVER_PORT:-3002} + - ${API_PORT:-3002}:${API_PORT:-3002} labels: traefik.enable: "true" traefik.http.routers.factuges_rodax_api.rule: Host(`${DOMAIN}`) && PathPrefix(`/api`) diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml index baa011fc..f135e2b2 100644 --- a/scripts/docker-compose.yml +++ b/scripts/docker-compose.yml @@ -73,7 +73,7 @@ services: environment: NODE_ENV: production COMPANY: ${COMPANY} - PORT: ${SERVER_PORT:-3002} + PORT: ${API_PORT:-3002} DB_DIALECT: "mysql" DB_HOST: "db" DB_PORT: ${DB_PORT} @@ -85,7 +85,7 @@ services: - internal - edge ports: - - ${SERVER_PORT:-3002}:${SERVER_PORT:-3002} + - ${API_PORT:-3002}:${API_PORT:-3002} labels: traefik.enable: "true" # Router @@ -93,7 +93,7 @@ services: traefik.http.routers.${COMPANY}-api.entrypoints: websecure traefik.http.routers.${COMPANY}-api.tls.certresolver: cfresolver # Servicio - traefik.http.services.${COMPANY}-api.loadbalancer.server.port: "${SERVER_PORT:-3002}" + traefik.http.services.${COMPANY}-api.loadbalancer.server.port: "${API_PORT:-3002}" # --- Web estática (React compilado por build-factuges.sh) --- web: diff --git a/scripts/stack.env b/scripts/stack.env index 33954a8b..3b809dfe 100644 --- a/scripts/stack.env +++ b/scripts/stack.env @@ -3,7 +3,7 @@ DOMAIN=rodax.factuges.rodax-software.local FRONTEND_URL=rodax.factuges.rodax-software.local WEB_VERSION=v0.0.4-latest API_IMAGE=factuges-server:rodax-latest -SERVER_PORT=3002 +API_PORT=3002 DB_HOST=db DB_DIALECT=mysql DB_PORT=3306 diff --git a/scripts/stacks/rodax/env.rodax b/scripts/stacks/rodax/env.rodax index e69de29b..d8fdcf06 100644 --- a/scripts/stacks/rodax/env.rodax +++ b/scripts/stacks/rodax/env.rodax @@ -0,0 +1,52 @@ +# Identidad de la compañía +COMPANY=rodax + +# Dominios +DOMAIN=factuges.rodax-software.local + +# MariaDB +DB_ROOT_PASS=verysecret +DB_USER=rodax_usr +DB_PASS=supersecret +DB_NAME=rodax_db +DB_PORT=3306 + +# API +API_PORT=3002 +API_IMAGE=factuges-server:rodax-latest +TEMPLATES_PATH=/repo/apps/server/templates +FRONTEND_URL=factuges.rodax-software.local + + +# SYNC +ENV = development +LOCAL_TZ = Europe/Madrid +STATE_PATH = /app/state +SYNC_MODE = all + +FACTUGES_HOST = factuges-pc.rodax-software.local +FACTUGES_PORT = 3050 +FACTUGES_DATABASE = C:\FactuGES\FACTUGES.FDB +FACTUGES_USER = sysdba +FACTUGES_PASSWORD = masterkey + +FWEB_MYSQL_HOST = db # ${DB_NAME} +FWEB_MYSQL_PORT = 3306 # ${DB_PORT} +FWEB_MYSQL_DATABASE = factuges_acana # ${DB_NAME} +FWEB_MYSQL_USER = acana # ${DB_USER} +FWEB_MYSQL_PASSWORD = r@U8%GJ+2e/AWR # ${DB_PASS} + +CTE_COMPANY_ID = '5e4dc5b3-96b9-4968-9490-14bd032fec5f' +CTE_SERIE = 'F25' +CTE_STATUS_INVOICE = 'issued' +CTE_IS_PROFORMA = 0 +CTE_STATUS_VERIFACTU = 'Pendiente' +CTE_LANGUAGE_CODE = 'es' +CTE_COUNTRY_CODE = 'es' +CTE_IS_COMPANY = 1 +CTE_SYNC_RESULT_OK = 1 +CTE_SYNC_RESULT_FAIL = 2 + +VERIFACTU_API_KEY = vf_prod_yfjonNPv2E4Fij+5J0hct0zCgUeFYT2dZzb23UZlM+Q= +VERIFACTU_BASE_URL = https://api.verifacti.com/ +VERIFACTU_NIFS_API_KEY = vfn_osYpNdqSzAdTAHpazXG2anz4F3o0gfbSb5FFrCBZcno=