diff --git a/.gitignore b/.gitignore index 2783194..5570520 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ dist/ !.env.example .ops/ +# local ops noise +scripts/dokploy-shots/ +tsconfig.tsbuildinfo + # debug / OS npm-debug.log* yarn-debug.log* diff --git a/scripts/dokploy_dump_traefik_harden.py b/scripts/dokploy_dump_traefik_harden.py new file mode 100644 index 0000000..15fd963 --- /dev/null +++ b/scripts/dokploy_dump_traefik_harden.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Dump Traefik gitea.yml + n8n-hardened.yml + middlewares for harden prep.""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + +from playwright.sync_api import sync_playwright + +BPS = Path("/Users/4ventures/Projects/bps-facturacion") +OUT = Path("/Users/4ventures/4ventures-client-portal/.ops/traefik-dumps") +SHOT = Path("/Users/4ventures/4ventures-client-portal/scripts/dokploy-shots") +BASE = "https://dokploy.4ventures.es" +FILES = ("middlewares.yml", "middleware-hardening.yml", "gitea.yml", "n8n-hardened.yml") + + +def env(key: str) -> str: + import os + + v = os.environ.get(key, "").strip() + if v: + return v + p = BPS / ".env" + if not p.is_file(): + return "" + for line in p.read_text(encoding="utf-8").splitlines(): + if line.startswith(f"{key}="): + return line.split("=", 1)[1].strip().strip('"').strip("'") + return "" + + +def main() -> int: + OUT.mkdir(parents=True, exist_ok=True) + email = env("DOKPLOY_EMAIL") or "marketing@4ventures.es" + password = env("DOKPLOY_PASSWORD") + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page(viewport={"width": 1600, "height": 1200}) + page.goto(BASE + "/", wait_until="domcontentloaded", timeout=90000) + page.wait_for_timeout(700) + if "/dashboard" not in page.url: + 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_url(re.compile(r"/dashboard"), timeout=90000) + print("login ok", flush=True) + + page.goto(f"{BASE}/dashboard/traefik", wait_until="networkidle", timeout=120000) + page.wait_for_timeout(1000) + page.get_by_text(re.compile(r"^dynamic$", re.I)).first.click() + page.wait_for_timeout(800) + + for name in FILES: + page.locator(f"text={name}").first.dblclick() + page.wait_for_timeout(1500) + # Unlock not needed for read; try extract from body line numbers area + body = page.inner_text("body") + # Heuristic: capture from 'http:' or 'http\n' yaml start + m = re.search(r"(?ms)^(http:|tcp:|udp:|tls:).*(?=Unlock|Skip YAML|Update\n|$)", body) + # Better: get cm content + text = page.evaluate( + """() => { + const rows = document.querySelector('.cm-content'); + return rows ? rows.innerText : ''; + }""" + ) + if not text or len(text) < 20: + # fallback strip UI chrome + lines = [] + capture = False + for ln in body.splitlines(): + if ln.strip() in ("http:", "tcp:", "tls:") or ln.startswith("http:"): + capture = True + if capture: + if ln.strip() in ("Unlock", "Update", "Skip YAML validation (for Go templating)"): + break + lines.append(ln) + text = "\n".join(lines) + (OUT / name).write_text(text + "\n", encoding="utf-8") + print("dumped", name, "chars", len(text), flush=True) + page.screenshot(path=str(SHOT / f"dump_{name.replace('.','_')}.png"), full_page=True) + + browser.close() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/smoke_chat_auth.py b/scripts/smoke_chat_auth.py new file mode 100644 index 0000000..1fe9253 --- /dev/null +++ b/scripts/smoke_chat_auth.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Authenticated chat smoke on app.4ventures.es.""" +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" + + +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": 1000}) + 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) + if "/login" in page.url: + print("LOGIN_FAILED", flush=True) + browser.close() + return 2 + + # unauth baseline via fresh context later; here authenticated + res = page.evaluate( + """async () => { + const r = await fetch('/api/chat', { + method: 'POST', + headers: {'content-type':'application/json'}, + body: JSON.stringify({ message: 'hola' }) + }); + const text = await r.text(); + let json = null; + try { json = JSON.parse(text); } catch {} + return { status: r.status, text: text.slice(0, 300), json }; + }""" + ) + print("auth_chat", json.dumps(res, ensure_ascii=False)[:500], flush=True) + browser.close() + + # unauth + import urllib.error + import urllib.request + + req = urllib.request.Request( + f"{APP}/api/chat", + data=b'{"message":"hola"}', + method="POST", + headers={"Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=15) as r: + print("unauth_chat", r.status, r.read()[:80], flush=True) + except urllib.error.HTTPError as e: + print("unauth_chat", e.code, e.read()[:80], flush=True) + + ok = res.get("status") == 200 and isinstance(res.get("json"), list) + print("SUCCESS" if ok else "PARTIAL", flush=True) + return 0 if ok else 5 + + +if __name__ == "__main__": + raise SystemExit(main())