| 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 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():
tok = request.headers.get("X-CCP-Token", "") or str(request.args.get("token", "") or "").strip()
if not tower_session() and not (ADMIN_TOKEN and tok == ADMIN_TOKEN):
return jsonify({"error": "unauthorized"}), 401
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/certificates/file")
def tower_certificate_file():
tok = request.headers.get("X-CCP-Token", "") or str(request.args.get("token", "") or "").strip()
if not tower_session() and not (ADMIN_TOKEN and tok == ADMIN_TOKEN):
return jsonify({"error": "unauthorized"}), 401
raw = str(request.args.get("path", "") or "").strip()
if not raw:
return jsonify({"error": "missing_path"}), 400
real = os.path.realpath(raw)
allowed_roots = [
os.path.realpath(CERT_BASE_DIR),
os.path.realpath(CERT_BC_DIR),
]
if not any(real.startswith(root + os.sep) or real == root 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
return send_file(real, as_attachment=False, download_name=os.path.basename(real))
@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)