diff --git a/README.md b/README.md index 27bd2b7..f8c646d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,20 @@ Cada app es un proyecto Vite independiente: `npm install && npm run dev` (o `npm Variables de entorno: copiar `.env.example` a `.env` y completar (el `.env` nunca se commitea). -## Deploy (servidor Ubuntu + Docker) +## Servidor actual (LXC sin Docker) + +Esta máquina es un contenedor LXC sin nesting: Docker no puede correr contenedores. +El stack corre directo en el host: + +- **NGINX** (apt) sirve los `dist/` de las apps y proxifica `/api/` → `127.0.0.1:3000`. + Config: `deploy/nginx-host.conf` → `/etc/nginx/sites-available/frasa`. +- **API** como servicio systemd: `deploy/frasa-api.service` → `frasa-api.service` + (`systemctl status frasa-api`, logs en `/var/log/frasa-api.log`). +- Rebuild de apps tras cambios: `VITE_API_URL=/api npm run build` en cada app + `systemctl reload nginx` (no requiere restart). + +El `docker-compose.yml` queda listo para cuando el stack se mude a una VM con Docker. + +## Deploy con Docker (VM futura) ```bash git clone https://git.consultoria-as.com/consultoria-as/FRASA-CAS.git /opt/frasa-ecosystem @@ -34,13 +47,12 @@ docker compose logs -f api curl https://frasadesarrollos.ddns.net/api/health ``` -Nota primer arranque: mientras no existan los certs, comenta el bloque `server 443` de -`nginx/nginx.conf` (o usa un cert autofirmado temporal); después de expedir, restaura y +Modo local sin SSL (LAN): `.env` con `NGINX_CONF=nginx.http.conf` hace que NGINX +sirva todo por HTTP en el puerto 80. Cuando el DDNS apunte a este servidor: +`certbot certonly --webroot -w /var/www/certbot -d frasadesarrollos.ddns.net`, +quita `NGINX_CONF` del `.env` (vuelve `nginx.conf` con 443) y `docker compose restart nginx`. Renovación: `certbot renew --deploy-hook "docker compose -f /opt/frasa-ecosystem/docker-compose.yml restart nginx"`. -Antes del deploy, cambiar en `apps/landing/src/data/modulos.ts` las URLs absolutas de -preview por rutas relativas (`/quote/`, `/client/`, `/pay/`) y rehacer el build. - Backup: `scripts/backup.sh` (cron sugerido `17 2 * * *`). ## Monday.com diff --git a/apps/alta-clientes/Dockerfile b/apps/alta-clientes/Dockerfile index 43d5c34..c730ae5 100644 --- a/apps/alta-clientes/Dockerfile +++ b/apps/alta-clientes/Dockerfile @@ -4,6 +4,8 @@ WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . +ARG VITE_API_URL=/api +ENV VITE_API_URL=$VITE_API_URL RUN npm run build # Etapa 2: servidor estático diff --git a/apps/cobranza/Dockerfile b/apps/cobranza/Dockerfile index 43d5c34..c730ae5 100644 --- a/apps/cobranza/Dockerfile +++ b/apps/cobranza/Dockerfile @@ -4,6 +4,8 @@ WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . +ARG VITE_API_URL=/api +ENV VITE_API_URL=$VITE_API_URL RUN npm run build # Etapa 2: servidor estático diff --git a/apps/cotizador/Dockerfile b/apps/cotizador/Dockerfile index 43d5c34..cd6dc6b 100644 --- a/apps/cotizador/Dockerfile +++ b/apps/cotizador/Dockerfile @@ -4,6 +4,9 @@ WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . +# URL del API horneada en el build (detras de NGINX es same-origin) +ARG VITE_API_URL=/api +ENV VITE_API_URL=$VITE_API_URL RUN npm run build # Etapa 2: servidor estático diff --git a/apps/landing/src/data/modulos.test.ts b/apps/landing/src/data/modulos.test.ts index f4130b1..e44c21d 100644 --- a/apps/landing/src/data/modulos.test.ts +++ b/apps/landing/src/data/modulos.test.ts @@ -7,20 +7,19 @@ describe('MODULOS', () => { expect(MODULOS.map((m) => m.id)).toEqual(['cotizador', 'alta-clientes', 'cobranza']) }) - it('cada módulo tiene nombre, descripción y URL de preview válida', () => { + it('cada módulo tiene nombre, descripción y URL válida', () => { for (const modulo of MODULOS) { expect(modulo.nombre.length).toBeGreaterThan(0) expect(modulo.descripcion.length).toBeGreaterThan(0) - expect(() => new URL(modulo.url)).not.toThrow() - expect(new URL(modulo.url).protocol).toBe('http:') + expect(modulo.url).toMatch(/^\/[a-z]+\/$/) } }) - it('apunta a los puertos y prefijos de preview correctos', () => { + it('apunta a los prefijos correctos (same-origin detrás de NGINX)', () => { const urls = Object.fromEntries(MODULOS.map((m) => [m.id, m.url])) - expect(urls['cotizador']).toBe('http://192.168.10.123:3001/quote/') - expect(urls['alta-clientes']).toBe('http://192.168.10.123:3002/client/') - expect(urls['cobranza']).toBe('http://192.168.10.123:3003/pay/') + expect(urls['cotizador']).toBe('/quote/') + expect(urls['alta-clientes']).toBe('/client/') + expect(urls['cobranza']).toBe('/pay/') }) it('no hay ids ni URLs duplicadas', () => { diff --git a/apps/landing/src/data/modulos.ts b/apps/landing/src/data/modulos.ts index 661fb46..70336d9 100644 --- a/apps/landing/src/data/modulos.ts +++ b/apps/landing/src/data/modulos.ts @@ -5,25 +5,24 @@ export interface Modulo { url: string } -// TODO: tras montar el NGINX de producción cambiar las URLs absolutas de -// preview por rutas relativas '/quote/', '/client/' y '/pay/'. +// Rutas relativas: la landing se sirve desde el mismo NGINX que las apps. export const MODULOS: Modulo[] = [ { id: 'cotizador', nombre: 'Cotizador', descripcion: 'Propuestas comerciales y PDF en tiempo real', - url: 'http://192.168.10.123:3001/quote/', + url: '/quote/', }, { id: 'alta-clientes', nombre: 'Alta de clientes', descripcion: 'Expediente digital, firmas y formatos PLD', - url: 'http://192.168.10.123:3002/client/', + url: '/client/', }, { id: 'cobranza', nombre: 'Cobranza', descripcion: 'Planes de enganche, vencidos y estados de cuenta', - url: 'http://192.168.10.123:3003/pay/', + url: '/pay/', }, ] diff --git a/deploy/frasa-api.service b/deploy/frasa-api.service new file mode 100644 index 0000000..6695efb --- /dev/null +++ b/deploy/frasa-api.service @@ -0,0 +1,17 @@ +[Unit] +Description=FRASA API Gateway +After=network.target + +[Service] +Type=simple +WorkingDirectory=/root/frasa-ecosystem/api-gateway +EnvironmentFile=/root/frasa-ecosystem/.env +Environment=PATH=/root/tools/node22/bin:/usr/local/bin:/usr/bin:/bin +ExecStart=/root/tools/node22/bin/node dist/index.js +Restart=always +RestartSec=3 +StandardOutput=append:/var/log/frasa-api.log +StandardError=append:/var/log/frasa-api.log + +[Install] +WantedBy=multi-user.target diff --git a/deploy/nginx-host.conf b/deploy/nginx-host.conf new file mode 100644 index 0000000..0503099 --- /dev/null +++ b/deploy/nginx-host.conf @@ -0,0 +1,39 @@ +# FRASA Ecosystem — nginx del host (modo HTTP, LAN) +# Instalado en /etc/nginx/sites-available/frasa +server { + listen 80 default_server; + server_name _; + + client_max_body_size 10m; + + # API Gateway (servicio systemd frasa-api en 127.0.0.1:3000) + location /api/ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Apps estáticas (builds de producción) + location /quote/ { + alias /root/frasa-ecosystem/apps/cotizador/dist/; + try_files $uri $uri/ /quote/index.html; + } + + location /client/ { + alias /root/frasa-ecosystem/apps/alta-clientes/dist/; + try_files $uri $uri/ /client/index.html; + } + + location /pay/ { + alias /root/frasa-ecosystem/apps/cobranza/dist/; + try_files $uri $uri/ /pay/index.html; + } + + # Landing (raíz) + location / { + root /root/frasa-ecosystem/apps/landing/dist; + try_files $uri $uri/ /index.html; + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 3aff82b..2089cbd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,8 @@ services: - "80:80" - "443:443" volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + # NGINX_CONF=nginx.http.conf para modo HTTP local (sin SSL todavia) + - ./nginx/${NGINX_CONF:-nginx.conf}:/etc/nginx/nginx.conf:ro - /etc/letsencrypt:/etc/letsencrypt:ro # certs siempre al día tras certbot renew - certbot_webroot:/var/www/certbot # challenge HTTP-01 depends_on: diff --git a/nginx/nginx.http.conf b/nginx/nginx.http.conf new file mode 100644 index 0000000..3b4ed3e --- /dev/null +++ b/nginx/nginx.http.conf @@ -0,0 +1,39 @@ +# FRASA Ecosystem — reverse proxy (modo HTTP, red local) +# Se usa cuando aun no hay certificado SSL (DDNS sin apuntar a este servidor). +# Cuando el dominio resuelva aqui: certbot --webroot y volver a nginx.conf (443). + +server { + listen 80; + server_name _; + + # Uploads de hasta 10MB (expedientes de clientes) + client_max_body_size 10m; + + # Challenge de Let's Encrypt (para cuando se expida el cert) + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # API Gateway + location /api/ { + proxy_pass http://api:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Apps web (cada contenedor sirve su SPA con su base path) + location /quote/ { proxy_pass http://cotizador:80; proxy_set_header Host $host; } + location /client/ { proxy_pass http://alta-clientes:80; proxy_set_header Host $host; } + location /pay/ { proxy_pass http://cobranza:80; proxy_set_header Host $host; } + + # Landing (raíz) + location / { + proxy_pass http://landing:80; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +}