403Webshell
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 :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /opt/ccp-ai-gateway/app.py.bak_tower_pdf_only_20260423
import os
import json
import time
import secrets
import subprocess
import re
from datetime import datetime, timezone

import hashlib, secrets
from flask import Flask, request, jsonify, make_response, send_file
from flask_cors import CORS
import requests
import stripe

# PDF deps
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm

# QR (optional but supported)
try:
    import qrcode
    HAS_QRCODE = True
except Exception:
    qrcode = None
    HAS_QRCODE = False

APP = Flask(__name__)
CORS(
    APP,
    resources={r"/*": {"origins": [
        "https://admin.copyrightchain.it",
        "https://app.copyrightchain.it",
        "https://copyrightchain.it",
    ]}},
    supports_credentials=True,
    allow_headers=["Content-Type", "Authorization", "X-CCP-Token"],
    methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
)

# ================== 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")

# Public base URL (optional): e.g. https://copyrightchain.it/api/ai
PUBLIC_BASE_URL = os.environ.get("CCP_PUBLIC_BASE_URL", "").rstrip("/")
VERIFY_BASE_URL = os.environ.get("CCP_VERIFY_BASE_URL", "").rstrip("/")

STRIPE_SECRET_KEY = os.environ.get("CCP_STRIPE_SECRET_KEY", "").strip()
STRIPE_WEBHOOK_SECRET = os.environ.get("CCP_STRIPE_WEBHOOK_SECRET", "").strip()
STRIPE_SUCCESS_URL = os.environ.get("CCP_STRIPE_SUCCESS_URL", "").strip()
STRIPE_CANCEL_URL = os.environ.get("CCP_STRIPE_CANCEL_URL", "").strip()

if STRIPE_SECRET_KEY:
    stripe.api_key = STRIPE_SECRET_KEY

# Certificates folders
CERT_BASE_DIR = os.environ.get("CCP_CERT_BASE_DIR", os.path.join(DATA_DIR, "certificates", "base"))
CERT_BC_DIR   = os.environ.get("CCP_CERT_BLOCKCHAIN_DIR", os.path.join(DATA_DIR, "certificates", "blockchain"))
OTS_BIN = os.environ.get("CCP_OTS_BIN", "/home/debian/.local/bin/ots").strip() or "/home/debian/.local/bin/ots"
OTS_WORK_DIR = os.environ.get("CCP_OTS_WORK_DIR", os.path.join(DATA_DIR, "ots"))
ABO_CERT_FOOTER = 'ABO BANK Legal Office, Department of Intellectual Property and Copyright - Bank Association Offshore 1001 Third Avenue W, Suite 190, Bradenton, FL 34205 21. ABO BANK Harley Street, London, England, United Kingdom, W1G 9QD . Associazione Banche Offshore Corso Venezia 1 Milano Incorporata Codice Fiscale 92034960457'

