from flask import Flask, send_from_directory, request, jsonify, Response import requests, time, threading, json, os import cloudinary import cloudinary.uploader import cloudinary.api app = Flask(__name__, static_folder='static') # ── Cloudinary config ───────────────────────────────────────── cloudinary.config( cloud_name = "db6wrclc7", api_key = "371945239656253", api_secret = "ZgN-B2lyAz6zk2MI2N5b_1dc1r8", secure = True ) # ── Firebase ────────────────────────────────────────────────── FIREBASE_API_KEY = "AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU" FIRESTORE_BASE = "https://firestore.googleapis.com/v1/projects/vservers1/databases/(default)/documents" # ════════════════════════════════════════════════════════════ # CLOUDINARY ROUTES # ════════════════════════════════════════════════════════════ @app.route("/drive/upload", methods=["POST"]) def drive_upload(): try: elev_vpass = request.form.get("elevId", "unknown") materie = request.form.get("materieId", "general") f = request.files.get("file") if not f: return jsonify({"error": "no file"}), 400 file_bytes = f.read() file_name = f.filename or "fisier" print(f"[UPLOAD] {elev_vpass}/{materie}/{file_name} ({len(file_bytes)} bytes)") # Folder: SGE/RV-0022/Matematica folder = f"SGE/{elev_vpass}/{materie}" # Upload ca raw — public_id = numele complet cu extensie import io result = cloudinary.uploader.upload( io.BytesIO(file_bytes), folder = folder, public_id = file_name, # pastram extensia resource_type = "raw", use_filename = True, unique_filename = False, overwrite = False, context = f"uploaded_at={int(time.time())}|original_name={file_name}", ) print(f"[UPLOAD] OK → {result['public_id']}") _fs_log("upload", {"elevVpass": elev_vpass, "materie": materie, "fisier": file_name, "fileId": result["public_id"]}) return jsonify({ "fileId": result["public_id"], "name": file_name, "size": result.get("bytes", 0), "url": result.get("secure_url", ""), }) except Exception as e: import traceback; traceback.print_exc() return jsonify({"error": str(e)}), 500 @app.route("/drive/list", methods=["GET"]) def drive_list(): try: elev_vpass = request.args.get("elevId", "") materie = request.args.get("materieId", "") folder = f"SGE/{elev_vpass}/{materie}" result = cloudinary.api.resources( type = "upload", resource_type = "raw", prefix = folder + "/", max_results = 100, ) files = [] for r in result.get("resources", []): pid = r["public_id"] # Incearca sa extraga numele original din context ctx = r.get("context", {}).get("custom", {}) name = ctx.get("original_name", "") if not name: # fallback: ultimul segment din public_id name = pid.split("/")[-1] files.append({ "id": pid, "name": name, "size": r.get("bytes", 0), "mimeType": r.get("resource_type", "raw"), "createdTime": r.get("created_at", ""), "url": r.get("secure_url", ""), }) return jsonify({"files": files}) except cloudinary.exceptions.NotFound: return jsonify({"files": []}) except Exception as e: print(f"[LIST ERROR] {e}") return jsonify({"files": [], "error": str(e)}) @app.route("/drive/download/", methods=["GET"]) def drive_download(file_id): try: # Cloudinary: genereaza URL de download direct url = cloudinary.utils.cloudinary_url( file_id, resource_type = "raw", type = "upload", )[0] # Redirect catre URL-ul Cloudinary from flask import redirect return redirect(url) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/drive/delete/", methods=["DELETE"]) def drive_delete(file_id): try: cloudinary.uploader.destroy(file_id, resource_type="raw") return jsonify({"ok": True}) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/drive/test", methods=["GET"]) def drive_test(): try: result = cloudinary.api.ping() return jsonify({"ok": True, "cloudinary": result}) except Exception as e: return jsonify({"ok": False, "error": str(e)}), 500 # ════════════════════════════════════════════════════════════ # FIRESTORE HELPERS # ════════════════════════════════════════════════════════════ def _to_fields(data): fields = {} for k, v in data.items(): if v is None: fields[k] = {"nullValue": None} elif isinstance(v, bool): fields[k] = {"booleanValue": v} elif isinstance(v, int): fields[k] = {"integerValue": str(v)} else: fields[k] = {"stringValue": str(v)} return fields def _fs_add(col, data): r = requests.post(f"{FIRESTORE_BASE}/{col}?key={FIREBASE_API_KEY}", json={"fields": _to_fields(data)}, timeout=15) return r.status_code == 200 def _fs_log(tip, extra={}): try: payload = {"tip": tip, "ts": int(time.time()), **extra} _fs_add("loguri", payload) except: pass # ── Log endpoint ────────────────────────────────────────────── @app.route("/api/log", methods=["POST"]) def api_log(): try: data = request.get_json(silent=True) or {} _fs_log(data.get("tip","event"), {k:v for k,v in data.items() if k!="tip"}) return jsonify({"ok": True}) except Exception as e: return jsonify({"error": str(e)}), 500 # ── Logs endpoint ───────────────────────────────────────────── @app.route("/api/logs", methods=["GET"]) def api_logs(): try: r = requests.get(f"{FIRESTORE_BASE}/loguri?key={FIREBASE_API_KEY}&pageSize=200", timeout=15) docs = r.json().get("documents", []) logs = [] for d in docs: f = d.get("fields", {}) entry = {} for k, v in f.items(): entry[k] = v.get("stringValue") or v.get("integerValue") or v.get("booleanValue") or "" entry["_id"] = d["name"].split("/")[-1] logs.append(entry) logs.sort(key=lambda x: int(x.get("ts",0)), reverse=True) return jsonify({"logs": logs[:150]}) except Exception as e: return jsonify({"error": str(e), "logs": []}), 500 # ── Ping / Online ───────────────────────────────────────────── _online = {} @app.route("/api/ping", methods=["POST"]) def api_ping(): data = request.get_json(silent=True) or {} key = f"{data.get('vpass','?')}_{data.get('role','?')}" _online[key] = {"ts": time.time(), "vpass": data.get("vpass","?"), "role": data.get("role","?"), "name": data.get("name","")} now = time.time() expired = [k for k,v in _online.items() if now - v["ts"] > 120] for k in expired: del _online[k] return jsonify({"ok": True}) @app.route("/api/online", methods=["GET"]) def api_online(): now = time.time() active = {k:v for k,v in _online.items() if now - v["ts"] <= 120} return jsonify({ "total": len(active), "elevi": [v for v in active.values() if v["role"]=="elev"], "profesori": [v for v in active.values() if v["role"]=="profesor"], }) # ════════════════════════════════════════════════════════════ # CLEANUP — sterge fisierele mai vechi de 7 zile din Cloudinary # ════════════════════════════════════════════════════════════ def cleanup_old_files(): while True: try: time.sleep(3600) print("[CLEANUP] Verific fisiere vechi...") limit_ts = int(time.time()) - 7 * 24 * 3600 result = cloudinary.api.resources( type="upload", resource_type="raw", prefix="SGE/", max_results=500, ) deleted = 0 for r in result.get("resources", []): created = r.get("created_at","") if created: try: from datetime import datetime, timezone dt = datetime.fromisoformat(created.replace("Z","+00:00")) if int(dt.timestamp()) < limit_ts: cloudinary.uploader.destroy(r["public_id"], resource_type="raw") deleted += 1 print(f"[CLEANUP] Sters: {r['public_id']}") _fs_log("cleanup_delete", {"fisier": r["public_id"]}) except: pass print(f"[CLEANUP] Gata: {deleted} sterse.") except Exception as ex: print(f"[CLEANUP] Eroare: {ex}") time.sleep(300) # ════════════════════════════════════════════════════════════ # AUTO SEED # ════════════════════════════════════════════════════════════ ELEVI = [ {"pozitie":1,"nume":"Andronic Aniela","vpassId":"AA-0001"}, {"pozitie":2,"nume":"Badan Alexandra","vpassId":"BA-0002"}, {"pozitie":3,"nume":"Bunescu Lavinia","vpassId":"BL-0003"}, {"pozitie":4,"nume":"Bunescu Lorena","vpassId":"BL-0004"}, {"pozitie":5,"nume":"Burdujan Damian","vpassId":"BD-0005"}, {"pozitie":6,"nume":"Ceban Madalina","vpassId":"CM-0006"}, {"pozitie":7,"nume":"Ceban Valeria","vpassId":"CV-0007"}, {"pozitie":8,"nume":"Cebanu Nichita","vpassId":"CN-0008"}, {"pozitie":9,"nume":"Chele Ilinca","vpassId":"CI-0009"}, {"pozitie":10,"nume":"Chihai Mirela","vpassId":"CM-0010"}, {"pozitie":11,"nume":"Cojocari Ciprian","vpassId":"CC-0011"}, {"pozitie":12,"nume":"Creciun Leonard","vpassId":"CL-0012"}, {"pozitie":13,"nume":"Lozovan Sanda","vpassId":"LS-0013"}, {"pozitie":14,"nume":"Lungu Sergiu","vpassId":"LS-0014"}, {"pozitie":15,"nume":"Mahnea Adelina","vpassId":"MA-0015"}, {"pozitie":16,"nume":"Morcan Ina","vpassId":"MI-0016"}, {"pozitie":17,"nume":"Morcan Ion","vpassId":"MI-0017"}, {"pozitie":18,"nume":"Popa Cristina","vpassId":"PC-0018"}, {"pozitie":19,"nume":"Pricob Adelina","vpassId":"PA-0019"}, {"pozitie":20,"nume":"Prinos Catalina","vpassId":"PC-0020"}, {"pozitie":21,"nume":"Radu Daniela","vpassId":"RD-0021"}, {"pozitie":22,"nume":"Rosca Victor","vpassId":"RV-0022"}, {"pozitie":23,"nume":"Rotari Cristian","vpassId":"RC-0023"}, {"pozitie":24,"nume":"Rotaru Camelia","vpassId":"RC-0024"}, {"pozitie":25,"nume":"Sambris Stanislav","vpassId":"SS-0025"}, {"pozitie":26,"nume":"Sirbu Delia","vpassId":"SD-0026"}, {"pozitie":27,"nume":"Soltoian Elena","vpassId":"SE-0027"}, {"pozitie":28,"nume":"Tabuncic Chiril","vpassId":"TC-0028"}, {"pozitie":29,"nume":"Turcan Gabriel","vpassId":"TG-0029"}, {"pozitie":30,"nume":"Turcan Mihai","vpassId":"TM-0030"}, {"pozitie":31,"nume":"Turcanu Ilie","vpassId":"TI-0031"}, {"pozitie":32,"nume":"Virlan Daniela","vpassId":"VD-0032"}, ] def auto_seed(): time.sleep(4) try: r = requests.get(f"{FIRESTORE_BASE}/elevi?key={FIREBASE_API_KEY}&pageSize=1", timeout=15) if r.json().get("documents"): print("[SGE] DB populat — seed omis."); return print("[SGE] Seed automat...") ok = 0 for e in ELEVI: if _fs_add("elevi", {"nume":e["nume"],"vpassId":e["vpassId"], "pozitie":e["pozitie"],"pin":None,"confirmed":False}): ok += 1 time.sleep(0.12) print(f"[SGE] Seed gata: {ok}/{len(ELEVI)}") except Exception as ex: print(f"[SGE] Seed eroare: {ex}") threading.Thread(target=auto_seed, daemon=True).start() threading.Thread(target=cleanup_old_files, daemon=True).start() # ── Serve static ────────────────────────────────────────────── @app.route("/") def index(): return send_from_directory("static", "index.html") @app.errorhandler(404) def not_found(e): return send_from_directory("static", "503.html"), 404 @app.route("/") def serve_files(filename): try: return send_from_directory("static", filename) except: return send_from_directory("static", "503.html"), 404 if __name__ == "__main__": app.run(host="0.0.0.0", port=7860, debug=False)