import logging import os import time from datetime import datetime, timezone from flask import Flask, jsonify, redirect, request, send_file, send_from_directory from server.core_adapters import get_adapter from server.database import ( add_subscriber, check_rate_limit, export_all, get_connection, get_subscriber_by_id, init_db, ) from server.profile_generator import ( create_esim_profile, derive_opc, generate_iccid, generate_imsi, generate_ki, ) from server.utils import ( bytes_to_hex, get_client_ip, get_mac_from_arp, hex_to_bytes, load_config, normalize_mac, validate_mac, ) logger = logging.getLogger(__name__) CONFIG_PATH = os.environ.get("CONFIG_PATH", "config.yaml") _profile_cache: dict[int, bytes] = {} _start_time = time.time() def create_app(config_path: str = CONFIG_PATH) -> Flask: cfg = load_config(config_path) portal_dir = os.path.join(os.path.dirname(__file__), "..", "portal") portal_dir = os.path.abspath(portal_dir) app = Flask(__name__, static_folder=portal_dir, static_url_path="") db_path = cfg["database"]["path"] os.makedirs(os.path.dirname(db_path), exist_ok=True) init_db(db_path) adapter = get_adapter(cfg) if adapter: logger.info(f"Core adapter: {adapter.__class__.__name__}") else: logger.info("Core adapter: none (export-only mode)") net_cfg = cfg["network"] mcc = net_cfg["mcc"] mnc = net_cfg["mnc"] op_bytes = hex_to_bytes(net_cfg["op_key"]) profile_cfg = cfg["profile"] profile_name = profile_cfg["profile_name"] carrier_name = profile_cfg["carrier_name"] rl_cfg = cfg["rate_limiting"] window_hours = rl_cfg["window_hours"] # ------------------------------------------------------------------ # Routes # ------------------------------------------------------------------ @app.route("/") def index(): return redirect("/activate", code=302) @app.route("/activate") def activate(): return send_from_directory(portal_dir, "index.html") @app.route("/api/provision", methods=["POST"]) def provision(): body = request.get_json(silent=True) or {} mac = body.get("mac_address", "").strip() if not mac: client_ip = get_client_ip(request) mac = get_mac_from_arp(client_ip) or "" if not mac: return jsonify({"error": "Could not determine MAC address"}), 400 mac = normalize_mac(mac) if not validate_mac(mac): return jsonify({"error": "Invalid MAC address"}), 400 conn = get_connection(db_path) try: if not check_rate_limit(conn, mac, window_hours): return jsonify( {"error": "Rate limit exceeded. One eSIM profile per device per hour."} ), 429 imsi = generate_imsi(mcc, mnc) ki = generate_ki() iccid = generate_iccid() opc = derive_opc(ki, op_bytes) ki_hex = bytes_to_hex(ki) opc_hex = bytes_to_hex(opc) profile_bytes = create_esim_profile( imsi, ki, opc, iccid, profile_name, carrier_name ) sub_id = add_subscriber(conn, imsi, iccid, ki_hex, opc_hex, mac) _profile_cache[sub_id] = profile_bytes if adapter: try: adapter.add_subscriber(imsi, ki_hex, opc_hex) except Exception as exc: logger.error(f"Core adapter sync failed for {imsi}: {exc}") logger.info(f"Provisioned IMSI {imsi} for MAC {mac} (id={sub_id})") return jsonify( { "id": sub_id, "imsi": imsi, "iccid": iccid, "profile_url": f"/api/profile/{sub_id}", } ), 200 except Exception as exc: logger.exception(f"Provision failed: {exc}") return jsonify({"error": "Profile generation failed"}), 500 finally: conn.close() @app.route("/api/profile/") def get_profile(sub_id: int): profile_bytes = _profile_cache.get(sub_id) if profile_bytes is None: conn = get_connection(db_path) sub = get_subscriber_by_id(conn, sub_id) conn.close() if sub is None: return jsonify({"error": "Profile not found"}), 404 return jsonify({"error": "Profile no longer cached; re-provision to download"}), 410 import io return send_file( io.BytesIO(profile_bytes), mimetype="application/octet-stream", as_attachment=True, download_name="esim-profile.bin", ) @app.route("/api/export") def export(): conn = get_connection(db_path) try: subscribers = export_all(conn) finally: conn.close() return jsonify( { "exported_at": datetime.now(timezone.utc).isoformat(), "count": len(subscribers), "subscribers": subscribers, } ) @app.route("/api/status") def status(): conn = get_connection(db_path) try: count = conn.execute("SELECT COUNT(*) FROM subscribers").fetchone()[0] finally: conn.close() return jsonify( { "status": "ok", "subscriber_count": count, "uptime_seconds": int(time.time() - _start_time), "core_adapter": adapter.__class__.__name__ if adapter else "none", } ) return app if __name__ == "__main__": logging.basicConfig(level=logging.INFO) cfg = load_config(CONFIG_PATH) app = create_app(CONFIG_PATH) app.run( host=cfg["server"]["host"], port=cfg["server"]["port"], debug=cfg["server"].get("debug", False), )