# Stima costo EUR (solo reporting, non billing)
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)
os.makedirs(CERT_BASE_DIR, exist_ok=True)
os.makedirs(CERT_BC_DIR, exist_ok=True)
os.makedirs(OTS_WORK_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 _draw_pdf_footer(c, generated_at_text=""):
    import textwrap
    text = c.beginText(2*cm, 2.2*cm)
    text.setFont("Helvetica", 7)
    text.setLeading(8)
    for line in textwrap.wrap(ABO_CERT_FOOTER, width=115):
        text.textLine(line)
    c.drawText(text)
    c.setFont("Helvetica-Oblique", 9)
    c.drawString(2*cm, 1.2*cm, generated_at_text or f"Generated at (UTC): {_now_iso()}")

# ================== 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.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.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", "")
    # cookie jar può salvarlo 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}})

def get_pricing():
    ensure_pricing_file()
    data = _json_load(PRICING_FILE, {"default_op_cost_abo": 1.0, "operations": {"generic": 1.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_find_seen(uid: str, request_id: str):
    """
    Ritorna dict log se già visto (con eventuale certificate_path), altrimenti None.
    """
    request_id = (request_id or "").strip()
    if not request_id:
        return None
    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 row
                except Exception:
                    continue
    except Exception:
        return None
    return None

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})

def summarize_usage(days=30):
    since = time.time() - int(days) * 86400
    res = {"by_user": {}, "total_requests": 0, "total_ok": 0, "total_cost_eur": 0.0}
    if not os.path.exists(USAGE_LOG):
        return {**res, "days": int(days), "from": datetime.fromtimestamp(since, timezone.utc).isoformat(), "to": _now_iso()}

    with open(USAGE_LOG, "r", encoding="utf-8") as f:
        for line in f:
            try:
                row = json.loads(line)
                ts = datetime.fromisoformat(row["ts"]).timestamp()
                if ts < since:
                    continue
                uid = str(row.get("user_id", "unknown"))
                u = res["by_user"].setdefault(uid, {"requests": 0, "ok": 0, "cost_eur": 0.0, "providers": {}})
                u["requests"] += 1
                res["total_requests"] += 1
                if row.get("ok"):
                    u["ok"] += 1
                    res["total_ok"] += 1
                cost = float(row.get("cost_eur", 0.0) or 0.0)
                u["cost_eur"] += cost
                res["total_cost_eur"] += cost
                p = row.get("provider", "unknown")
                u["providers"][p] = u["providers"].get(p, 0) + 1
            except Exception:
                pass

    return {**res, "days": int(days), "from": datetime.fromtimestamp(since, timezone.utc).isoformat(), "to": _now_iso()}

# ================== PDF CERTIFICATE ==================
def _qr_to_temp_png(data: str, out_dir: str, filename: str) -> str:
    if not HAS_QRCODE:
        return ""
    try:
        img = qrcode.make(data)
        path = os.path.join(out_dir, filename)
        img.save(path)
        return path
    except Exception:
        return ""

def generate_base_certificate(payload: dict, uid: str, request_id: str) -> str:
    """
    Crea PDF base con campi:
    cert_id, work_id, deposit_date, author, original_filename, verify_url, file_hash
    """
    os.makedirs(CERT_BASE_DIR, exist_ok=True)

    cert_id = str(payload.get("cert_id", "")).strip()
    work_id = str(payload.get("work_id", "")).strip()
    deposit_date = str(payload.get("deposit_date", "")).strip()
    author = str(payload.get("author", "")).strip()
    original_filename = str(payload.get("original_filename", "")).strip()
    verify_url = str(payload.get("verify_url", "")).strip()
    file_hash = str(payload.get("file_hash", "")).strip()

    filename = f"cert_base_{uid}_{request_id}.pdf"
    fullpath = os.path.join(CERT_BASE_DIR, filename)

    # QR code temp
    qr_path = ""
    if verify_url:
        qr_path = _qr_to_temp_png(verify_url, CERT_BASE_DIR, f"qr_{uid}_{request_id}.png")

    c = canvas.Canvas(fullpath, pagesize=A4)
    w, h = A4

    # Header
    c.setFont("Helvetica-Bold", 20)
    c.drawString(2*cm, h - 2.5*cm, "COPYRIGHTCHAIN")
    c.setFont("Helvetica", 11)
    c.drawString(2*cm, h - 3.2*cm, "Certificato di Copyright - Deposito con Data Certa (BASE)")

    # Box dati
    y = h - 5.0*cm
    c.setFont("Helvetica-Bold", 12)
    c.drawString(2*cm, y, "Dati Certificato")
    y -= 0.8*cm

    c.setFont("Helvetica", 11)
    def line(label, value):
        nonlocal y
        if value:
            c.setFont("Helvetica-Bold", 10)
            c.drawString(2*cm, y, f"{label}:")
            c.setFont("Helvetica", 10)
            c.drawString(6.2*cm, y, value)
            y -= 0.6*cm

    line("Cert ID", cert_id or f"{uid}-{request_id}")
    line("Work ID", work_id)
    line("Deposit Date", deposit_date)
    line("Author / Owner", author or uid)
    line("Original filename", original_filename)
    line("File hash (SHA256)", file_hash)
    if verify_url:
        line("Verify URL", verify_url)

    # QR
    if qr_path and os.path.exists(qr_path):
        try:
            c.drawImage(qr_path, w - 6.5*cm, h - 7.5*cm, width=4.5*cm, height=4.5*cm, preserveAspectRatio=True, mask='auto')
            c.setFont("Helvetica", 8)
            c.drawRightString(w - 2*cm, h - 7.8*cm, "Scansiona per verificare")
        except Exception:
            pass

    # Footer
    c.setFont("Helvetica-Oblique", 9)
    c.drawString(2*cm, 2.0*cm, f"Generated at (UTC): {datetime.now(timezone.utc).isoformat()}")
    c.setFont("Helvetica", 8)
    c.drawString(2*cm, 1.4*cm, "Questo certificato attesta il deposito del contenuto e l'associazione univoca ai dati sopra indicati.")

    c.showPage()
    c.save()

    # cleanup QR temp (non obbligatorio)
    try:
        if qr_path and os.path.exists(qr_path):
            os.remove(qr_path)
    except Exception:
        pass

    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["id"]), user, None
    uid = payload.get("user_id")
    if uid:
        return str(uid), None, None
    return None, None, (jsonify({"error": "unauthorized"}), 401)

def _build_public_cert_url(fullpath: str) -> str:
    if not PUBLIC_BASE_URL:
        return ""
    # esponiamo via endpoint /cert/file?path=... (più sotto)
    return f"{PUBLIC_BASE_URL}/cert/file?path={fullpath}"

def _build_verify_url(cert_id: str, file_hash: str = "") -> str:
    if not VERIFY_BASE_URL:
        return ""
    cert_id = str(cert_id or "").strip()
    file_hash = str(file_hash or "").strip()
    if cert_id and file_hash:
        return f"{VERIFY_BASE_URL}/?id={cert_id}&hash={file_hash}"
    if cert_id:
        return f"{VERIFY_BASE_URL}/?id={cert_id}"
    if file_hash:
        return f"{VERIFY_BASE_URL}/?hash={file_hash}"
    return VERIFY_BASE_URL

def _cert_debit_core(payload):
    cert_type = str(payload.get("cert_type", "base")).strip().lower()
    request_id = str(payload.get("request_id", "")).strip()

    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 robusta: se già visto MA file non esiste → rigenera senza addebito
    seen = cert_find_seen(uid, request_id)
    if seen:
        seen_path = str(seen.get("certificate_path", "") or "")
        expected = seen_path
        if not expected:
            # fallback: ricostruiamo path standard
            base_dir = CERT_BC_DIR if cert_type == "blockchain" else CERT_BASE_DIR
            expected = os.path.join(base_dir, f"cert_{cert_type}_{uid}_{request_id}.pdf") if cert_type == "blockchain" else os.path.join(base_dir, f"cert_base_{uid}_{request_id}.pdf")

        if expected and os.path.exists(expected):
            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),
                "certificate_path": expected,
                "certificate_url": _build_verify_url(seen.get("cert_id", ""), seen.get("file_hash", ""))
            })
        # se file manca: rigenera sotto senza addebito

    # 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

    cert_id = f"CD-{abs(hash(f'{uid}-{request_id}')) % 1000000:06d}"
    file_hash = str(payload.get("file_hash", "")).strip()
    verify_url = _build_verify_url(cert_id, file_hash)
    payload = dict(payload or {})
    payload["verify_url"] = verify_url
    payload["cert_id"] = cert_id

    ots_meta = {
        "status": "pending",
        "ots_path": "",
        "ots_manifest_path": "",
        "btc_txid": "",
        "btc_block_height": None,
        "ots_last_upgrade_at": "",
        "anchored_at": "",
        "confirmed_at": "",
    }

    # genera PDF
    try:
        if cert_type == "base":
            certificate_path = generate_base_certificate(payload, uid, request_id)
        else:
            ots_meta = _ots_stamp_for_payload(payload, uid, request_id, cert_id, verify_url)

            os.makedirs(CERT_BC_DIR, exist_ok=True)
            certificate_path = os.path.join(CERT_BC_DIR, f"cert_blockchain_{uid}_{request_id}.pdf")
            c = canvas.Canvas(certificate_path, pagesize=A4)
            w, h = A4
            c.setFont("Helvetica-Bold", 18)
            c.drawString(2*cm, h - 3*cm, "CERTIFICATO BLOCKCHAIN (OpenTimestamps)")
            c.setFont("Helvetica", 11)
            c.drawString(2*cm, h - 4.2*cm, f"User ID: {uid}")
            c.drawString(2*cm, h - 5.0*cm, f"Request ID: {request_id}")
            c.drawString(2*cm, h - 5.8*cm, f"Cert ID: {cert_id}")
            c.drawString(2*cm, h - 6.6*cm, f"File Hash: {file_hash}")
            c.drawString(2*cm, h - 7.4*cm, f"OTS Status: {ots_meta.get('status','pending')}")
            if ots_meta.get("btc_txid"):
                c.drawString(2*cm, h - 8.2*cm, f"Bitcoin TxID: {ots_meta.get('btc_txid','')}")
            if ots_meta.get("btc_block_height") is not None:
                c.drawString(2*cm, h - 9.0*cm, f"Block Height: {ots_meta.get('btc_block_height')}")
            c.setFont("Helvetica-Oblique", 9)
            c.drawString(2*cm, 2.8*cm, f"OTS Proof: {ots_meta.get('ots_path','')}")
            c.drawString(2*cm, 2.2*cm, f"Verify URL: {verify_url}")
            c.drawString(2*cm, 1.6*cm, f"Generated at (UTC): {_now_iso()}")
            c.showPage()
            c.save()
    except Exception as e:
        # se fallisce la generazione: rimborsa ABO
        add_balance(uid, cost_abo)
        log_ai_usage(
            user_id=uid, provider="gateway", ok=False, status=500, ms=0, cost_eur=0.0,
            op=op, abo_cost=cost_abo, cert_type=cert_type, request_id=request_id,
            error="certificate_generation_failed"
        )
        return jsonify({"error": "certificate_generation_failed", "details": str(e)}), 500

    # log certificato (idempotenza) SOLO dopo successo generazione

    cert_log_request(
        user_id=uid,
        request_id=request_id,
        op=op,
        cert_type=cert_type,
        abo_cost=cost_abo,
        certificate_path=certificate_path,
        cert_id=cert_id,
        file_hash=file_hash,
        verify_url=verify_url,
        original_filename=str(payload.get("original_filename", "")).strip(),
        author=str(payload.get("author", "")).strip(),
        deposit_date=str(payload.get("deposit_date", "")).strip() or _now_iso(),
        blockchain_status=ots_meta.get("status", "pending") if cert_type == "blockchain" else "",
        ots_path=ots_meta.get("ots_path", ""),
        ots_manifest_path=ots_meta.get("ots_manifest_path", ""),
        ots_last_upgrade_at=ots_meta.get("ots_last_upgrade_at", ""),
        btc_txid=ots_meta.get("btc_txid", ""),
        btc_block_height=ots_meta.get("btc_block_height"),
        anchored_at=ots_meta.get("anchored_at", ""),
        confirmed_at=ots_meta.get("confirmed_at", "")
    )

    # log generale
    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,
        "certificate_url": verify_url,
        "verify_url": verify_url,
        "cert_id": cert_id,
        "file_hash": file_hash,
        "blockchain_status": ots_meta.get("status", "") if cert_type == "blockchain" else "",
        "ots": ots_meta if cert_type == "blockchain" else None
    })



