feat: outbound webhooks for node lifecycle — closes the "users + nodes" gap from the comparison
Last remaining actionable row from the docs.rw comparison table pulled two commits ago: "Webhook event support — Users + nodes (Remnawave) / Users only (Marzban)". Every webhook we send is subscription/payment events — user-side only, same as Marzban, even after last commit's revoke/hold/resume additions. Zero node events. node.added on creation, node.deleted on deletion (captures the node's label before it's gone, since delete_node doesn't return the row), node.enabled/node.disabled on the PATCH route — but only when the enabled field actually changes value, not on every save. Editing just the label, or PATCHing enabled to the same value it already had, correctly fires nothing — checked this specifically since a naive "enabled is in the request body" check would have spammed an event on every harmless edit of an already-enabled node. Verification: same two-part approach as the subscription lifecycle webhooks. AST-extracted the actual admin_update_node() body out of api.py (still can't import it directly) and ran it against a fake db/webhooks module — 5 cases: enabling, disabling, a same-value no-op save, and an unrelated-field-only edit, confirming the webhook fires exactly when and only when it should. Then a real local HTTP server for all four event types through the actual webhooks.send(), receiver-side HMAC recomputed independently from its own copy of the secret and compared against X-Signature, not trusted from the sender. README's feature list had also fallen behind the last three commits (webhooks, hold/pause, subscription search never got a bullet) — caught up all three while I was in there, not just the one this commit adds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
b8c4949201
commit
bbbab9998e
3 changed files with 16 additions and 3 deletions
12
api.py
12
api.py
|
|
@ -1086,6 +1086,7 @@ def admin_create_node(request: Request, body: dict = Body(...)):
|
|||
sni=body["sni"], flow=body.get("flow", "xtls-rprx-vision"),
|
||||
shared_uuid=body.get("shared_uuid"),
|
||||
)
|
||||
webhooks.send("node.added", {"code": node["code"], "label": node["label"], "kind": node["kind"]})
|
||||
return node
|
||||
|
||||
|
||||
|
|
@ -1096,16 +1097,25 @@ def admin_update_node(code: str, request: Request, body: dict = Body(...)):
|
|||
if code == "de1":
|
||||
editable = {"label"}
|
||||
allowed = {k: v for k, v in body.items() if k in editable}
|
||||
return db.update_node(code, **allowed)
|
||||
before = db.get_node(code)
|
||||
updated = db.update_node(code, **allowed)
|
||||
if before and updated and "enabled" in allowed and bool(before["enabled"]) != bool(updated["enabled"]):
|
||||
webhooks.send("node.enabled" if updated["enabled"] else "node.disabled", {
|
||||
"code": code, "label": updated["label"],
|
||||
})
|
||||
return updated
|
||||
|
||||
|
||||
@app.delete("/admin/api/nodes/{code}")
|
||||
def admin_delete_node(code: str, request: Request):
|
||||
require_admin(request)
|
||||
node = db.get_node(code)
|
||||
try:
|
||||
db.delete_node(code)
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e))
|
||||
if node:
|
||||
webhooks.send("node.deleted", {"code": code, "label": node["label"]})
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue