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
|
|
@ -29,6 +29,9 @@
|
|||
- **Drag-n-drop ноды** — порядок нод в списке настраивается мышкой, как у Remnawave.
|
||||
- **Rate-limit на вход** — по IP, отдельно на пароль и на 2FA-код.
|
||||
- **Свой путь входа** — страницу логина можно увести с дефолтного `/admin` на любой другой (`ADMIN_PATH` в `.env`), доп. слой поверх rate-limit и 2FA — у Remnawave это в списке заявленных мер безопасности, у Marzban нет вообще.
|
||||
- **Пауза подписки** — временно отключить доступ без потери оплаченных дней (Marzban это умеет, Remnawave — нет): при возобновлении срок сдвигается ровно на длительность паузы.
|
||||
- **Исходящие вебхуки** — на оплату, выдачу/отзыв/паузу/возобновление подписки и на добавление/удаление/вкл-выкл ноды, с HMAC-подписью тела. У Remnawave это события по юзерам и нодам, у Marzban — только по юзерам; мы покрываем оба класса.
|
||||
- **Поиск и фильтр по подпискам** — по юзернейму/tg id/ноде/тарифу и по статусу, прямо в таблице.
|
||||
|
||||
## Архитектура
|
||||
|
||||
|
|
|
|||
|
|
@ -616,7 +616,7 @@
|
|||
<h2>Платежи и вебхуки</h2>
|
||||
<p>Вкладка Платежи → «Настройка приёма платежей» собирает публичную оферту и политику конфиденциальности (<code>/offer</code>, <code>/privacy</code>) из введённых реквизитов — ЮKassa их спросит при регистрации магазина. Дата вступления в силу проставляется один раз, правки реквизитов её не двигают.</p>
|
||||
<p>Ключи ЮKassa проверяются вживую через их <code>/v3/me</code> перед сохранением; у Platega такого эндпоинта нет, ключи сохраняются без проверки. Оба провайдера включаются независимо.</p>
|
||||
<p>Исходящие вебхуки (Настройки → Webhook на события) — панель стучится на указанный URL при оплате (<code>payment.paid</code>), ручной выдаче подписки админом (<code>subscription.granted_by_admin</code>), отзыве (<code>subscription.revoked</code>), постановке на паузу (<code>subscription.held</code>) и возобновлении (<code>subscription.resumed</code>). Тело подписано <code>X-Signature</code> (HMAC-SHA256). Секрет выдаётся один раз и не меняется при правке URL — для интеграций со своими системами, без опроса API.</p>
|
||||
<p>Исходящие вебхуки (Настройки → Webhook на события) — панель стучится на указанный URL при оплате (<code>payment.paid</code>), ручной выдаче подписки админом (<code>subscription.granted_by_admin</code>), отзыве (<code>subscription.revoked</code>), постановке на паузу (<code>subscription.held</code>) и возобновлении (<code>subscription.resumed</code>), а также при добавлении (<code>node.added</code>), удалении (<code>node.deleted</code>) и включении/выключении ноды (<code>node.enabled</code>/<code>node.disabled</code> — только когда состояние реально поменялось, не на каждое сохранение формы редактирования). Тело подписано <code>X-Signature</code> (HMAC-SHA256). Секрет выдаётся один раз и не меняется при правке URL — для интеграций со своими системами, без опроса API.</p>
|
||||
<p>Вкладка Платежи → «Тарифы» — цены по срокам и общий рубильник приёма оплаты. Как и реквизиты с ключами провайдеров, это читается панелью напрямую из <code>.env</code> при каждом запросе — правки в UI применяются мгновенно везде (бот, API, проверка вебхуков), рестарт панели нигде не требуется.</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -723,7 +723,7 @@
|
|||
<div style="flex:0"><label class="f"> </label><button class="btn" onclick="saveWebhookSettings()">Сохранить</button></div>
|
||||
</div>
|
||||
<p class="check-hint">
|
||||
События: <code>payment.paid</code>, <code>subscription.granted_by_admin</code>, <code>subscription.revoked</code>, <code>subscription.held</code>, <code>subscription.resumed</code>. Тело — JSON <code>{"event": "...", "data": {...}}</code>, подписано заголовком <code>X-Signature</code> (HMAC-SHA256 от тела запроса на секрете ниже) — так получатель проверяет, что запрос реально от панели.
|
||||
События: <code>payment.paid</code>, <code>subscription.granted_by_admin</code>, <code>subscription.revoked</code>, <code>subscription.held</code>, <code>subscription.resumed</code>, <code>node.added</code>, <code>node.deleted</code>, <code>node.enabled</code>, <code>node.disabled</code>. Тело — JSON <code>{"event": "...", "data": {...}}</code>, подписано заголовком <code>X-Signature</code> (HMAC-SHA256 от тела запроса на секрете ниже) — так получатель проверяет, что запрос реально от панели.
|
||||
Секрет для проверки: <code id="webhook-secret-display">—</code>
|
||||
</p>
|
||||
<div id="webhook-result"></div>
|
||||
|
|
|
|||
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