def _safe_fs(value: str) -> str:
    return re.sub(r"[^A-Za-z0-9._-]+", "_", str(value or "").strip())[:180]

def _run_ots(*args, timeout=180):
    env = dict(os.environ)
    env["PATH"] = f"{os.path.dirname(OTS_BIN)}:{env.get('PATH','')}"
    proc = subprocess.run(
        [OTS_BIN, *list(args)],
        capture_output=True,
        text=True,
        timeout=timeout,
        env=env,
    )
    out = ((proc.stdout or "") + "\n" + (proc.stderr or "")).strip()
    return proc.returncode, out

def _parse_ots_status(text: str):
    text = text or ""

    txids = re.findall(r"(?:Transaction id|Timestamped by transaction)\s+([0-9a-fA-F]{64})", text)
    heights = [int(x) for x in re.findall(r"BitcoinBlockHeaderAttestation\((\d+)\)", text)]

    if not heights:
        m = re.search(r"block(?:\s+height)?\s+(\d+)", text, re.I)
        if m:
            try:
                heights = [int(m.group(1))]
            except Exception:
                heights = []

    status = "pending"
    if heights or re.search(r"Success! Timestamp complete|attests data existed as of", text, re.I):
        status = "confirmed"
    elif txids or re.search(r"waiting for \d+ confirmations", text, re.I):
        status = "anchored"
    elif re.search(r"PendingAttestation|Pending confirmation in Bitcoin blockchain", text, re.I):
        status = "pending"

    return {
        "status": status,
        "btc_txid": txids[-1] if txids else "",
        "btc_txids": txids,
        "btc_block_height": max(heights) if heights else None,
        "btc_block_heights": heights,
        "ots_raw": text[-12000:],
    }

def _build_ots_manifest(payload: dict, uid: str, request_id: str, cert_id: str, verify_url: str):
    file_hash = str(payload.get("file_hash", "")).strip()
    if not file_hash:
        raise ValueError("missing_file_hash")

    return {
        "schema": "copyrightchain-ots-v1",
        "cert_id": str(cert_id),
        "user_id": str(uid),
        "request_id": str(request_id),
        "file_hash": file_hash,
        "hash_algorithm": "sha256",
        "original_filename": str(payload.get("original_filename", "")).strip(),
        "author": str(payload.get("author", "")).strip(),
        "deposit_date": str(payload.get("deposit_date", "")).strip() or _now_iso(),
        "verify_url": str(verify_url or "").strip(),
        "generated_at": _now_iso(),
    }

def _rewrite_cert_usage_row(user_id: str, request_id: str, updates: dict):
    if not os.path.exists(CERT_USAGE_LOG):
        return False

    changed = False
    rows = []
    with open(CERT_USAGE_LOG, "r", encoding="utf-8") as f:
        for line in f:
            try:
                row = json.loads(line)
            except Exception:
                continue
            if str(row.get("user_id")) == str(user_id) and str(row.get("request_id")) == str(request_id):
                row.update(updates or {})
                changed = True
            rows.append(row)

    if changed:
        tmp = CERT_USAGE_LOG + ".tmp"
        with open(tmp, "w", encoding="utf-8") as f:
            for row in rows:
                f.write(json.dumps(row, ensure_ascii=False) + "\n")
        os.replace(tmp, CERT_USAGE_LOG)

    return changed

def _ots_refresh_files(manifest_path: str, ots_path: str):
    outputs = []

    for cmd, timeout in (("upgrade", 90), ("verify", 90), ("info", 30)):
        try:
            rc, out = _run_ots(cmd, ots_path, timeout=timeout)
            if out:
                outputs.append(f"$ ots {cmd}\n{out}")
        except Exception as e:
            outputs.append(f"$ ots {cmd}\n{e}")

    combined = "\n\n".join(outputs).strip()
    meta = _parse_ots_status(combined)
    meta["ots_last_upgrade_at"] = _now_iso()
    return meta

def _ots_stamp_for_payload(payload: dict, uid: str, request_id: str, cert_id: str, verify_url: str):
    manifest = _build_ots_manifest(payload, uid, request_id, cert_id, verify_url)

    base = f"{_safe_fs(uid)}_{_safe_fs(request_id)}"
    manifest_path = os.path.join(OTS_WORK_DIR, f"manifest_{base}.json")
    ots_path = manifest_path + ".ots"

    with open(manifest_path, "w", encoding="utf-8") as f:
        json.dump(manifest, f, ensure_ascii=False, sort_keys=True, separators=(",", ":"))

    if os.path.exists(ots_path):
        os.remove(ots_path)

    rc, out = _run_ots("stamp", manifest_path, timeout=60)
    if rc != 0 or not os.path.exists(ots_path):
        raise RuntimeError(f"ots_stamp_failed: {out}")

    meta = {
        "status": "pending",
        "ots_path": ots_path,
        "ots_manifest_path": manifest_path,
        "ots_stamped_at": _now_iso(),
        "ots_last_upgrade_at": "",
        "btc_txid": "",
        "btc_block_height": None,
        "anchored_at": "",
        "confirmed_at": "",
    }

    refreshed = _ots_refresh_files(manifest_path, ots_path)
    meta.update({
        "status": refreshed.get("status", "pending"),
        "ots_last_upgrade_at": refreshed.get("ots_last_upgrade_at", ""),
        "btc_txid": refreshed.get("btc_txid", ""),
        "btc_block_height": refreshed.get("btc_block_height"),
    })

    if meta["status"] in ("anchored", "confirmed"):
        meta["anchored_at"] = _now_iso()
    if meta["status"] == "confirmed":
        meta["confirmed_at"] = _now_iso()

    return meta

def _ots_refresh_row(row: dict):
    row = dict(row or {})
    if str(row.get("cert_type", "base")).strip() != "blockchain":
        return row

    manifest_path = str(row.get("ots_manifest_path", "") or "").strip()
    ots_path = str(row.get("ots_path", "") or "").strip()

    if not manifest_path or not ots_path or not os.path.exists(ots_path):
        row["blockchain_status"] = str(row.get("blockchain_status", "") or "").strip() or "pending"
        return row

    refreshed = _ots_refresh_files(manifest_path, ots_path)
    row["blockchain_status"] = refreshed.get("status", row.get("blockchain_status", "pending"))
    row["ots_last_upgrade_at"] = refreshed.get("ots_last_upgrade_at", "")
    row["btc_txid"] = refreshed.get("btc_txid") or row.get("btc_txid", "")
    row["btc_block_height"] = refreshed.get("btc_block_height") if refreshed.get("btc_block_height") is not None else row.get("btc_block_height")

    if row["blockchain_status"] in ("anchored", "confirmed") and not row.get("anchored_at"):
        row["anchored_at"] = _now_iso()
    if row["blockchain_status"] == "confirmed" and not row.get("confirmed_at"):
        row["confirmed_at"] = _now_iso()

    _rewrite_cert_usage_row(
        str(row.get("user_id", "") or "").strip(),
        str(row.get("request_id", "") or "").strip(),
        {
            "blockchain_status": row.get("blockchain_status", ""),
            "ots_last_upgrade_at": row.get("ots_last_upgrade_at", ""),
            "btc_txid": row.get("btc_txid", ""),
            "btc_block_height": row.get("btc_block_height"),
            "anchored_at": row.get("anchored_at", ""),
            "confirmed_at": row.get("confirmed_at", ""),
        }
    )
    return row

