| 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
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
# 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)
# ================== 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("/")
# 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"))
# 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)
# ================== 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")
# ================== 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
# genera PDF (solo base qui; blockchain potrai agganciare alla tua logica txid/ots)
try:
if cert_type == "base":
certificate_path = generate_base_certificate(payload, uid, request_id)
else:
# placeholder: per ora creiamo un pdf "semplice" in dir blockchain
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 (placeholder)")
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"File Hash: {str(payload.get('file_hash','')).strip()}")
c.drawString(2*cm, h - 6.6*cm, f"Verify URL: {str(payload.get('verify_url','')).strip()}")
c.setFont("Helvetica-Oblique", 9)
c.drawString(2*cm, 2.0*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_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)
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()
)
# 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
})
# ================== 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()
@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()
if not current_password or not new_password:
return jsonify({"error": "missing_fields"}), 400
if len(new_password) < 8:
return jsonify({"error": "weak_password"}), 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()
if q and q in (row_cert_id, row_hash, row_req):
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
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()
blockchain_status = "anchored" if cert_type == "blockchain" else "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,
"certificate_path": certificate_path,
"certificate_url": _build_public_cert_url(certificate_path) if certificate_path else "",
"verify_url": str(found.get("verify_url", "") or "").strip()
})
@APP.get("/cert/file")
def cert_file():
"""
Download certificato dal server path.
Sicurezza: consentito se tower session valida oppure header X-CCP-Token == ADMIN_TOKEN (se impostato).
"""
allowed = False
if tower_session():
allowed = True
elif ADMIN_TOKEN and request.headers.get("X-CCP-Token", "") == ADMIN_TOKEN:
allowed = True
if not allowed:
return jsonify({"error": "unauthorized"}), 401
path = (request.args.get("path", "") or "").strip()
if not path:
return jsonify({"error": "missing_path"}), 400
# limita ai cert dir
ap = os.path.abspath(path)
if not (ap.startswith(os.path.abspath(CERT_BASE_DIR)) or ap.startswith(os.path.abspath(CERT_BC_DIR))):
return jsonify({"error": "forbidden_path"}), 403
if not os.path.exists(ap):
return jsonify({"error": "not_found"}), 404
return send_file(ap, mimetype="application/pdf", as_attachment=True, download_name=os.path.basename(ap))
@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("/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)