From 8315a8098fec2c6e071c1fd4bdf5bace16598940 Mon Sep 17 00:00:00 2001 From: 4Ventures <4ventures@MacBook-Air-de-4Ventures.local> Date: Mon, 21 Sep 2026 18:07:04 +0200 Subject: [PATCH] fix: stop baking Supabase anon key into the Docker image Co-authored-by: Cursor --- Dockerfile | 4 +- docs/RUNBOOK.md | 22 ++++++++-- scripts/smoke_agencia_simulate.py | 67 +++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 scripts/smoke_agencia_simulate.py diff --git a/Dockerfile b/Dockerfile index bd8a390..d5b428c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,8 +12,8 @@ COPY --from=deps /app/node_modules ./node_modules COPY . . RUN mkdir -p public ENV NEXT_TELEMETRY_DISABLED=1 -ENV NEXT_PUBLIC_SUPABASE_URL=https://supabase.4ventures.es -ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3NzIzMTgzMTAsImV4cCI6MTg5MzQ1NjAwMCwicm9sZSI6ImFub24iLCJpc3MiOiJzdXBhYmFzZSJ9.UWNn1leZNljna9Qwi9U5RtGqsh5_SnEoiPO8HkQIgi8 +# NEXT_PUBLIC_* must come from Dokploy "Create Environment File" (.env) or build-args. +# Do not bake secrets into the image. RUN npm run build FROM base AS runner diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md index 1db9e5e..255b064 100644 --- a/docs/RUNBOOK.md +++ b/docs/RUNBOOK.md @@ -1,20 +1,30 @@ # Runbook — Centro de Mando 4Ventures -Operador: `marketing@4ventures.es` +Operador: `marketing@4ventures.es` +Credenciales locales (gitignored): `.ops/marketing-credentials.txt` ## Health ```bash curl -sf https://app.4ventures.es/api/health +# expect: serviceRoleConfigured true ./scripts/smoke.sh https://app.4ventures.es +python3 scripts/smoke_operator_login.py +python3 scripts/smoke_chat_auth.py ``` ## Deploy (Dokploy) 1. Push a `main` en Gitea `Marketing/4ventures-client-portal`. -2. Dokploy rebuild del servicio `app`. -3. Env: `NEXT_PUBLIC_SUPABASE_*`, `SUPABASE_SERVICE_ROLE_KEY`, `ANTHROPIC_API_KEY`. -4. Smoke: `/login` → KPIs → simulador → chat. +2. Dokploy → **Web4Ventures / production / client-portal** → Deploy + (id interno `web4ventures-clientportal-jntjhs`). +3. Environment (runtime + build via “Create Environment File”): + - `NEXT_PUBLIC_SUPABASE_URL` + - `NEXT_PUBLIC_SUPABASE_ANON_KEY` + - `SUPABASE_SERVICE_ROLE_KEY` + - `ANTHROPIC_API_KEY` (opcional, LLM) +4. Traefik: `portal.yml` → `http://web4ventures-clientportal-jntjhs:3000` +5. Smoke: `/login` → KPIs → creatividades → chat. ## Alta cliente @@ -28,6 +38,10 @@ UI `/dashboard/agencia` (roles admin/team) o RPC `provision_client`. `POST https://n8n-agencia.4ventures.es/webhook/lead-inbound` +## Endurecer git / n8n + +Ver `.ops/HARDEN_SURFACES.md` (IP allowlist; no aplicado hasta tener IPs). + ## Incidentes Ver `SECURITY.md`. diff --git a/scripts/smoke_agencia_simulate.py b/scripts/smoke_agencia_simulate.py new file mode 100644 index 0000000..beee726 --- /dev/null +++ b/scripts/smoke_agencia_simulate.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Authenticated smoke: agencia + simulate.""" +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +from playwright.sync_api import sync_playwright + +ROOT = Path("/Users/4ventures/4ventures-client-portal") +APP = "https://app.4ventures.es" +CREDS = ROOT / ".ops" / "marketing-credentials.txt" +SHOT = ROOT / "scripts/dokploy-shots" + + +def main() -> int: + email, password = "marketing@4ventures.es", "" + for line in CREDS.read_text().splitlines(): + if line.startswith("email="): + email = line.split("=", 1)[1].strip() + if line.startswith("password="): + password = line.split("=", 1)[1].strip() + + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page(viewport={"width": 1400, "height": 1100}) + page.goto(f"{APP}/login", wait_until="networkidle", timeout=90000) + page.locator('input[type="email"], input[name="email"]').first.fill(email) + page.locator('input[type="password"]').first.fill(password) + page.locator('button[type="submit"]').first.click() + page.wait_for_timeout(3500) + + page.goto(f"{APP}/dashboard/agencia", wait_until="networkidle", timeout=60000) + page.wait_for_timeout(1500) + SHOT.mkdir(parents=True, exist_ok=True) + page.screenshot(path=str(SHOT / "smoke_agencia.png"), full_page=True) + body = page.inner_text("body") + missing_key = "Falta SUPABASE_SERVICE_ROLE_KEY" in body + print("agencia_missing_service_key", missing_key, flush=True) + print("agencia_snip", " | ".join([ln.strip() for ln in body.splitlines() if ln.strip()][10:22]), flush=True) + + sim = page.evaluate( + """async () => { + const r = await fetch('/api/simulate', { + method: 'POST', + headers: {'content-type':'application/json'}, + body: JSON.stringify({ amount: 1000, channel: 'meta' }) + }); + const text = await r.text(); + let json=null; try { json=JSON.parse(text); } catch {} + return { status: r.status, text: text.slice(0,250), json }; + }""" + ) + print("simulate", json.dumps(sim, ensure_ascii=False)[:400], flush=True) + browser.close() + + if missing_key: + print("FAIL_AGENCIA", flush=True) + return 2 + print("SUCCESS", flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())