# ================== ROUTES ==================
@APP.get("/health")
def health():
    return jsonify({
        "ok": True,
        "has_openai_key": bool(OPENAI_API_KEY),
        "has_gemini_key": bool(GEMINI_API_KEY),
        "has_qrcode": HAS_QRCODE,
        "tower_configured": bool(TOWER_PASSWORD and SESSION_SECRET)
    })

@APP.get("/openapi.json")
def openapi_json():
    return jsonify({
        "openapi": "3.0.0",
        "info": {"title": "CCP AI Gateway", "version": "1.0.0"},
        "paths": {
            "/health": {"get": {"summary": "Health"}},
            "/openapi.json": {"get": {"summary": "OpenAPI spec"}},
            "/docs": {"get": {"summary": "HTML docs"}},
            "/tower/login": {"post": {"summary": "Tower login"}},
            "/tower/users/upsert": {"post": {"summary": "Upsert user"}},
            "/tower/users/list": {"get": {"summary": "List users"}},
            "/tower/ai/stats": {"get": {"summary": "AI stats"}},
            "/tower/balances": {"get": {"summary": "Balances dump"}},
            "/tower/balances/topup": {"post": {"summary": "Topup"}},
            "/tower/balances/set": {"post": {"summary": "Set balance"}},
            "/tower/pricing": {"get": {"summary": "Get pricing"}},
            "/tower/pricing/set": {"post": {"summary": "Set pricing"}},
            "/me/balance": {"get": {"summary": "My balance"}},
            "/certify": {"post": {"summary": "Create & debit certificate (base/blockchain)"}},
            "/cert/debit": {"post": {"summary": "Debit certificate (compat)"}},
            "/cert/file": {"get": {"summary": "Download certificate by server path (needs tower or admin token)"}},
            "/text": {"post": {"summary": "AI text"}}
        }
    })

@APP.get("/docs")
def docs():
    html = """<!doctype html><html><head><meta charset="utf-8"><title>CCP AI Gateway Docs</title></head>
<body style="font-family:Arial,sans-serif;padding:20px">
<h2>CCP AI Gateway</h2>
<ul>
<li><a href="/health">/health</a></li>
<li><a href="/openapi.json">/openapi.json</a></li>
</ul>
<p>Endpoints principali: /tower/*, /certify, /text</p>
</body></html>"""
    return make_response(html, 200, {"Content-Type": "text/html; charset=utf-8"})

@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}))
    # su localhost http: non mettere Secure, altrimenti curl cookie-jar ok ma browser no.
    resp.set_cookie("ccp_tower", tok, httponly=True, secure=False, samesite="Lax", path="/")
    return resp

@APP.post("/tower/users/upsert")
def tower_users_upsert():
    g = _tower_guard()
    if g:
        return g
    data = request.get_json(silent=True) or {}
    if not data.get("id"):
        return jsonify({"error": "missing_id"}), 400
    user = {
        "id": str(data["id"]),
        "token": data.get("token", ""),
        "email": data.get("email", ""),
        "name": data.get("name", ""),
        "wallet": data.get("wallet", ""),
        "plan": data.get("plan", "free"),
        "ai_limit_monthly": int(data.get("ai_limit_monthly", 0) or 0),
        "updated_at": _now_iso()
    }
    upsert_user(user)
    return jsonify({"ok": True})


@APP.post("/tower/users/create")
def tower_users_create():
    g = _tower_guard()
    if g:
        return g

    data = request.get_json(silent=True) or {}
    uid = str(data.get("id", "")).strip()
    email = str(data.get("email", "")).strip().lower()
    name = str(data.get("name", "")).strip()
    wallet = str(data.get("wallet", "")).strip()
    plan = str(data.get("plan", "free") or "free").strip().lower()
    token = str(data.get("token", "")).strip()
    temp_password = str(data.get("temp_password", "")).strip()
    initial_balance = data.get("initial_balance", 0)

    if not uid:
        return jsonify({"error": "missing_id"}), 400
    if not email:
        return jsonify({"error": "missing_email"}), 400
    if not temp_password or len(temp_password) < 8:
        return jsonify({"error": "weak_temp_password"}), 400

    existing_by_email = find_user_by_email(email)
    if existing_by_email and str(existing_by_email.get("id", "")) != uid:
        return jsonify({"error": "email_exists"}), 409

    ensure_users_file()
    current = _json_load(USERS_FILE, {"users": []})
    for u in current.get("users", []):
        if str(u.get("id", "")).strip() == uid:
            return jsonify({"error": "id_exists"}), 409

    now = _now_iso()
    user = {
        "id": uid,
        "token": token or secrets.token_hex(24),
        "email": email,
        "name": name,
        "wallet": wallet,
        "plan": plan or "free",
        "ai_limit_monthly": int(data.get("ai_limit_monthly", 0) or 0),
        "password_hash": _pw_hash(temp_password),
        "must_change_password": True,
        "created_at": now,
        "updated_at": now
    }
    upsert_user(user)

    try:
        initial_balance = float(initial_balance or 0)
    except Exception:
        initial_balance = 0.0
    set_balance(uid, initial_balance)

    return jsonify({
        "ok": True,
        "user": _user_public(user),
        "temp_password_set": True,
        "initial_balance": initial_balance
    })



@APP.post("/tower/users/reset_password")
def tower_users_reset_password():
    g = _tower_guard()
    if g:
        return g

    data = request.get_json(silent=True) or {}
    uid = str(data.get("user_id", "")).strip()
    temp_password = str(data.get("temp_password", "")).strip()

    if not uid:
        return jsonify({"error": "missing_user_id"}), 400

    ensure_users_file()
    users_data = _json_load(USERS_FILE, {"users": []})
    users = users_data.get("users", [])

    target = None
    for u in users:
        if str(u.get("id", "")).strip() == uid:
            target = u
            break

    if not target:
        return jsonify({"error": "user_not_found"}), 404

    if not temp_password or len(temp_password) < 8:
        temp_password = "Temp" + secrets.token_hex(4) + "!"

    target["password_hash"] = _pw_hash(temp_password)
    target["must_change_password"] = True
    target["updated_at"] = _now_iso()
    upsert_user(target)

    return jsonify({
        "ok": True,
        "user_id": uid,
        "temp_password": temp_password,
        "must_change_password": True
    })

@APP.post("/tower/users/delete")
def tower_users_delete():
    g = _tower_guard()
    if g:
        return g

    data = request.get_json(silent=True) or {}
    uid = str(data.get("user_id", "")).strip()
    if not uid:
        return jsonify({"error": "missing_user_id"}), 400

    ensure_users_file()
    users_data = _json_load(USERS_FILE, {"users": []})
    users = users_data.get("users", [])

    target = None
    kept = []
    for u in users:
        if str(u.get("id", "")).strip() == uid:
            target = u
        else:
            kept.append(u)

    if not target:
        return jsonify({"error": "user_not_found"}), 404

    if str(target.get("plan", "")).strip().lower() == "admin":
        return jsonify({"error": "protected_admin_user"}), 403

    users_data["users"] = kept
    _json_save(USERS_FILE, users_data)

    ensure_balances_file()
    bal = _json_load(BALANCES_FILE, {"balances": {}})
    bal.setdefault("balances", {}).pop(uid, None)
    _json_save(BALANCES_FILE, bal)

    return jsonify({
        "ok": True,
        "deleted_user_id": uid,
        "deleted_email": target.get("email", ""),
        "deleted_name": target.get("name", "")
    })

