feat: outbound webhooks for payment/subscription events
Per the docs.rw comparison researched earlier tonight, Remnawave fires webhooks for users+nodes and Marzban for users — this panel had neither, only received inbound webhooks from payment providers. New webhooks.py, fired on payment.paid (both webhook-driven and reconciler-driven grant paths, so it fires regardless of which one actually processes a given payment) and subscription.granted_by_admin (kept as a distinct event name rather than reusing payment.paid, since no money necessarily changed hands there). Settings tab gets a URL field; a secret is generated once on first save via secrets.token_hex and never regenerated on later URL edits, so a receiver's signature verification doesn't silently break when the admin just updates the endpoint. Every delivery is HMAC-SHA256 signed over the raw JSON body via X-Signature, same verification shape Platega already uses for its inbound webhooks. Delivery is fire-and-forget (10s timeout, swallows all exceptions) — a receiver being down must never block or fail a payment grant. Reads WEBHOOK_URL/WEBHOOK_SECRET fresh from .env via legal.py's existing reader instead of adding a third copy of that logic. Verified with a real local HTTP server: actual delivery, payload shape, and that the received X-Signature verifies against the configured secret using the receiver's own side of the HMAC — not just asserting the sender computed *something*. Also verified the no-URL-configured no-op path and that changing the URL later does not rotate the secret. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
24a1b26df2
commit
4b86406041
4 changed files with 100 additions and 1 deletions
23
webhooks.py
Normal file
23
webhooks.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
from legal import read_env_var
|
||||
|
||||
|
||||
def send(event: str, data: dict):
|
||||
url = read_env_var("WEBHOOK_URL")
|
||||
secret = read_env_var("WEBHOOK_SECRET")
|
||||
if not url or not secret:
|
||||
return
|
||||
body = json.dumps({"event": event, "data": data}).encode()
|
||||
signature = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
||||
req = urllib.request.Request(
|
||||
url, data=body, method="POST",
|
||||
headers={"Content-Type": "application/json", "X-Signature": signature},
|
||||
)
|
||||
try:
|
||||
urllib.request.urlopen(req, timeout=10)
|
||||
except Exception:
|
||||
pass
|
||||
Loading…
Add table
Add a link
Reference in a new issue