from __future__ import annotations import jwt import os import logging from logging.handlers import RotatingFileHandler from flask import Flask, request, jsonify from flask_cors import CORS from flask import Response from werkzeug.middleware.proxy_fix import ProxyFix try: from dotenv import load_dotenv load_dotenv() except Exception: pass from helpers.netopia import verify_ipn, get_status app = Flask(__name__) CORS(app, resources={r"/api/*": {"origins": "*"}}) # Tell Flask it is behind a proxy and should trust the X-Forwarded headers app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) # ---------- Logging ---------- app.logger.setLevel(logging.INFO) _log_dir = os.getenv("LOG_DIR", "logs") os.makedirs(_log_dir, exist_ok=True) _handler = RotatingFileHandler(os.path.join(_log_dir, "netopia_api.log"), maxBytes=1_000_000, backupCount=3) _handler.setLevel(logging.INFO) _handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")) app.logger.addHandler(_handler) @app.get("/healthz") def healthz(): return {"ok": True}, 200 # @app.post("/api/payments/ipn") # def ipn(): # try: # # Pass the whole request object, not just request.data # data = verify_ipn(request) # app.logger.info("IPN OK: %s", data) # return jsonify({"errorCode": 0}), 200 # except Exception as e: # app.logger.exception("IPN verification failed: %s", e) # return jsonify({"errorCode": 0}), 200 @app.post("/api/payments/ipn") def ipn(): token = request.headers.get('Verification-Token') or request.headers.get('X-Netopia-Signature') if not token: app.logger.error("No token found") return jsonify({"errorCode": 1, "error": "No token"}), 400 try: # Get the clean key from your env/settings from helpers.netopia import NetopiaSettings settings = NetopiaSettings.from_env() # Ensure newlines are correct public_key = settings.public_key_str.replace('\\n', '\n').strip() if not public_key.startswith("-----BEGIN"): public_key = f"-----BEGIN PUBLIC KEY-----\n{public_key}\n-----END PUBLIC KEY-----" # MANUAL VERIFICATION # We allow 30 seconds of 'leeway' for the clock # We specify RS512 because that's what Netopia uses decoded_data = jwt.decode( token, public_key, algorithms=["RS256", "RS512"], audience=settings.pos_signature, leeway=30 ) app.logger.info(f"VERIFIED DATA: {decoded_data}") # If we got here, the signature is VALID! # Now we just need to get the order status from the body json_body = request.get_json() order_id = json_body.get('order', {}).get('orderID') status = json_body.get('payment', {}).get('status') app.logger.info(f"Order {order_id} has status {status}") return jsonify({"errorCode": 0}), 200 except jwt.ExpiredSignatureError: app.logger.error("Token expired") return jsonify({"errorCode": 1, "error": "Expired"}), 400 except jwt.InvalidTokenError as e: app.logger.error(f"Invalid Token: {e}") return jsonify({"errorCode": 1, "error": str(e)}), 400 @app.get("/api/payments/status") def status(): ntp_id = request.args.get("ntpID") order_id = request.args.get("orderID") try: resp = get_status(ntp_id=ntp_id, order_id=order_id) return jsonify({"ok": True, "data": resp}), 200 except Exception as e: app.logger.exception("Status query failed: %s", e) return jsonify({"ok": False, "error": str(e)}), 400 if __name__ == "__main__": host = os.getenv("API_HOST", "0.0.0.0") port = int(os.getenv("API_PORT", "9000")) app.logger.info("Starting NETOPIA Flask sidecar on %s:%s", host, port) app.run(host=host, port=port)