@APP.get("/tower/users/list")
def tower_users_list():
    g = _tower_guard()
    if g:
        return g
    ensure_users_file()
    return jsonify(_json_load(USERS_FILE, {"users": []}))

@APP.get("/tower/ai/stats")
def tower_ai_stats():
    g = _tower_guard()
    if g:
        return g
    days = int(request.args.get("days", "30") or "30")
    days = max(1, min(days, 365))
    return jsonify(summarize_usage(days))


@APP.get("/tower/users/spend_summary")
def tower_users_spend_summary():
    g = _tower_guard()
    if g:
        return g

    out = {}

    def ensure_uid(uid):
        uid = str(uid or "").strip()
        if not uid:
            return None
        if uid not in out:
            out[uid] = {
                "user_id": uid,
                "abo_spent_total": 0.0,
                "ai_abo_spent": 0.0,
                "cert_abo_spent": 0.0,
                "ai_calls": 0,
                "cert_ops": 0,
                "total_ops": 0,
                "last_ts": ""
            }
        return out[uid]

    for path, bucket in [(USAGE_LOG, "ai"), (CERT_USAGE_LOG, "cert")]:
        if not os.path.exists(path):
            continue
        try:
            with open(path, "r", encoding="utf-8") as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        row = json.loads(line)
                    except Exception:
                        continue

                    uid = str(row.get("user_id", "")).strip()
                    item = ensure_uid(uid)
                    if not item:
                        continue

                    abo = row.get("abo_cost", 0) or 0
                    try:
                        abo = float(abo)
                    except Exception:
                        abo = 0.0

                    item["abo_spent_total"] += abo
                    item["total_ops"] += 1

                    ts = str(row.get("ts", "") or "")
                    if ts and (not item["last_ts"] or ts > item["last_ts"]):
                        item["last_ts"] = ts

                    if bucket == "ai":
                        item["ai_abo_spent"] += abo
                        item["ai_calls"] += 1
                    else:
                        item["cert_abo_spent"] += abo
                        item["cert_ops"] += 1
        except Exception:
            continue

    return jsonify({"users": list(out.values())})

@APP.get("/tower/balances")
def tower_balances():
    g = _tower_guard()
    if g:
        return g
    ensure_balances_file()
    return jsonify(_json_load(BALANCES_FILE, {"balances": {}}))

@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 {}
    default_cost = data.get("default_op_cost_abo", None)
    operations = data.get("operations", None)

    cur = get_pricing()
    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})


# === CLIENT AUTH START ===
def find_user_by_email(email):
    ensure_users_file()
    email = str(email or "").strip().lower()
    data = _json_load(USERS_FILE, {"users": []})
    for u in data.get("users", []):
        if str(u.get("email", "")).strip().lower() == email:
            return u
    return None

def _user_public(u):
    return {
        "id": str(u.get("id", "")),
        "email": u.get("email", ""),
        "name": u.get("name", ""),
        "wallet": u.get("wallet", ""),
        "plan": u.get("plan", "free"),
        "ai_limit_monthly": int(u.get("ai_limit_monthly", 0) or 0),
        "updated_at": u.get("updated_at", ""),
        "created_at": u.get("created_at", ""),
        "must_change_password": bool(u.get("must_change_password", False))
    }

def _pw_hash(raw):
    return hashlib.sha256(str(raw or "").encode("utf-8")).hexdigest()

def _make_user_id():
    return "USR-" + secrets.token_hex(4).upper()



def _client_cert_title(row: dict) -> str:
    title = str(row.get("original_filename", "") or "").strip()
    low = title.lower()
    for ext in (".png", ".jpg", ".jpeg", ".pdf", ".txt", ".doc", ".docx"):
        if low.endswith(ext):
            return title[:-len(ext)]
    return title

def _build_client_certificate_row(row: dict):
    cert_type = str(row.get("cert_type", "base") or "base").strip()
    if cert_type == "blockchain":
        row = _ots_refresh_row(row)

    certificate_path = str(row.get("certificate_path", "") or "").strip()
    cert_id = str(row.get("cert_id", "") or "").strip()
    verify_url = str(row.get("verify_url", "") or "").strip()
    blockchain_status = str(row.get("blockchain_status", "") or "").strip()
    if cert_type == "blockchain" and not blockchain_status:
        blockchain_status = "pending"

    return {
        "cert_id": cert_id,
        "request_id": str(row.get("request_id", "") or "").strip(),
        "cert_type": cert_type,
        "title": _client_cert_title(row),
        "original_filename": str(row.get("original_filename", "") or "").strip(),
        "author": str(row.get("author", "") or "").strip(),
        "deposit_date": str(row.get("deposit_date", "") or "").strip(),
        "file_hash": str(row.get("file_hash", "") or "").strip(),
        "verify_url": verify_url,
        "certificate_path": certificate_path,
        "certificate_url": _build_public_cert_url(certificate_path) if certificate_path else "",
        "blockchain_status": blockchain_status,
        "btc_txid": str(row.get("btc_txid", "") or "").strip(),
        "btc_block_height": row.get("btc_block_height"),
        "ots_path": str(row.get("ots_path", "") or "").strip(),
        "ots_manifest_path": str(row.get("ots_manifest_path", "") or "").strip(),
        "ots_proof_url": _build_public_cert_url(str(row.get("ots_path", "") or "").strip()) if str(row.get("ots_path", "") or "").strip() else "",
        "ots_manifest_url": _build_public_cert_url(str(row.get("ots_manifest_path", "") or "").strip()) if str(row.get("ots_manifest_path", "") or "").strip() else "",
        "anchored_at": str(row.get("anchored_at", "") or "").strip(),
        "confirmed_at": str(row.get("confirmed_at", "") or "").strip(),
        "legal_attestation": f"Certificato emesso via CopyrightChain AI Gateway. ID: {cert_id}. Verifica: {verify_url}"
    }

@APP.get("/client/certificates")
def client_certificates():
    tok = request.headers.get("X-CCP-Token", "")
    user = find_user_by_token(tok)
    if not user:
        return jsonify({"error": "unauthorized"}), 401

    uid = str(user.get("id", "") or "").strip()
    items = []

    try:
        if os.path.exists(CERT_USAGE_LOG):
            with open(CERT_USAGE_LOG, "r", encoding="utf-8") as f:
                for line in f:
                    try:
                        row = json.loads(line)
                    except Exception:
                        continue
                    if str(row.get("user_id", "") or "").strip() != uid:
                        continue
                    items.append(_build_client_certificate_row(row))
    except Exception as e:
        return jsonify({"error": "client_certificates_failed", "details": str(e)}), 500

    items.sort(key=lambda x: str(x.get("deposit_date", "") or ""), reverse=True)
    return jsonify({"ok": True, "certificates": items})




