| Server IP : 54.37.205.81 / Your IP : 216.73.216.76 Web Server : nginx/1.22.1 System : Linux vps-249481fa 6.1.0-50-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.176-1 (2026-07-02) x86_64 User : debian ( 1000) PHP Version : 8.2.32 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /opt/ccp-ai-gateway/ |
Upload File : |
import os
import json
import time
import secrets
import tempfile
import re
from datetime import datetime, timezone
from flask import Flask, request, jsonify, make_response
from flask_cors import CORS
import requests
# PDF + QR
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.lib import colors
try:
import qrcode # pip install "qrcode[pil]"
except Exception:
qrcode = None
APP = Flask(__name__)
CORS(APP)
# ================== ENV ==================
ADMIN_TOKEN = os.environ.get("CCP_AI_ADMIN_TOKEN", "")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
TOWER_PASSWORD = os.environ.get("CCP_TOWER_PASSWORD", "")
SESSION_SECRET = os.environ.get("CCP_SESSION_SECRET", "")
DATA_DIR = os.environ.get("CCP_AI_DATA_DIR", "/opt/ccp-ai-gateway/data")
USERS_FILE = os.path.join(DATA_DIR, "users.json")
USAGE_LOG = os.path.join(DATA_DIR, "ai_usage.jsonl")
CERT_USAGE_LOG = os.path.join(DATA_DIR, "cert_usage.jsonl")
BALANCES_FILE = os.path.join(DATA_DIR, "balances.json")
PRICING_FILE = os.path.join(DATA_DIR, "pricing.json")
# stima costo EUR (solo reporting)
COST_OPENAI_EUR = float(os.environ.get("CCP_COST_OPENAI_EUR_PER_REQ", "0.01"))
COST_GEMINI_EUR = float(os.environ.get("CCP_COST_GEMINI_EUR_PER_REQ", "0.04"))
os.makedirs(DATA_DIR, exist_ok=True)
# ================== UTILS ==================
def _now_iso():
return datetime.now(timezone.utc).isoformat()
def _json_load(path, default):
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return default
def _json_save(path, obj):
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(obj, f, ensure_ascii=False, indent=2)
os.replace(tmp, path)
def _append_jsonl(path, obj):
with open(path, "a", encoding="utf-8") as f:
f.write(json.dumps(obj, ensure_ascii=False) + "\n")
def _safe_filename(s: str) -> str:
s = (s or "").strip()
s = re.sub(r"[^a-zA-Z0-9_\-\.]+", "_", s)
return s[:120] if s else "file"
# ================== AUTH (admin header token) ==================
def require_admin_token(req):
if not ADMIN_TOKEN:
return True
return req.headers.get("X-CCP-Token", "") == ADMIN_TOKEN
# ================== TOWER COOKIE SIGN ==================
def _sign(payload: dict) -> str:
raw = json.dumps(payload, separators=(",", ":"), sort_keys=True)
salt = secrets.token_urlsafe(12)
import hashlib
h = hashlib.sha256()
h.update((SESSION_SECRET or "").encode("utf-8"))
h.update(salt.encode("utf-8"))
h.update(raw.encode("utf-8"))
return f"{salt}.{h.hexdigest()}.{raw}"
def _verify(token: str):
try:
salt, digest, raw = token.split(".", 2)
import hashlib
h = hashlib.sha256()
h.update((SESSION_SECRET or "").encode("utf-8"))
h.update(salt.encode("utf-8"))
h.update(raw.encode("utf-8"))
if h.hexdigest() != digest:
return None
payload = json.loads(raw)
if payload.get("exp") and time.time() > float(payload["exp"]):
return None
return payload
except Exception:
return None
def tower_session():
tok = request.cookies.get("ccp_tower", "")
# curl cookie-jar può salvare tra doppi apici e con virgole escaped (\054)
tok = (tok or "").strip('"').replace("\\054", ",")
if not tok or not SESSION_SECRET:
return None
payload = _verify(tok)
if not payload or payload.get("role") != "tower_admin":
return None
return payload
def _tower_guard():
if not (TOWER_PASSWORD and SESSION_SECRET and tower_session()):
return jsonify({"error": "unauthorized"}), 401
return None
# ================== USERS ==================
def ensure_users_file():
if not os.path.exists(USERS_FILE):
_json_save(USERS_FILE, {"users": []})
def find_user_by_token(tok: str):
tok = (tok or "").strip()
if not tok:
return None
ensure_users_file()
data = _json_load(USERS_FILE, {"users": []})
for u in data.get("users", []):
if (u.get("token") or "").strip() == tok:
return u
return None
def upsert_user(user: dict):
ensure_users_file()
data = _json_load(USERS_FILE, {"users": []})
users = data.get("users", [])
uid = str(user.get("id"))
found = False
for i, u in enumerate(users):
if str(u.get("id")) == uid:
users[i] = {**u, **user}
found = True
break
if not found:
users.append(user)
data["users"] = users
_json_save(USERS_FILE, data)
# ================== BALANCES (ABO) ==================
def ensure_balances_file():
if not os.path.exists(BALANCES_FILE):
_json_save(BALANCES_FILE, {"balances": {}})
def get_balance(uid: str) -> float:
ensure_balances_file()
data = _json_load(BALANCES_FILE, {"balances": {}})
b = data.get("balances", {}).get(str(uid), 0.0)
try:
return float(b)
except Exception:
return 0.0
def set_balance(uid: str, amount: float):
ensure_balances_file()
data = _json_load(BALANCES_FILE, {"balances": {}})
data.setdefault("balances", {})[str(uid)] = float(amount)
_json_save(BALANCES_FILE, data)
def add_balance(uid: str, delta: float) -> float:
cur = get_balance(uid)
newv = float(cur) + float(delta)
set_balance(uid, newv)
return newv
# ================== PRICING (ABO per operazione) ==================
def ensure_pricing_file():
if not os.path.exists(PRICING_FILE):
_json_save(PRICING_FILE, {
"default_op_cost_abo": 1.0,
"operations": {
"generic": 1.0,
"cert_base": 1.0,
"cert_blockchain": 2.0
}
})
def get_pricing():
ensure_pricing_file()
data = _json_load(PRICING_FILE, {
"default_op_cost_abo": 1.0,
"operations": {"generic": 1.0, "cert_base": 1.0, "cert_blockchain": 2.0}
})
data.setdefault("default_op_cost_abo", 1.0)
data.setdefault("operations", {"generic": data["default_op_cost_abo"]})
return data
def op_cost_abo(op: str) -> float:
p = get_pricing()
op = (op or "generic").strip().lower()
ops = p.get("operations", {}) or {}
if op in ops:
try:
return float(ops[op])
except Exception:
return float(p.get("default_op_cost_abo", 1.0))
return float(p.get("default_op_cost_abo", 1.0))
def debit_or_reject(uid: str, op: str):
cost = op_cost_abo(op)
bal = get_balance(uid)
if bal < cost:
return False, bal, cost
newb = add_balance(uid, -cost)
return True, newb, cost
# ================== USAGE (log) ==================
def cert_seen_request(uid: str, request_id: str) -> bool:
request_id = (request_id or "").strip()
if not request_id:
return False
try:
with open(CERT_USAGE_LOG, "r", encoding="utf-8") as f:
for line in f:
try:
row = json.loads(line)
if str(row.get("user_id")) == str(uid) and str(row.get("request_id")) == request_id:
return True
except Exception:
continue
except Exception:
return False
return False
def cert_log_request(**kw):
_append_jsonl(CERT_USAGE_LOG, {"ts": _now_iso(), **kw})
def log_ai_usage(**kw):
_append_jsonl(USAGE_LOG, {"ts": _now_iso(), **kw})
# ================== PDF CERTIFICATE (BASE - template stile tuo PDF) ==================
def generate_base_certificate(
file_hash: str,
uid: str,
request_id: str,
*,
cert_id: str = None,
work_id: str = None,
deposit_date: str = None,
author: str = None,
author_org: str = None,
original_filename: str = None,
verify_url: str = None
):
out_dir = os.path.join(DATA_DIR, "certificates", "base")
os.makedirs(out_dir, exist_ok=True)
cert_id = (cert_id or f"CD-{request_id}").strip()
work_id = (work_id or request_id).strip()
deposit_date = (deposit_date or datetime.now(timezone.utc).strftime("%d/%m/%Y")).strip()
author = (author or uid).strip()
author_org = (author_org or "ABO Bank Ufficio Legale\nDipartimento di Proprietà\nIntellettuale e Diritto d’Autore").strip()
original_filename = (original_filename or f"{work_id}.pdf").strip()
verify_url = (verify_url or "").strip() or f"https://copyrightchain.it/verify?cid={cert_id}"
filename = f"cert_base_{_safe_filename(uid)}_{_safe_filename(request_id)}.pdf"
fullpath = os.path.join(out_dir, filename)
# colori
C_TEXT = colors.HexColor("#0b1220")
C_MUTED = colors.HexColor("#667085")
C_BORDER = colors.HexColor("#e5e7eb")
C_PANEL = colors.HexColor("#f3f4f6")
C_BADGE = colors.HexColor("#eef2ff")
C_BADGE_TEXT = colors.HexColor("#1d4ed8")
C_DARK = colors.HexColor("#111827")
c = canvas.Canvas(fullpath, pagesize=A4)
w, h = A4
M = 2.0 * cm
TOP = h - 2.2 * cm
# icona
shield_x = w/2 - 0.8*cm
shield_y = TOP + 0.2*cm
c.setFillColor(C_DARK)
c.roundRect(shield_x, shield_y, 1.6*cm, 1.0*cm, 0.35*cm, stroke=0, fill=1)
c.setFillColor(colors.white)
c.setFont("Helvetica-Bold", 10)
c.drawCentredString(w/2, shield_y + 0.32*cm, "✓")
# titolo
c.setFillColor(C_TEXT)
c.setFont("Helvetica-Bold", 20)
c.drawCentredString(w/2, TOP - 0.6*cm, "CERTIFICATO DI PROPRIETÀ")
c.drawCentredString(w/2, TOP - 1.35*cm, "INTELLETTUALE")
c.setFont("Helvetica", 8.5)
c.setFillColor(C_MUTED)
c.drawCentredString(w/2, TOP - 2.0*cm, "EMESSO DA ABO BANK LEGAL DIVISION")
# badge proof
badge_y = TOP - 3.2*cm
badge_h = 1.2*cm
badge_w = w - 2*M
c.setFillColor(C_BADGE)
c.roundRect(M, badge_y, badge_w, badge_h, 0.25*cm, stroke=0, fill=1)
c.setFillColor(C_BADGE_TEXT)
c.setFont("Helvetica-Bold", 9)
c.drawString(M + 1.0*cm, badge_y + 0.75*cm, "DIGITAL FINGERPRINT PROOF")
c.setFont("Helvetica", 8)
c.setFillColor(C_MUTED)
c.drawString(M + 1.0*cm, badge_y + 0.35*cm, "STATUS: CERTIFIED")
# info 2 colonne
info_top = badge_y - 0.9*cm
col_w = (w - 2*M) / 2.0
c.setFillColor(C_MUTED); c.setFont("Helvetica", 8)
c.drawString(M, info_top, "TITOLO / IDENTIFICATORE")
c.setFillColor(C_TEXT); c.setFont("Helvetica-Bold", 11)
c.drawString(M, info_top - 0.55*cm, str(work_id))
c.setFillColor(C_MUTED); c.setFont("Helvetica", 8)
c.drawString(M + col_w, info_top, "ID CERTIFICATO")
c.setFillColor(C_BADGE_TEXT); c.setFont("Helvetica-Bold", 11)
c.drawString(M + col_w, info_top - 0.55*cm, str(cert_id))
# riga 3 campi
row2_y = info_top - 1.55*cm
third = (w - 2*M) / 3.0
c.setFillColor(C_MUTED); c.setFont("Helvetica", 8)
c.drawString(M, row2_y, "DATA DI DEPOSITO")
c.setFillColor(C_TEXT); c.setFont("Helvetica-Bold", 9.5)
c.drawString(M, row2_y - 0.45*cm, str(deposit_date))
c.setFillColor(C_MUTED); c.setFont("Helvetica", 8)
c.drawString(M + third, row2_y, "AUTORE / REGISTRANTE")
c.setFillColor(C_TEXT); c.setFont("Helvetica-Bold", 9.5)
c.drawString(M + third, row2_y - 0.45*cm, str(author))
c.setFillColor(C_MUTED); c.setFont("Helvetica", 8)
c.drawString(M + 2*third, row2_y, "FILE ORIGINALE")
c.setFillColor(C_TEXT); c.setFont("Helvetica-Bold", 9.5)
c.drawString(M + 2*third, row2_y - 0.45*cm, str(original_filename))
# separatore
sep_y = row2_y - 0.9*cm
c.setStrokeColor(C_BORDER)
c.setLineWidth(1)
c.line(M, sep_y, w - M, sep_y)
# box attestazione
box_y = sep_y - 8.2*cm
box_h = 7.4*cm
box_w = w - 2*M
c.setFillColor(C_PANEL)
c.roundRect(M, box_y, box_w, box_h, 0.25*cm, stroke=0, fill=1)
c.setFillColor(C_MUTED)
c.setFont("Helvetica-Bold", 9)
c.drawString(M + 0.6*cm, box_y + box_h - 0.8*cm, "ATTESTAZIONE LEGALE")
legal = (
"Il presente documento costituisce attestazione di avvenuta registrazione con marcatura temporale "
"e impronta digitale (SHA-256) del contenuto indicato. L’impronta digitale consente di verificare "
"l’integrità del file: qualsiasi modifica, anche minima, produce un’impronta differente.\n\n"
"La certificazione BASE fornisce prova di esistenza alla data indicata mediante registrazione e log "
"di sistema. La verifica può essere effettuata confrontando l’hash riportato con quello calcolato "
"sul file originale."
)
c.setFillColor(C_TEXT)
c.setFont("Helvetica", 9)
text = c.beginText(M + 0.6*cm, box_y + box_h - 1.5*cm)
text.setLeading(12)
for line in legal.split("\n"):
text.textLine(line)
c.drawText(text)
# footer fingerprint + evidence + QR
foot_y = box_y - 2.2*cm
fp_w = (w - 2*M) * 0.62
fp_h = 1.0*cm
c.setFillColor(colors.white)
c.setStrokeColor(C_BORDER)
c.roundRect(M, foot_y + 0.9*cm, fp_w, fp_h, 0.2*cm, stroke=1, fill=1)
c.setFillColor(C_MUTED)
c.setFont("Helvetica", 8)
c.drawString(M, foot_y + 2.2*cm, "DIGITAL FINGERPRINT (SHA-256 HASH)")
c.setFillColor(C_TEXT)
c.setFont("Helvetica", 8.5)
c.drawString(M + 0.35*cm, foot_y + 1.22*cm, (file_hash or "")[:90])
ev_x = M + fp_w + 0.6*cm
ev_w = (w - 2*M) - fp_w - 0.6*cm - 4.0*cm
c.setFillColor(C_DARK)
c.setStrokeColor(C_DARK)
c.roundRect(ev_x, foot_y + 0.9*cm, ev_w, fp_h, 0.2*cm, stroke=1, fill=1)
c.setFillColor(colors.white)
c.setFont("Helvetica", 8)
c.drawString(ev_x + 0.35*cm, foot_y + 1.22*cm, "EVIDENCE: BASE (N/A)")
# QR
qr_x = w - M - 3.6*cm
qr_y = foot_y + 0.35*cm
qr_size = 3.2*cm
if qrcode is not None:
tmp_png = None
try:
qr_img = qrcode.make(verify_url)
fd, tmp_png = tempfile.mkstemp(prefix="ccp_qr_", suffix=".png")
os.close(fd)
qr_img.save(tmp_png)
c.drawImage(tmp_png, qr_x, qr_y, width=qr_size, height=qr_size, mask="auto")
finally:
if tmp_png and os.path.exists(tmp_png):
try:
os.remove(tmp_png)
except Exception:
pass
else:
# fallback: niente qr, solo testo url
c.setFillColor(C_MUTED)
c.setFont("Helvetica", 7.5)
c.drawString(qr_x - 1.0*cm, qr_y + 1.5*cm, "QR DISABILITATO")
c.drawString(qr_x - 1.0*cm, qr_y + 1.1*cm, verify_url[:55])
c.setFillColor(C_MUTED)
c.setFont("Helvetica-Bold", 7.5)
c.drawCentredString(qr_x + qr_size/2, qr_y - 0.2*cm, "SCANSIONA PER VERIFICARE")
# badge finale
badge2_y = 1.2*cm
c.setFillColor(colors.HexColor("#e8fff0"))
c.setStrokeColor(colors.HexColor("#86efac"))
c.roundRect(M, badge2_y, 6.0*cm, 0.8*cm, 0.2*cm, stroke=1, fill=1)
c.setFillColor(colors.HexColor("#166534"))
c.setFont("Helvetica-Bold", 8)
c.drawCentredString(M + 3.0*cm, badge2_y + 0.27*cm, "CERTIFICAZIONE AUTENTICATA")
c.setFillColor(C_MUTED)
c.setFont("Helvetica", 7.5)
c.drawString(M, 0.6*cm, "Documento generato dal gateway CopyrightChain ai sensi delle normative vigenti.")
c.drawRightString(w - M, 0.6*cm, "ABO BANK LEGAL SERVICES • INTELLECTUAL PROPERTY DIVISION")
c.showPage()
c.save()
return fullpath
# ================== CERT CORE ==================
def _resolve_uid_from_request(payload):
tok = request.headers.get("X-CCP-Token", "")
user = find_user_by_token(tok) if tok else None
if user:
return str(user.get("id")), user, None
uid = payload.get("user_id")
if uid:
return str(uid), None, None
return None, None, (jsonify({"error": "unauthorized"}), 401)
def _cert_debit_core(payload):
cert_type = str(payload.get("cert_type", "base")).strip().lower()
request_id = str(payload.get("request_id", "")).strip()
file_hash = str(payload.get("file_hash", "")).strip()
# campi extra per PDF (come nel tuo modello)
cert_id = (payload.get("cert_id") or None)
work_id = (payload.get("work_id") or None)
deposit_date = (payload.get("deposit_date") or None)
author = (payload.get("author") or None)
author_org = (payload.get("author_org") or None)
original_filename = (payload.get("original_filename") or None)
verify_url = (payload.get("verify_url") or None)
cert_id = str(cert_id).strip() if cert_id is not None else None
work_id = str(work_id).strip() if work_id is not None else None
deposit_date = str(deposit_date).strip() if deposit_date is not None else None
author = str(author).strip() if author is not None else None
author_org = str(author_org).strip() if author_org is not None else None
original_filename = str(original_filename).strip() if original_filename is not None else None
verify_url = str(verify_url).strip() if verify_url is not None else None
if cert_type not in ("base", "blockchain"):
return jsonify({"error": "invalid_cert_type", "allowed": ["base", "blockchain"]}), 400
if not request_id:
return jsonify({"error": "missing_request_id"}), 400
uid, user, err = _resolve_uid_from_request(payload)
if err:
return err
op = "cert_blockchain" if cert_type == "blockchain" else "cert_base"
# idempotenza
if cert_seen_request(uid, request_id):
return jsonify({
"ok": True,
"user_id": uid,
"cert_type": cert_type,
"op": op,
"idempotent_replay": True,
"request_id": request_id,
"balance_abo": get_balance(uid)
}), 200
# addebito
ok_debit, bal_before, cost_abo = debit_or_reject(uid, op)
if not ok_debit:
return jsonify({
"error": "insufficient_balance",
"op": op,
"cert_type": cert_type,
"cost_abo": cost_abo,
"balance_abo": bal_before
}), 402
certificate_path = None
try:
# genera PDF SOLO per base
if cert_type == "base":
certificate_path = generate_base_certificate(
file_hash=file_hash,
uid=str(uid),
request_id=str(request_id),
cert_id=cert_id,
work_id=work_id,
deposit_date=deposit_date,
author=author,
author_org=author_org,
original_filename=original_filename,
verify_url=verify_url
)
except Exception as e:
# se fallisce la generazione PDF: rimborsa ABO e NON registra la request (così puoi riprovare)
add_balance(uid, cost_abo)
log_ai_usage(
user_id=uid,
provider="gateway",
ok=False,
status=500,
ms=0,
cost_eur=0.0,
op="cert_pdf_base",
abo_cost=0.0,
cert_type=cert_type,
request_id=request_id,
error="pdf_generation_failed",
details=str(e)
)
return jsonify({"error": "pdf_generation_failed", "details": str(e)}), 500
# registra request SOLO se tutto ok
cert_log_request(
user_id=uid,
request_id=request_id,
op=op,
cert_type=cert_type,
abo_cost=cost_abo,
file_hash=file_hash
)
log_ai_usage(
user_id=uid,
provider="gateway",
ok=True,
status=200,
ms=0,
cost_eur=0.0,
op=op,
abo_cost=cost_abo,
cert_type=cert_type,
request_id=request_id
)
return jsonify({
"ok": True,
"user_id": uid,
"cert_type": cert_type,
"op": op,
"debited_abo": cost_abo,
"idempotent_replay": False,
"request_id": request_id,
"balance_abo": get_balance(uid),
"certificate_path": certificate_path
}), 200
# ================== ROUTES ==================
@APP.get("/health")
def health():
return jsonify({
"ok": True,
"has_openai_key": bool(OPENAI_API_KEY),
"has_gemini_key": bool(GEMINI_API_KEY),
"tower_configured": bool(TOWER_PASSWORD and SESSION_SECRET),
"has_qrcode": bool(qrcode is not None)
})
@APP.post("/tower/login")
def tower_login():
data = request.get_json(silent=True) or {}
if data.get("password") != TOWER_PASSWORD:
return jsonify({"error": "unauthorized"}), 401
tok = _sign({"role": "tower_admin", "exp": time.time() + 86400})
resp = make_response(jsonify({"ok": True}))
# NOTA: secure=False per permettere curl su http://127.0.0.1
resp.set_cookie("ccp_tower", tok, httponly=True, secure=False, samesite="Lax", path="/")
return resp
@APP.post("/tower/balances/topup")
def tower_balances_topup():
g = _tower_guard()
if g:
return g
data = request.get_json(silent=True) or {}
uid = str(data.get("user_id", "")).strip()
amount = data.get("amount", None)
if not uid or amount is None:
return jsonify({"error": "missing_params"}), 400
try:
amount = float(amount)
except Exception:
return jsonify({"error": "invalid_amount"}), 400
newb = add_balance(uid, amount)
return jsonify({"ok": True, "user_id": uid, "balance_abo": newb})
@APP.post("/tower/balances/set")
def tower_balances_set():
g = _tower_guard()
if g:
return g
data = request.get_json(silent=True) or {}
uid = str(data.get("user_id", "")).strip()
balance = data.get("balance", None)
if not uid or balance is None:
return jsonify({"error": "missing_params"}), 400
try:
balance = float(balance)
except Exception:
return jsonify({"error": "invalid_balance"}), 400
set_balance(uid, balance)
return jsonify({"ok": True, "user_id": uid, "balance_abo": balance})
@APP.get("/tower/pricing")
def tower_pricing_get():
g = _tower_guard()
if g:
return g
return jsonify(get_pricing())
@APP.post("/tower/pricing/set")
def tower_pricing_set():
g = _tower_guard()
if g:
return g
data = request.get_json(silent=True) or {}
cur = get_pricing()
default_cost = data.get("default_op_cost_abo", None)
operations = data.get("operations", None)
if default_cost is not None:
try:
cur["default_op_cost_abo"] = float(default_cost)
except Exception:
return jsonify({"error": "invalid_default_cost"}), 400
if operations is not None:
if not isinstance(operations, dict):
return jsonify({"error": "invalid_operations"}), 400
cleaned = {}
for k, v in operations.items():
try:
cleaned[str(k).strip().lower()] = float(v)
except Exception:
continue
if cleaned:
cur["operations"] = cleaned
_json_save(PRICING_FILE, cur)
return jsonify({"ok": True, "pricing": cur})
@APP.post("/certify")
def certify():
payload = request.get_json(silent=True) or {}
return _cert_debit_core(payload)
@APP.post("/cert/debit")
def cert_debit():
payload = request.get_json(silent=True) or {}
tok = request.headers.get("X-CCP-Token", "")
user = find_user_by_token(tok)
if not user:
return jsonify({"error": "unauthorized"}), 401
return _cert_debit_core(payload)
@APP.post("/text")
def ai_text():
payload = request.get_json(silent=True) or {}
tok = request.headers.get("X-CCP-Token", "")
user = find_user_by_token(tok)
if not user:
return jsonify({"error": "unauthorized"}), 401
prompt = str(payload.get("prompt", "")).strip()
if not prompt:
return jsonify({"error": "missing_prompt"}), 400
op = str(payload.get("op", "generic")).strip().lower()
uid = str(user.get("id"))
ok_debit, bal_before, cost_abo = debit_or_reject(uid, op)
if not ok_debit:
return jsonify({
"error": "insufficient_balance",
"op": op,
"cost_abo": cost_abo,
"balance_abo": bal_before
}), 402
t0 = time.time()
r = requests.post(
"https://api.openai.com/v1/responses",
headers={"Authorization": f"Bearer {OPENAI_API_KEY}"},
json={"model": "gpt-4.1-mini", "input": [{"role": "user", "content": [{"type": "input_text", "text": prompt}]}]},
timeout=120
)
ms = int((time.time() - t0) * 1000)
if r.status_code >= 400:
add_balance(uid, cost_abo)
log_ai_usage(
user_id=uid, provider="openai", ok=False, status=r.status_code, ms=ms,
cost_eur=0.0, op=op, abo_cost=cost_abo, error="openai_error"
)
return jsonify({"error": "openai_error", "details": r.text}), r.status_code
log_ai_usage(
user_id=uid, provider="openai", ok=True, status=200, ms=ms,
cost_eur=COST_OPENAI_EUR, op=op, abo_cost=cost_abo
)
try:
j = r.json()
except Exception:
j = {}
text = (j.get("output_text") or "").strip()
if not text:
out = j.get("output") or []
if isinstance(out, list):
parts = []
for it in out:
if isinstance(it, dict):
for c_ in (it.get("content") or []):
if isinstance(c_, dict) and (c_.get("text") or ""):
parts.append(c_.get("text"))
text = ("".join(parts)).strip()
return jsonify({"ok": True, "text": text})
if __name__ == "__main__":
APP.run("127.0.0.1", 5060)