119 lines
3.9 KiB
Python
119 lines
3.9 KiB
Python
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)
|