@APP.get("/tower/certificates")
def tower_certificates():
    g = _tower_guard()
    if g:
        return g

    q = str(request.args.get("q", "") or "").strip().lower()
    status_filter = str(request.args.get("status", "") or "").strip().lower()
    type_filter = str(request.args.get("type", "") or "").strip().lower()

    users_by_id = {}
    try:
        ensure_users_file()
        users_data = _json_load(USERS_FILE, {"users": []})
        for u in users_data.get("users", []):
            uid = str(u.get("id", "") or "").strip()
            if uid:
                users_by_id[uid] = u
    except Exception:
        users_by_id = {}

    items = []

    try:
        if os.path.exists(CERT_USAGE_LOG):
            with open(CERT_USAGE_LOG, "r", encoding="utf-8") as f:
                for line in f:
                    try:
                        row = json.loads(line)
                    except Exception:
                        continue

                    cert = _build_client_certificate_row(row)

                    uid = str(row.get("user_id", "") or "").strip()
                    user = users_by_id.get(uid, {})

                    cert["user_id"] = uid
                    cert["user_email"] = str(user.get("email", "") or "").strip()
                    cert["user_name"] = str(user.get("name", "") or "").strip()

                    searchable = " ".join([
                        str(cert.get("cert_id", "") or ""),
                        str(cert.get("request_id", "") or ""),
                        str(cert.get("title", "") or ""),
                        str(cert.get("original_filename", "") or ""),
                        str(cert.get("file_hash", "") or ""),
                        str(cert.get("btc_txid", "") or ""),
                        str(cert.get("user_id", "") or ""),
                        str(cert.get("user_email", "") or ""),
                        str(cert.get("user_name", "") or "")
                    ]).lower()

                    if q and q not in searchable:
                        continue

                    if status_filter and str(cert.get("blockchain_status", "") or "").strip().lower() != status_filter:
                        continue

                    if type_filter and str(cert.get("cert_type", "") or "").strip().lower() != type_filter:
                        continue

                    items.append(cert)
    except Exception as e:
        return jsonify({"error": "tower_certificates_failed", "details": str(e)}), 500

    items.sort(key=lambda x: str(x.get("deposit_date", "") or ""), reverse=True)
    return jsonify({"ok": True, "count": len(items), "certificates": items})


@APP.post("/client/register")
def client_register():
    data = request.get_json(silent=True) or {}
    name = str(data.get("name", "")).strip()
    email = str(data.get("email", "")).strip().lower()
    password = str(data.get("password", "")).strip()
    wallet = str(data.get("wallet", "")).strip()

    if not name or not email or not password:
        return jsonify({"error": "missing_fields"}), 400
    if "@" not in email:
        return jsonify({"error": "invalid_email"}), 400
    if len(password) < 8:
        return jsonify({"error": "weak_password"}), 400
    if find_user_by_email(email):
        return jsonify({"error": "email_exists"}), 409

    uid = _make_user_id()
    token = secrets.token_hex(24)
    now = _now_iso()
    user = {
        "id": uid,
        "token": token,
        "email": email,
        "name": name,
        "wallet": wallet,
        "plan": "free",
        "ai_limit_monthly": 0,
        "password_hash": _pw_hash(password),
        "created_at": now,
        "updated_at": now
    }
    upsert_user(user)
    set_balance(uid, get_balance(uid))

    return jsonify({
        "ok": True,
        "token": token,
        "user": _user_public(user),
        "balance_abo": get_balance(uid),
        "pricing": get_pricing()
    })

@APP.post("/client/login")
def client_login():
    data = request.get_json(silent=True) or {}
    email = str(data.get("email", "")).strip().lower()
    password = str(data.get("password", "")).strip()

    if not email or not password:
        return jsonify({"error": "missing_fields"}), 400

    user = find_user_by_email(email)
    if not user:
        return jsonify({"error": "unauthorized"}), 401

    if str(user.get("password_hash", "")) != _pw_hash(password):
        return jsonify({"error": "unauthorized"}), 401

    uid = str(user.get("id", ""))
    return jsonify({
        "ok": True,
        "token": user.get("token", ""),
        "user": _user_public(user),
        "must_change_password": bool(user.get("must_change_password", False)),
        "balance_abo": get_balance(uid),
        "pricing": get_pricing()
    })


@APP.post("/client/logout")
def client_logout():
    tok = request.headers.get("X-CCP-Token", "")
    user = find_user_by_token(tok)
    if not user:
        return jsonify({"error": "unauthorized"}), 401

    user["token"] = secrets.token_hex(24)
    user["updated_at"] = _now_iso()
    upsert_user(user)

    return jsonify({"ok": True})


@APP.post("/client/change_password")
def client_change_password():
    tok = request.headers.get("X-CCP-Token", "")
    user = find_user_by_token(tok)
    if not user:
        return jsonify({"error": "unauthorized"}), 401

    data = request.get_json(silent=True) or {}
    current_password = str(data.get("current_password", "")).strip()
    new_password = str(data.get("new_password", "")).strip()
    force_change = bool(user.get("must_change_password", False))

    if not new_password:
        return jsonify({"error": "missing_fields"}), 400
    if len(new_password) < 8:
        return jsonify({"error": "weak_password"}), 400

    if not force_change:
        if not current_password:
            return jsonify({"error": "missing_fields"}), 400
        if str(user.get("password_hash", "")) != _pw_hash(current_password):
            return jsonify({"error": "wrong_current_password"}), 401

    user["password_hash"] = _pw_hash(new_password)
    user["must_change_password"] = False
    user["updated_at"] = _now_iso()
    upsert_user(user)

    return jsonify({
        "ok": True,
        "user": _user_public(user)
    })

@APP.get("/client/me")
def client_me():
    tok = request.headers.get("X-CCP-Token", "")
    user = find_user_by_token(tok)
    if not user:
        return jsonify({"error": "unauthorized"}), 401

    uid = str(user.get("id", ""))
    return jsonify({
        "ok": True,
        "user": _user_public(user),
        "balance_abo": get_balance(uid),
        "pricing": get_pricing()
    })

# === CLIENT AUTH END ===

@APP.get("/me/balance")
def me_balance():
    tok = request.headers.get("X-CCP-Token", "")
    user = find_user_by_token(tok)
    if not user:
        return jsonify({"error": "unauthorized"}), 401
    uid = str(user.get("id"))
    return jsonify({"ok": True, "user_id": uid, "balance_abo": get_balance(uid), "pricing": get_pricing()})

