Files
turf_saas/portal_server.py
DevOps Engineer 7f5573f076 feat(security): add IP-based rate limiting on /api/v1/auth/login — fix brute force HRT-62
- saas_auth.py: in-memory sliding-window rate limiter (5 attempts/5min, 15min block)
  using collections.defaultdict + threading.Lock, stdlib only, no new deps
- portal_server.py: register rate_limit_middleware + access_log_middleware
  (was missing, leaving global 100req/min limit unApplied on portal routes)
- tests/security/test_security.py: add TestLoginRateLimit class with
  test_login_brute_force_blocked_after_5_attempts and test_login_429_has_retry_after_header

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-27 14:50:08 +02:00

901 lines
30 KiB
Python
Executable File

#!/usr/bin/env python3
from flask import Flask, send_from_directory, jsonify, request, make_response
import os
import json
import requests
import subprocess
import db
from middleware import rate_limit_middleware, access_log_middleware
app = Flask(__name__)
rate_limit_middleware(app)
access_log_middleware(app)
DASHBOARD_API_URL = "http://localhost:8791"
COMBINED_API_URL = "http://localhost:8790"
SAAS_DIR = "/home/h3r7/turf_saas"
# ─── SaaS Auth & API v1 blueprints ────────────────────────────────────────────
try:
from saas_auth import auth_bp
from saas_api_v1 import api_v1_bp
app.register_blueprint(auth_bp)
app.register_blueprint(api_v1_bp)
print("[portal] SaaS auth & API v1 blueprints registered ✅")
except Exception as e:
print(f"[portal] Warning: could not register SaaS blueprints: {e}")
# ─── Landing & SaaS pages ─────────────────────────────────────────────────────
@app.route("/health")
def health():
"""Health check endpoint for Docker/load balancer. Returns 200 if app is running."""
return {"status": "ok", "service": "portal"}, 200
@app.route("/")
def landing():
"""Marketing landing page."""
return send_from_directory(SAAS_DIR, "landing.html")
@app.route("/login")
def login_page():
return send_from_directory(SAAS_DIR, "login.html")
@app.route("/register")
def register_page():
return send_from_directory(SAAS_DIR, "register.html")
@app.route("/dashboard")
def dashboard_saas():
return send_from_directory(SAAS_DIR, "dashboard_saas.html")
@app.route("/onboarding")
def onboarding():
return send_from_directory(SAAS_DIR, "onboarding.html")
@app.route("/account")
def account():
return send_from_directory(SAAS_DIR, "account.html")
@app.route("/portal")
@app.route("/portail")
def portal_legacy():
"""Legacy portal redirect."""
return send_from_directory(SAAS_DIR, "portail.html")
@app.route("/favicon.ico")
def favicon():
return send_from_directory(SAAS_DIR, "favicon.ico")
@app.route("/prompts", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
@app.route("/prompts/", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
@app.route("/prompts/<path:subpath>", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
def proxy_prompts(subpath=""):
PROMPTS_API_URL = "http://localhost:8781"
full_url = PROMPTS_API_URL + ("/" + subpath if subpath else "/")
if request.query_string:
full_url += "?" + request.query_string.decode()
try:
headers = {
k: v
for k, v in request.headers
if k.lower()
not in ("host", "content-length", "transfer-encoding", "connection")
}
raw_body = request.get_data()
resp = requests.request(
request.method,
full_url,
headers=headers,
data=raw_body,
cookies=request.cookies,
allow_redirects=False,
timeout=10,
)
if resp.status_code in (301, 302, 303, 307, 308):
location = resp.headers.get("Location", "")
if location.startswith("/") and not location.startswith("/prompts"):
location = "/prompts" + location
r = make_response(b"", resp.status_code)
r.headers["Location"] = location
return r
response = make_response(resp.content, resp.status_code)
for k, v in resp.headers.items():
if k.lower() not in ("content-encoding", "transfer-encoding", "connection"):
response.headers[k] = v
return response
except Exception as e:
return f"Erreur proxy prompts: {e}", 502
@app.route("/business")
def business():
return send_from_directory("/home/h3r7/depenses_trello/templates", "business.html")
@app.route(
"/depenses", methods=["GET", "POST", "PUT", "DELETE", "PATCH"], strict_slashes=False
)
@app.route(
"/depenses/",
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
strict_slashes=False,
)
@app.route(
"/depenses/<path:subpath>", methods=["GET", "POST", "PUT", "DELETE", "PATCH"]
)
def proxy_depenses(subpath=""):
backend_url = "http://localhost:8769"
if subpath:
full_url = f"{backend_url}/{subpath}"
else:
full_url = backend_url
if request.query_string:
full_url += "?" + request.query_string.decode()
try:
print(f"[DEPENSES PROXY] {request.method} {full_url}")
# Transmettre les headers bruts sans reparser
headers = {
k: v
for k, v in request.headers
if k.lower()
not in ("host", "content-length", "transfer-encoding", "connection")
}
# Body brut transmis tel quel — Content-Type préservé
raw_body = request.get_data()
resp = requests.request(
request.method,
full_url,
headers=headers,
data=raw_body,
cookies=request.cookies,
allow_redirects=False,
timeout=10,
)
print(f"[DEPENSES PROXY] resp={resp.status_code}")
if resp.status_code in (301, 302, 303, 307, 308):
location = resp.headers.get("Location", "")
if location.startswith("/") and not location.startswith("/depenses"):
location = "/depenses" + location
response = make_response(b"", resp.status_code)
response.headers["Location"] = location
response.headers["Content-Length"] = "0"
return response
if resp.status_code >= 400:
return resp.content, resp.status_code
response = make_response(resp.content, resp.status_code)
for k, v in resp.headers.items():
if k.lower() not in ("content-encoding", "transfer-encoding", "connection"):
response.headers[k] = v
return response
except Exception as e:
return f"Erreur proxy depenses: {e}", 502
@app.route("/skills", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
@app.route("/skills/", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
@app.route("/skills/<path:subpath>", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
def proxy_skills(subpath=""):
SKILLS_API_URL = "http://localhost:8772"
full_url = SKILLS_API_URL + ("/" + subpath if subpath else "/")
if request.query_string:
full_url += "?" + request.query_string.decode()
try:
headers = {
k: v
for k, v in request.headers
if k.lower()
not in ("host", "content-length", "transfer-encoding", "connection")
}
raw_body = request.get_data()
resp = requests.request(
request.method,
full_url,
headers=headers,
data=raw_body,
cookies=request.cookies,
allow_redirects=False,
timeout=10,
)
if resp.status_code in (301, 302, 303, 307, 308):
location = resp.headers.get("Location", "")
if location.startswith("/") and not location.startswith("/skills"):
location = "/skills" + location
r = make_response(b"", resp.status_code)
r.headers["Location"] = location
return r
response = make_response(resp.content, resp.status_code)
for k, v in resp.headers.items():
if k.lower() not in ("content-encoding", "transfer-encoding", "connection"):
response.headers[k] = v
return response
except Exception as e:
return f"Erreur proxy skills: {e}", 502
@app.route("/crm", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
@app.route("/crm/", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
@app.route("/crm/<path:subpath>", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
def proxy_crm(subpath=""):
CRM_API_URL = "http://localhost:8770"
full_url = CRM_API_URL + ("/" + subpath if subpath else "/")
if request.query_string:
full_url += "?" + request.query_string.decode()
try:
headers = {
k: v
for k, v in request.headers
if k.lower()
not in ("host", "content-length", "transfer-encoding", "connection")
}
raw_body = request.get_data()
resp = requests.request(
request.method,
full_url,
headers=headers,
data=raw_body,
cookies=request.cookies,
allow_redirects=False,
timeout=10,
)
if resp.status_code in (301, 302, 303, 307, 308):
location = resp.headers.get("Location", "")
if location.startswith("/") and not location.startswith("/crm"):
location = "/crm" + location
r = make_response(b"", resp.status_code)
r.headers["Location"] = location
return r
response = make_response(resp.content, resp.status_code)
for k, v in resp.headers.items():
if k.lower() not in ("content-encoding", "transfer-encoding", "connection"):
response.headers[k] = v
return response
except Exception as e:
return f"Erreur proxy crm: {e}", 502
@app.route("/gitea", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
@app.route("/gitea/", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
@app.route("/gitea/<path:subpath>", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
def proxy_gitea(subpath=""):
GITEA_API_URL = "http://localhost:3000"
full_url = GITEA_API_URL + ("/" + subpath if subpath else "/")
if request.query_string:
full_url += "?" + request.query_string.decode()
try:
headers = {
k: v
for k, v in request.headers
if k.lower()
not in ("host", "content-length", "transfer-encoding", "connection")
}
raw_body = request.get_data()
resp = requests.request(
request.method,
full_url,
headers=headers,
data=raw_body,
cookies=request.cookies,
allow_redirects=False,
timeout=10,
)
if resp.status_code in (301, 302, 303, 307, 308):
location = resp.headers.get("Location", "")
if location.startswith("/") and not location.startswith("/gitea"):
location = "/gitea" + location
r = make_response(b"", resp.status_code)
r.headers["Location"] = location
return r
response = make_response(resp.content, resp.status_code)
for k, v in resp.headers.items():
if k.lower() not in ("content-encoding", "transfer-encoding", "connection"):
response.headers[k] = v
return response
except Exception as e:
return f"Erreur proxy gitea: {e}", 502
@app.route("/boite_a_idees.html")
def boite_a_idees():
return send_from_directory("/home/h3r7/depenses_trello", "boite_a_idees.html")
@app.route("/niches_business.html")
def niches_business():
return send_from_directory("/home/h3r7/depenses_trello/templates", "business.html")
@app.route("/template_restaurant_json.html")
def template_restaurant():
return send_from_directory("/home/h3r7/turf_saas", "template_restaurant_json.html")
@app.route("/template_boulangerie_final.html")
def template_boulangerie():
return send_from_directory(
"/home/h3r7/turf_saas", "template_boulangerie_final.html"
)
@app.route("/template_artisan_final.html")
def template_artisan():
return send_from_directory("/home/h3r7/turf_saas", "template_artisan_final.html")
@app.route("/template_restaurant_final.html")
def template_restaurant_final():
return send_from_directory("/home/h3r7/turf_saas", "template_restaurant_final.html")
@app.route("/template_complet.html")
def template_complet():
return send_from_directory("/home/h3r7/turf_saas", "template_complet.html")
@app.route("/boite_a_idees_dashboard")
def boite_a_idees_dashboard():
return send_from_directory("/home/h3r7/turf_saas", "boite_a_idees_dashboard.html")
@app.route("/datagouv_explorer.html")
def datagouv_explorer():
return send_from_directory("/home/h3r7/turf_saas", "datagouv_explorer.html")
@app.route("/api_datagouv_reference.html")
def api_datagouv_reference():
return send_from_directory("/home/h3r7/turf_saas", "api_datagouv_reference.html")
# Agent IA - Page principale
@app.route("/agent-ia")
@app.route("/agent-ia/")
def agent_ia():
return send_from_directory("/home/h3r7/turf_saas", "agent_ia.html")
# Agent IA - Page de config
@app.route("/agent-ia/config")
@app.route("/agent-ia/config/")
def agent_ia_config():
return send_from_directory("/home/h3r7/turf_saas", "agent_ia_config.html")
# Ancienne page (compatibilité)
@app.route("/agent-ia/legacy")
def agent_ia_legacy():
return send_from_directory("/home/h3r7/turf_saas", "gemini_agent.html")
# --- API Chat ---
@app.route("/api/chat/workflows", methods=["GET"])
def api_chat_workflows():
try:
workflows = db.get_workflows()
return jsonify([dict(w) for w in workflows])
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/chat/nvidia-models", methods=["GET"])
def api_nvidia_models():
return jsonify(
[
{
"id": k,
"name": v.split("/")[-1]
.replace("-instruct", "")
.replace("-", " ")
.title(),
"full_id": v,
}
for k, v in NVIDIA_MODELS.items()
]
)
@app.route("/api/chat/sessions", methods=["GET"])
def api_chat_sessions():
workflow_slug = request.args.get("workflow")
if not workflow_slug:
return jsonify({"error": "Paramètre workflow requis"}), 400
try:
sessions = db.get_sessions(workflow_slug)
return jsonify([dict(s) for s in sessions])
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/chat/history", methods=["GET"])
def api_chat_history():
session_id = request.args.get("session_id")
workflow_slug = request.args.get("workflow")
if not session_id or not workflow_slug:
return jsonify({"error": "session_id et workflow requis"}), 400
try:
messages = db.get_messages(session_id, workflow_slug)
return jsonify([dict(m) for m in messages])
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/chat/search", methods=["GET"])
def api_chat_search():
query = request.args.get("q")
workflow_slug = request.args.get("workflow")
if not query or not workflow_slug:
return jsonify({"error": "q et workflow requis"}), 400
try:
results = db.search_messages(query, workflow_slug)
return jsonify([dict(r) for r in results])
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/chat/session", methods=["POST"])
def api_chat_session_create():
data = request.json or {}
session_id = data.get("session_id")
workflow_slug = data.get("workflow")
title = data.get("title")
if not session_id or not workflow_slug:
return jsonify({"error": "session_id et workflow requis"}), 400
try:
workflows = db.get_workflows()
wf = next((w for w in workflows if w["slug"] == workflow_slug), None)
if not wf:
return jsonify({"error": "Workflow introuvable"}), 404
db.create_session(session_id, wf["id"], title)
return jsonify({"status": "ok", "session_id": session_id})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/chat/session", methods=["PUT"])
def api_chat_session_rename():
data = request.json or {}
session_id = request.args.get("session_id")
workflow_slug = request.args.get("workflow")
new_title = data.get("title")
if not session_id or not workflow_slug:
return jsonify({"error": "session_id et workflow requis"}), 400
try:
db.rename_session(session_id, workflow_slug, new_title)
return jsonify({"status": "ok"})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/chat/session", methods=["DELETE"])
def api_chat_session_delete():
session_id = request.args.get("session_id")
workflow_slug = request.args.get("workflow")
if not session_id or not workflow_slug:
return jsonify({"error": "session_id et workflow requis"}), 400
try:
deleted = db.delete_session(session_id, workflow_slug)
return jsonify({"status": "ok", "deleted": deleted})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/chat/cleanup", methods=["POST"])
def api_chat_cleanup():
data = request.json or {}
days = data.get("days")
workflow_slug = data.get("workflow")
if not days or not workflow_slug:
return jsonify({"error": "days et workflow requis"}), 400
try:
deleted = db.delete_messages_before(days, workflow_slug)
return jsonify({"status": "ok", "deleted": deleted})
except Exception as e:
return jsonify({"error": str(e)}), 500
# --- Webhook proxy avec persistance ---
OPENCLAW_TOKEN = "szDd75yG15ZRADWEhGus3dfNoQve7ZhdxpGq9gudeEzoVhUqB0TomVJd90XcwkUz"
OPENCLAW_CONTAINER = "openclaw-ow404080wwkkgkgc4oswwssc"
NVIDIA_API_KEY = (
"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"
)
NVIDIA_API_URL = "https://integrate.api.nvidia.com/v1/chat/completions"
NVIDIA_MODEL = "meta/llama-3.1-8b-instruct" # Default model
NVIDIA_MODELS = {
"llama-3.1-8b": "meta/llama-3.1-8b-instruct",
"llama-3.1-70b": "meta/llama-3.1-70b-instruct",
"llama-3.3-70b": "meta/llama-3.3-70b-instruct",
"llama-4-scout": "meta/llama-4-scout-17b-16e-instruct",
"nemotron-mini": "nvidia/nemotron-mini-4b-instruct",
"nemotron-super": "nvidia/llama-3.3-nemotron-super-49b-v1",
"mistral-small": "mistralai/mistral-small-3.1-24b-instruct-2503",
"mistral-medium": "mistralai/mistral-medium-3-instruct",
"mistral-large": "mistralai/mistral-large-3-675b-instruct-2512",
"qwen-coder": "qwen/qwen3-coder-480b-a35b-instruct",
"gemma-3": "google/gemma-3-27b-it",
"deepseek": "deepseek-ai/deepseek-v3.2",
}
@app.route("/webhook/telegram", methods=["POST"])
def telegram_webhook():
try:
data = request.get_data()
resp = requests.post(
"http://localhost:5003/webhook",
data=data,
headers={"Content-Type": "application/json"},
timeout=30,
)
return resp.content, resp.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/webhook/<workflow_slug>", methods=["POST"])
def webhook_proxy(workflow_slug):
try:
workflows = db.get_workflows()
wf = next((w for w in workflows if w["slug"] == workflow_slug), None)
if not wf:
return jsonify({"error": f'Workflow "{workflow_slug}" introuvable'}), 404
session_id = request.headers.get("X-Session-ID", "default")
user_message = request.json.get("message", "")
db.create_session(session_id, wf["id"])
db.save_message(session_id, wf["id"], "user", user_message)
mode = wf.get("mode", "n8n")
if mode == "direct":
# OpenClaw gateway bind sur 127.0.0.1 uniquement → docker exec
import subprocess
escaped_msg = user_message.replace('"', '\\"').replace("\n", "\\n")
cmd = [
"docker",
"exec",
OPENCLAW_CONTAINER,
"curl",
"-s",
"http://127.0.0.1:18789/v1/chat/completions",
"-H",
"Content-Type: application/json",
"-H",
f"Authorization: Bearer {OPENCLAW_TOKEN}",
"-d",
f'{{"model":"openclaw","messages":[{{"role":"user","content":"{escaped_msg}"}}],"max_tokens":4096,"temperature":0.7}}',
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode != 0:
raise Exception(f"docker exec failed: {result.stderr}")
data = json.loads(result.stdout)
ai_response = (
data.get("choices", [{}])[0]
.get("message", {})
.get("content", str(data))
)
elif mode == "nvidia":
# Appel direct a l API Nvidia NIM
# Recuperer le modele choisi (par defaut: llama-3.1-8b)
model_key = request.json.get("model", "llama-3.1-8b")
model_id = NVIDIA_MODELS.get(model_key, NVIDIA_MODEL)
resp = requests.post(
NVIDIA_API_URL,
headers={
"Authorization": f"Bearer {NVIDIA_API_KEY}",
"Content-Type": "application/json",
},
json={
"model": model_id,
"messages": [{"role": "user", "content": user_message}],
"max_tokens": 1024,
"temperature": 0.7,
},
timeout=60,
)
data = resp.json()
ai_response = (
data.get("choices", [{}])[0]
.get("message", {})
.get("content", str(data))
)
else:
# Proxy vers webhook n8n
resp = requests.post(
wf["webhook_url"],
headers={
"Content-Type": "application/json",
"X-Session-ID": session_id,
},
json=request.json,
timeout=60,
)
ai_response = resp.text
db.save_message(session_id, wf["id"], "ai", ai_response)
return ai_response, 200, {"Content-Type": "text/plain; charset=utf-8"}
except Exception as e:
return str(e), 500
# --- Anciens webhooks (compatibilité) ---
@app.route("/webhook/gemini-agent", methods=["POST"])
def webhook_proxy_legacy_gemini():
return webhook_proxy("llm-models")
@app.route("/webhook/openclaw-web", methods=["POST"])
def webhook_proxy_legacy_openclaw():
return webhook_proxy("openclaw")
TELEGRAM_TOKEN = "8649773134:AAFqzZVtSHfPPFDadcte1B-1h23nZ8DmdYE"
@app.route("/webhook/telegram-opencode", methods=["POST"])
def webhook_proxy_telegram():
"""Handle Telegram messages directly"""
try:
data = request.get_json(force=True, silent=True)
print(f"[TELEGRAM] Raw: {request.data}")
print(f"[TELEGRAM] Remote: {request.remote_addr}")
print(f"[TELEGRAM] Headers: {request.headers.get('X-Forwarded-For', 'none')}")
if not data:
return jsonify({"error": "No data", "raw": request.data.decode()}), 400
# Accept both direct message and Telegram update format
message = data.get("message") or data.get("update", {}).get("message")
if not message:
return jsonify({"data": data, "keys": list(data.keys())}), 400
chat_id = message["chat"]["id"]
text = message.get("text", "")
user_id = message["from"]["id"]
if text.startswith("/start"):
reply = "🤖 *OpencdPilot Bot*\n\nPilotez OpenCode depuis Telegram!"
elif text.startswith("/help"):
reply = "*Commandes:*\n/start - Démarrer\n/help - Aide\n/status - Statut"
elif text.startswith("/status"):
reply = "✅ *Système actif*\n- OpenCode: OK\n- Bot: OK"
else:
# Call OpenCode API
opencode_resp = requests.post(
"http://localhost:8792/api/opencode", json={"prompt": text}, timeout=180
)
opencode_data = opencode_resp.json()
reply = opencode_data.get("output", opencode_data.get("error", "Erreur"))[
:4000
]
# Send reply
requests.post(
f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
json={"chat_id": chat_id, "text": reply, "parse_mode": "Markdown"},
)
return jsonify({"ok": True})
except Exception as e:
print(f"[TELEGRAM] Error: {e}")
return jsonify({"error": str(e)}), 500
# --- Turf Dashboard ---
@app.route("/dashboard")
@app.route("/dashboard.html")
def dashboard():
return send_from_directory("/home/h3r7/turf_saas", "dashboard_system.html")
@app.route("/turf/")
@app.route("/turf")
def turf_index():
return send_from_directory("/home/h3r7/turf_saas", "dashboard.html")
@app.route("/turf/<path:filename>")
def turf_static(filename):
return send_from_directory("/home/h3r7/turf_saas", filename)
# --- POD Routes ---
@app.route("/pod/")
@app.route("/pod/<path:filename>")
def pod_static(filename=""):
return send_from_directory(
"/home/h3r7/turf_saas/POD", filename if filename else "pod_manager.html"
)
@app.route("/turf/api")
@app.route("/turf/api/")
@app.route("/turf/api/<path:api_path>")
def api_proxy(api_path=""):
if api_path.startswith("vitesse"):
url = f"{COMBINED_API_URL}/turf/api/{api_path}"
elif api_path.startswith("n8n-proxy"):
url = f"{COMBINED_API_URL}/turf/api/{api_path}"
elif api_path.startswith("backtest"):
url = f"{COMBINED_API_URL}/turf/api/{api_path}"
elif api_path.startswith("stats"):
url = f"{COMBINED_API_URL}/turf/api/{api_path}"
elif api_path.startswith("predictions_analysis"):
url = f"{COMBINED_API_URL}/turf/api/{api_path}"
elif api_path.startswith("parisroi"):
url = f"{COMBINED_API_URL}/turf/api/{api_path}"
elif api_path.startswith("paris"):
url = f"{COMBINED_API_URL}/turf/api/{api_path}"
elif api_path.startswith("scoring"):
url = f"{DASHBOARD_API_URL}/turf/api/{api_path}"
elif api_path:
url = f"{DASHBOARD_API_URL}/turf/api/{api_path}"
else:
url = f"{DASHBOARD_API_URL}/turf/api"
try:
fwd_method = request.method
fwd_json = (
request.get_json(silent=True)
if fwd_method in ("POST", "PUT", "PATCH")
else None
)
fwd_headers = {"Content-Type": "application/json"}
if request.headers.get("Authorization"):
fwd_headers["Authorization"] = request.headers.get("Authorization")
resp = requests.request(
method=fwd_method, url=url, json=fwd_json, timeout=30, headers=fwd_headers
)
return resp.content, resp.status_code, {"Content-Type": "application/json"}
except Exception as e:
return jsonify({"error": str(e), "url": url}), 500
@app.route("/api/opencode", methods=["POST"])
def opencode_api():
"""Execute OpenCode commands via API"""
try:
data = request.get_json()
prompt = data.get("prompt", "")
if not prompt:
return jsonify({"error": "No prompt provided"}), 400
# Execute opencode with the wrapper script
result = subprocess.run(
["/home/h3r7/opencode_wrapper.sh", prompt],
cwd="/home/h3r7",
capture_output=True,
text=True,
timeout=180,
)
return jsonify(
{
"output": result.stdout,
"error": result.stderr,
"returncode": result.returncode,
}
)
except subprocess.TimeoutExpired:
return jsonify({"error": "Timeout"}), 504
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/candidatures/")
def candidatures_index():
return send_from_directory("/home/h3r7/turf_saas", "crm_candidatures.html")
@app.route("/candidatures/<path:filename>")
def candidatures_static(filename):
return send_from_directory("/home/h3r7/turf_saas", filename)
@app.route("/map")
def map_visual():
return send_from_directory("/home/h3r7/turf_saas", "map_visual.html")
@app.route("/architecture.json")
def architecture_json():
return send_from_directory("/home/h3r7/turf_saas", "architecture.json")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8792, debug=False)
# Telegram Bot Webhook Proxy
def telegram_webhook():
"""Proxy Telegram webhook to bot service"""
try:
data = request.get_data()
resp = requests.post(
"http://localhost:5003/webhook",
data=data,
headers={"Content-Type": "application/json"},
timeout=30,
)
return resp.content, resp.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
PROMPTS_API_URL = "http://localhost:8781"
@app.route("/testprompts", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
def proxy_prompts_test():
return "TEST_PROMPTS_OK"
full_url = PROMPTS_API_URL + ("/" + subpath if subpath else "/")
if request.query_string:
full_url += "?" + request.query_string.decode()
try:
headers = {
k: v
for k, v in request.headers
if k.lower()
not in (
"host",
"content-length",
"transfer-encoding",
"connection",
"authorization",
)
}
raw_body = request.get_data()
resp = requests.request(
request.method,
full_url,
headers=headers,
data=raw_body,
cookies=request.cookies,
allow_redirects=False,
timeout=10,
)
if resp.status_code in (301, 302, 303, 307, 308):
location = resp.headers.get("Location", "")
if location.startswith("/") and not location.startswith("/prompts"):
location = "/prompts" + location
r = make_response(b"", resp.status_code)
r.headers["Location"] = location
return r
response = make_response(resp.content, resp.status_code)
for k, v in resp.headers.items():
if k.lower() not in ("content-encoding", "transfer-encoding", "connection"):
response.headers[k] = v
return response
except Exception as e:
return f"Erreur proxy prompts: {e}", 502