From 0dc2dcc35f6c79275ec1e5e72d3430556870c515 Mon Sep 17 00:00:00 2001 From: Padreug Date: Thu, 21 May 2026 12:23:10 +0200 Subject: [PATCH] fix: gate event edits through the approval workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PUT /events/{id} endpoint blindly copied every field from the request body onto the existing event, including `status`. A non-admin owner with auto_approve=false could PUT {"status": "approved", ...} and self-approve, bypassing review entirely. Replace the blanket setattr loop with an explicit field list (status omitted) and derive the new status from the same admin / auto_approve gate that api_event_create uses. Reconcile Nostr against the status transition: approved → approved : re-publish the replaceable NIP-52 event proposed → approved : fresh publish approved → proposed : NIP-09 delete so the public feed drops it until the edit is re-approved proposed → proposed : no-op Also apply the same end/closing-date defaulting as create_event so an edit that omits those fields doesn't wipe them. Co-Authored-By: Claude Opus 4.7 (1M context) --- views_api.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/views_api.py b/views_api.py index fd59390..afea116 100644 --- a/views_api.py +++ b/views_api.py @@ -218,6 +218,18 @@ async def api_event_update( data: CreateEvent, wallet: WalletTypeInfo = Depends(require_admin_key), ) -> Event: + """Update an event. The owner can edit any mutable field; the status + is derived (admin / `auto_approve` ⇒ approved, otherwise proposed) + and is NEVER taken from the request body — that would let owners + self-approve. + + Nostr is reconciled against the status transition: + approved → approved : re-publish the replaceable NIP-52 event + proposed → approved : fresh publish + approved → proposed : NIP-09 delete so the public feed drops it + until the edit is re-approved + proposed → proposed : no-op + """ event = await get_event(event_id) if not event: raise HTTPException( @@ -227,13 +239,54 @@ async def api_event_update( raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="Not your event." ) - for k, v in data.dict().items(): - setattr(event, k, v) + + from lnbits.settings import settings + + ext_settings = await get_settings() + user_id = wallet.wallet.user + is_admin = user_id == settings.super_user or user_id in settings.lnbits_admin_users + + previous_status = event.status + + # Same defaulting as create_event: optional end/closing dates fall + # back to start_date when omitted, so an edit that doesn't restate + # them doesn't wipe them. + if not data.event_end_date: + data.event_end_date = data.event_start_date + if not data.closing_date: + data.closing_date = data.event_end_date + + # Explicit field list — never copy `status` from the request body. + # Includes upstream v1.6.1 fields (allow_fiat, fiat_currency) so an + # owner editing a fiat-enabled event keeps the fiat config. + for field in ( + "name", + "info", + "closing_date", + "event_start_date", + "event_end_date", + "currency", + "allow_fiat", + "fiat_currency", + "amount_tickets", + "price_per_ticket", + "banner", + "location", + "categories", + "extra", + ): + setattr(event, field, getattr(data, field)) + + event.status = "approved" if (is_admin or ext_settings.auto_approve) else "proposed" + + event = await update_event(event) - # Re-publish the replaceable NIP-52 event if we already announced it. - if event.status == "approved" and event.nostr_event_id: + if event.status == "approved": await publish_or_delete_nostr_event(event) + elif previous_status == "approved": + # Take it down from the public feed while it waits for re-approval. + await publish_or_delete_nostr_event(event, delete=True) return event