@APP.get("/verify/public")
def verify_public():
    code = (request.args.get("code", "") or "").strip()
    cert_id = (request.args.get("id", "") or "").strip()
    file_hash = (request.args.get("hash", "") or "").strip()
    txid = (request.args.get("txid", "") or "").strip()

    q = code or cert_id or file_hash or txid
    if not q:
        return jsonify({"ok": False, "error": "missing_query"}), 400

    found = None
    try:
        if os.path.exists(CERT_USAGE_LOG):
            with open(CERT_USAGE_LOG, "r", encoding="utf-8") as f:
                for line in f:
                    try:
                        row = json.loads(line)
                    except Exception:
                        continue

                    row_cert_id = str(row.get("cert_id", "") or "").strip()
                    row_hash = str(row.get("file_hash", "") or "").strip()
                    row_req = str(row.get("request_id", "") or "").strip()
                    row_txid = str(row.get("btc_txid", "") or "").strip()

                    if q and q in (row_cert_id, row_hash, row_req, row_txid):
                        found = row
                        break
                    if cert_id and cert_id == row_cert_id:
                        found = row
                        break
                    if file_hash and file_hash == row_hash:
                        found = row
                        break
                    if txid and txid == row_txid:
                        found = row
                        break
    except Exception as e:
        return jsonify({"ok": False, "error": "verify_scan_failed", "details": str(e)}), 500

    if not found:
        return jsonify({"ok": False, "error": "not_found"}), 404

    certificate_path = str(found.get("certificate_path", "") or "").strip()
    cert_type = str(found.get("cert_type", "base") or "base").strip()

    if cert_type == "blockchain":
        found = _ots_refresh_row(found)

    blockchain_status = str(found.get("blockchain_status", "") or "").strip()
    if cert_type == "blockchain" and not blockchain_status:
        blockchain_status = "pending"

    title = str(found.get("original_filename", "") or "").strip()
    if title.lower().endswith(".png") or title.lower().endswith(".jpg") or title.lower().endswith(".jpeg") or title.lower().endswith(".pdf"):
        title = title.rsplit(".", 1)[0]

    return jsonify({
        "ok": True,
        "verified": True,
        "query": q,
        "cert_id": str(found.get("cert_id", "") or "").strip(),
        "file_hash": str(found.get("file_hash", "") or "").strip(),
        "request_id": str(found.get("request_id", "") or "").strip(),
        "user_id": str(found.get("user_id", "") or "").strip(),
        "title": title,
        "author": str(found.get("author", "") or "").strip(),
        "deposit_date": str(found.get("deposit_date", "") or "").strip(),
        "cert_type": cert_type,
        "blockchain_status": blockchain_status,
        "btc_txid": str(found.get("btc_txid", "") or "").strip(),
        "btc_block_height": found.get("btc_block_height"),
        "ots_path": str(found.get("ots_path", "") or "").strip(),
        "ots_manifest_path": str(found.get("ots_manifest_path", "") or "").strip(),
        "ots_last_upgrade_at": str(found.get("ots_last_upgrade_at", "") or "").strip(),
        "anchored_at": str(found.get("anchored_at", "") or "").strip(),
        "confirmed_at": str(found.get("confirmed_at", "") or "").strip(),
        "certificate_path": certificate_path,
        "certificate_url": _build_public_cert_url(certificate_path) if certificate_path else "",
        "ots_proof_url": _build_public_cert_url(str(found.get("ots_path", "") or "").strip()) if str(found.get("ots_path", "") or "").strip() else "",
        "ots_manifest_url": _build_public_cert_url(str(found.get("ots_manifest_path", "") or "").strip()) if str(found.get("ots_manifest_path", "") or "").strip() else "",
        "verify_url": str(found.get("verify_url", "") or "").strip()
    })

@APP.get("/cert/file")
def cert_file():
    """
    Download file certificato/proof/manifest dal server path.
    Accesso consentito a:
    - tower session
    - admin token
    - client autenticato proprietario del file richiesto
    """
    path = (request.args.get("path", "") or "").strip()
    if not path:
        return jsonify({"error": "missing_path"}), 400

    real = os.path.realpath(path)

    allowed_roots = [
        os.path.realpath(CERT_BASE_DIR),
        os.path.realpath(CERT_BC_DIR),
        os.path.realpath(OTS_WORK_DIR),
    ]

    if not any(real == root or real.startswith(root + os.sep) for root in allowed_roots):
        return jsonify({"error": "forbidden_path"}), 403

    if not os.path.exists(real) or not os.path.isfile(real):
        return jsonify({"error": "file_not_found"}), 404

    allowed = False
    tok = request.headers.get("X-CCP-Token", "")

    if tower_session():
        allowed = True
    elif ADMIN_TOKEN and tok == ADMIN_TOKEN:
        allowed = True
    else:
        user = find_user_by_token(tok)
        if user:
            uid = str(user.get("id", "") or "").strip()
            try:
                if os.path.exists(CERT_USAGE_LOG):
                    with open(CERT_USAGE_LOG, "r", encoding="utf-8") as f:
                        for line in f:
                            try:
                                row = json.loads(line)
                            except Exception:
                                continue

                            if str(row.get("user_id", "") or "").strip() != uid:
                                continue

                            row_paths = [
                                str(row.get("certificate_path", "") or "").strip(),
                                str(row.get("ots_path", "") or "").strip(),
                                str(row.get("ots_manifest_path", "") or "").strip(),
                            ]

                            row_paths = [os.path.realpath(x) for x in row_paths if x]
                            if real in row_paths:
                                allowed = True
                                break
            except Exception as e:
                return jsonify({"error": "auth_scan_failed", "details": str(e)}), 500

    if not allowed:
        return jsonify({"error": "unauthorized"}), 401

    return send_file(real, as_attachment=True, download_name=os.path.basename(real))

@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 {}
    return _cert_debit_core(payload)

@APP.post("/billing/stripe/checkout")
def billing_stripe_checkout():
    if not STRIPE_SECRET_KEY:
        return jsonify({"error": "stripe_not_configured"}), 500

    tok = request.headers.get("X-CCP-Token", "")
    user = find_user_by_token(tok)
    if not user:
        return jsonify({"error": "unauthorized"}), 401

    data = request.get_json(silent=True) or {}
    try:
        abo_amount = int(data.get("abo_amount", 0) or 0)
    except Exception:
        abo_amount = 0

    if abo_amount <= 0:
        return jsonify({"error": "invalid_abo_amount"}), 400

    unit_amount_cents = 490
    uid = str(user.get("id", "")).strip()
    email = str(user.get("email", "")).strip()

    try:
        session = stripe.checkout.Session.create(
            mode="payment",
            success_url=STRIPE_SUCCESS_URL or "https://app.copyrightchain.it/?billing=success",
            cancel_url=STRIPE_CANCEL_URL or "https://app.copyrightchain.it/?billing=cancel",
            customer_email=email or None,
            payment_method_types=["card"],
            allow_promotion_codes=True,
            line_items=[{
                "price_data": {
                    "currency": "eur",
                    "product_data": {
                        "name": f"ABO Coin Recharge ({abo_amount} ABO)",
                        "description": f"Wallet recharge for user {uid}"
                    },
                    "unit_amount": unit_amount_cents
                },
                "quantity": abo_amount
            }],
            metadata={
                "type": "abo_recharge",
                "user_id": uid,
                "abo_amount": str(abo_amount)
            }
        )
        return jsonify({"ok": True, "checkout_url": session.url, "session_id": session.id})
    except Exception as e:
        return jsonify({"error": "stripe_checkout_error", "detail": str(e)}), 500


@APP.post("/billing/stripe/webhook")
def billing_stripe_webhook():
    if not STRIPE_SECRET_KEY or not STRIPE_WEBHOOK_SECRET:
        return jsonify({"error": "stripe_not_configured"}), 500

    payload = request.get_data()
    sig_header = request.headers.get("Stripe-Signature", "")

    try:
        event = stripe.Webhook.construct_event(
            payload=payload,
            sig_header=sig_header,
            secret=STRIPE_WEBHOOK_SECRET
        )
    except Exception as e:
        return jsonify({"error": "invalid_webhook", "detail": str(e)}), 400

    payload_json = {}
    try:
        payload_json = json.loads(payload.decode("utf-8"))
    except Exception:
        payload_json = {}

    event_type = str(payload_json.get("type", "")).strip()
    obj = (((payload_json.get("data") or {}).get("object")) or {})
    meta = obj.get("metadata", {}) or {}

    if event_type == "checkout.session.completed":
        uid = str(meta.get("user_id", "")).strip()
        payment_id = str(obj.get("id", "")).strip()
        try:
            abo_amount = int(meta.get("abo_amount", "0") or "0")
        except Exception:
            abo_amount = 0

        print("[STRIPE WEBHOOK DEBUG] event_type=", event_type, "uid=", uid, "payment_id=", payment_id, "abo_amount=", abo_amount, flush=True)

        if uid and abo_amount > 0 and payment_id:
                already_done = False
                try:
                    if os.path.exists(CERT_USAGE_LOG):
                        with open(CERT_USAGE_LOG, "r", encoding="utf-8") as f:
                            for line in f:
                                try:
                                    row = json.loads(line)
                                    if row.get("kind") == "stripe_recharge" and row.get("payment_id") == payment_id:
                                        already_done = True
                                        break
                                except Exception:
                                    continue
                except Exception:
                    pass

                if not already_done:
                    set_balance(uid, get_balance(uid) + abo_amount)
                    cert_log_request(
                        kind="stripe_recharge",
                        payment_id=payment_id,
                        user_id=uid,
                        abo_amount=abo_amount,
                        balance_after=get_balance(uid)
                    )

    return jsonify({"ok": True})





@APP.post("/tower/users/reset-password")
def tower_users_reset_password_hyphen():
    tok = request.headers.get("X-CCP-Token", "")
    if not tower_session() and not (ADMIN_TOKEN and tok == ADMIN_TOKEN):
        return jsonify({"error": "unauthorized"}), 401

    data = request.get_json(silent=True) or {}
    uid = str(data.get("user_id", "") or "").strip()
    new_password = str(data.get("new_password", "") or "").strip()

    if not uid:
        return jsonify({"error": "missing_user_id"}), 400
    if not new_password:
        return jsonify({"error": "missing_new_password"}), 400
    if len(new_password) < 8:
        return jsonify({"error": "weak_password"}), 400

    users_data = _json_load(USERS_FILE, {"users": []})
    users = users_data.get("users", [])
    found = None
    for u in users:
        if str(u.get("id", "") or "").strip() == uid:
            found = u
            break

    if not found:
        return jsonify({"error": "user_not_found"}), 404

    found["password_hash"] = _pw_hash(new_password)
    found["must_change_password"] = True
    found["updated_at"] = _now_iso()
    _json_save(USERS_FILE, users_data)

    try:
        pw_log = os.path.join(DATA_DIR, "tower_password_log.jsonl")
        row = {
            "ts": _now_iso(),
            "user_id": uid,
            "action": "reset_password",
            "must_change_password": True,
            "actor": "tower",
        }
        with open(pw_log, "a", encoding="utf-8") as f:
            f.write(json.dumps(row, ensure_ascii=False) + "\n")
    except Exception:
        pass

    return jsonify({
        "ok": True,
        "user_id": uid,
        "must_change_password": True
    })


@APP.post("/tower/users/credit")
def tower_users_credit():
    tok = request.headers.get("X-CCP-Token", "")
    if not tower_session() and not (ADMIN_TOKEN and tok == ADMIN_TOKEN):
        return jsonify({"error": "unauthorized"}), 401

    data = request.get_json(silent=True) or {}
    uid = str(data.get("user_id", "") or "").strip()
    amount = data.get("amount", None)
    note = str(data.get("note", "") or "").strip()

    if not uid:
        return jsonify({"error": "missing_user_id"}), 400
    try:
        amount = float(amount)
    except Exception:
        return jsonify({"error": "invalid_amount"}), 400

    if amount == 0:
        return jsonify({"error": "zero_amount"}), 400

    users_data = _json_load(USERS_FILE, {"users": []})
    users = users_data.get("users", [])
    found = None
    for u in users:
        if str(u.get("id", "") or "").strip() == uid:
            found = u
            break
    if not found:
        return jsonify({"error": "user_not_found"}), 404

    before = float(get_balance(uid) or 0)
    add_balance(uid, amount)
    after = float(get_balance(uid) or 0)

    try:
        credit_log = os.path.join(DATA_DIR, "tower_credit_log.jsonl")
        row = {
            "ts": _now_iso(),
            "user_id": uid,
            "amount": amount,
            "balance_before": before,
            "balance_after": after,
            "note": note,
            "actor": "tower",
        }
        with open(credit_log, "a", encoding="utf-8") as f:
            f.write(json.dumps(row, ensure_ascii=False) + "\n")
    except Exception:
        pass

    return jsonify({
        "ok": True,
        "user_id": uid,
        "amount": amount,
        "balance_before": before,
        "balance_after": after
    })



@APP.get("/tower/users/<user_id>/certificates")
def tower_user_certificates(user_id):
    tok = request.headers.get("X-CCP-Token", "")
    if not tower_session() and not (ADMIN_TOKEN and tok == ADMIN_TOKEN):
        return jsonify({"error": "unauthorized"}), 401

    uid = str(user_id or "").strip()
    if not uid:
        return jsonify({"error": "missing_user_id"}), 400

    users_data = _json_load(USERS_FILE, {"users": []})
    users = users_data.get("users", [])
    target = None
    for u in users:
        if str(u.get("id", "")).strip() == uid:
            target = u
            break

    if not target:
        return jsonify({"error": "user_not_found"}), 404

    certs = []
    try:
        if os.path.exists(CERT_USAGE_LOG):
            with open(CERT_USAGE_LOG, "r", encoding="utf-8") as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        row = json.loads(line)
                    except Exception:
                        continue

                    row_uid = str(row.get("user_id", "") or "").strip()
                    if row_uid != uid:
                        continue

                    cert_type = str(row.get("cert_type", "base") or "base").strip()
                    if cert_type == "blockchain":
                        try:
                            row = _ots_refresh_row(row)
                        except Exception:
                            pass

                    certs.append(_build_client_certificate_row(row))

        certs.sort(key=lambda r: str(r.get("deposit_date", "") or r.get("ts", "") or ""), reverse=True)
    except Exception:
        certs = []

    return jsonify({
        "ok": True,
        "user_id": uid,
        "count": len(certs),
        "certificates": certs
    })



@APP.get("/tower/users")
def tower_users():
    tok = request.headers.get("X-CCP-Token", "")
    if not tower_session() and not (ADMIN_TOKEN and tok == ADMIN_TOKEN):
        return jsonify({"error": "unauthorized"}), 401

    data = _json_load(USERS_FILE, {"users": []})
    users = data.get("users", [])
    if not isinstance(users, list):
        users = []

    cert_counts = {}
    try:
        if os.path.exists(CERT_USAGE_LOG):
            with open(CERT_USAGE_LOG, "r", encoding="utf-8") as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        row = json.loads(line)
                    except Exception:
                        continue
                    uid = str(row.get("user_id", "") or "").strip()
                    if not uid:
                        continue
                    cert_counts[uid] = cert_counts.get(uid, 0) + 1
    except Exception:
        cert_counts = {}

    out = []
    for u in users:
        uid = str(u.get("id", "") or "").strip()
        out.append({
            "id": uid,
            "email": str(u.get("email", "") or "").strip(),
            "name": str(u.get("name", "") or "").strip(),
            "token": str(u.get("token", "") or "").strip(),
            "wallet": str(u.get("wallet", "") or "").strip(),
            "plan": str(u.get("plan", "free") or "free").strip(),
            "ai_limit_monthly": int(u.get("ai_limit_monthly", 0) or 0),
            "must_change_password": bool(u.get("must_change_password", False)),
            "created_at": str(u.get("created_at", "") or "").strip(),
            "updated_at": str(u.get("updated_at", "") or "").strip(),
            "balance_abo": get_balance(uid) if uid else 0,
            "cert_count": int(cert_counts.get(uid, 0)),
        })

    out.sort(key=lambda x: (x.get("created_at", ""), x.get("id", "")), reverse=True)
    return jsonify({"ok": True, "count": len(out), "users": out})


@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)


Youez - 2016 - github.com/yon3zu
LinuXploit