feat: organizer ticket scanning over nostr-transport + secure legacy HTTP register endpoint #19

Merged
padreug merged 5 commits from ticket-scanner-nostr into main 2026-05-24 16:54:01 +00:00
Owner

Summary

The events extension's first nostr-transport RPC: organizers scan
attendees' tickets at the door from the webapp, signing the
register call with their Nostr key. The dispatcher resolves pubkey
→ Account → wallet (AUTH_WALLET); this handler adds event-level
ownership verification on top.

Companion webapp PR pending. Backend ships first so the RPC is
registered before the webapp goes live (without the handler,
every scan attempt times out).

Commits

Commit What
2b3d9df feat: events_ticket_register RPC over nostr transport
1d8dacb fix: require admin_key + owner check on PUT /tickets/register

RPC wire contract

  • rpc_name: events_ticket_register
  • auth_level: AUTH_WALLET (caller must sign + provide wallet_id)
  • body: { event_id: str, ticket_id: str }
  • response data on OK: full Ticket dict with registered=True,
    reg_timestamp set
  • errors (string into response.error):
    • "event_id and ticket_id are required" (bad request)
    • "Ticket does not exist on this event" (404-ish)
    • "Ticket not paid for" (paid=false)
    • "Ticket already registered" (idempotent rejection)
    • "Event does not exist"
    • "You do not own this event" (owner mismatch)
    • "Wallet access required (provide wallet_id)" (from dispatcher)

Registration in events_start() is guarded with try / except ImportError so the extension still loads on LNbits versions that
pre-date the transport (the HTTP path stays the fallback).

Legacy HTTP endpoint hardened

PUT /events/api/v1/tickets/register/{ticket_id} previously had
no auth decorator and no owner check. Any caller with a ticket
id could mark it registered.
This PR adds require_admin_key +
the same event.wallet ∈ user.wallet_ids check as the RPC.

The in-tree Quasar register page (static/js/register.js)
already sends the session admin_key via LNbits.api.request, so
the LNbits admin UI keeps scanning cleanly. Breaking change
for any external integration that hit the unauthed endpoint —
they need to start sending an admin_key.

Smoke

  • docker restart regtest-lnbits-1 → events extension loads,
    log line: [EVENTS] Registered nostr-transport RPC: events_ticket_register.
  • curl -X PUT http://localhost:5001/events/api/v1/tickets/register/<any>
    without admin_key → 401 (was: 200/403 depending on ticket state).

Test plan

  • Sign + publish a kind-21000 RPC event for
    events_ticket_register from a wallet that owns the event;
    response status=OK, ticket row updated.
  • Same RPC from a different wallet's pubkey → status=ERROR,
    error="You do not own this event".
  • Same RPC against an unpaid ticket → error="Ticket not paid for".
  • Same RPC against an already-registered ticket →
    error="Ticket already registered" (idempotent).
  • Same RPC with mismatched event_id ↔ ticket → error="Ticket does not exist on this event".
  • HTTP endpoint: no admin_key → 401. Wrong-wallet admin_key →
    403 ("You do not own this event"). Owner's admin_key → 200 +
    registered.
  • Existing Quasar /events/register/<event_id> page still
    scans (session admin_key still passes).

🤖 Generated with Claude Code

## Summary The events extension's first nostr-transport RPC: organizers scan attendees' tickets at the door from the webapp, signing the register call with their Nostr key. The dispatcher resolves pubkey → Account → wallet (`AUTH_WALLET`); this handler adds event-level ownership verification on top. Companion webapp PR pending. Backend ships first so the RPC is registered before the webapp goes live (without the handler, every scan attempt times out). ## Commits | Commit | What | |---|---| | `2b3d9df` | `feat: events_ticket_register RPC over nostr transport` | | `1d8dacb` | `fix: require admin_key + owner check on PUT /tickets/register` | ## RPC wire contract - **rpc_name**: `events_ticket_register` - **auth_level**: `AUTH_WALLET` (caller must sign + provide wallet_id) - **body**: `{ event_id: str, ticket_id: str }` - **response data** on OK: full `Ticket` dict with `registered=True`, `reg_timestamp` set - **errors** (string into `response.error`): - `"event_id and ticket_id are required"` (bad request) - `"Ticket does not exist on this event"` (404-ish) - `"Ticket not paid for"` (paid=false) - `"Ticket already registered"` (idempotent rejection) - `"Event does not exist"` - `"You do not own this event"` (owner mismatch) - `"Wallet access required (provide wallet_id)"` (from dispatcher) Registration in `events_start()` is guarded with `try / except ImportError` so the extension still loads on LNbits versions that pre-date the transport (the HTTP path stays the fallback). ## Legacy HTTP endpoint hardened `PUT /events/api/v1/tickets/register/{ticket_id}` previously had no auth decorator and no owner check. **Any caller with a ticket id could mark it registered.** This PR adds `require_admin_key` + the same `event.wallet ∈ user.wallet_ids` check as the RPC. The in-tree Quasar register page (`static/js/register.js`) already sends the session admin_key via `LNbits.api.request`, so the LNbits admin UI keeps scanning cleanly. **Breaking change** for any external integration that hit the unauthed endpoint — they need to start sending an admin_key. ## Smoke - `docker restart regtest-lnbits-1` → events extension loads, log line: `[EVENTS] Registered nostr-transport RPC: events_ticket_register`. - `curl -X PUT http://localhost:5001/events/api/v1/tickets/register/<any>` without admin_key → **401** (was: 200/403 depending on ticket state). ## Test plan - [ ] Sign + publish a kind-21000 RPC event for `events_ticket_register` from a wallet that owns the event; response `status=OK`, ticket row updated. - [ ] Same RPC from a different wallet's pubkey → `status=ERROR`, `error="You do not own this event"`. - [ ] Same RPC against an unpaid ticket → `error="Ticket not paid for"`. - [ ] Same RPC against an already-registered ticket → `error="Ticket already registered"` (idempotent). - [ ] Same RPC with mismatched event_id ↔ ticket → `error="Ticket does not exist on this event"`. - [ ] HTTP endpoint: no admin_key → 401. Wrong-wallet admin_key → 403 ("You do not own this event"). Owner's admin_key → 200 + registered. - [ ] Existing Quasar `/events/register/<event_id>` page still scans (session admin_key still passes). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Organizer-side ticket scanning over LNbits's freshly-merged
nostr-transport (kind 21000, NIP-44 v2). The organizer signs the
RPC event with their Nostr key; the transport dispatcher resolves
pubkey → Account → wallet (AUTH_WALLET) and the handler verifies
event-level ownership (event.wallet ∈ caller_user.wallet_ids)
before flipping `registered = True`.

Idempotence + state transitions mirror the legacy HTTP endpoint:
"Ticket not paid for" / "Ticket already registered" / "Ticket
does not exist on this event" / "You do not own this event" come
back as ERROR responses. Registration in events_start() is
guarded with try/except ImportError so the extension still loads
on older LNbits versions that pre-date the transport (HTTP path
stays the fallback there).

Webapp uses this as the new primary scan call site instead of
the legacy HTTP endpoint — see companion webapp PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix: require admin_key + owner check on PUT /tickets/register
Some checks failed
lint.yml / fix: require admin_key + owner check on PUT /tickets/register (pull_request) Failing after 0s
1d8dacbaa3
The legacy register endpoint had no auth decorator and no
event-ownership check — any caller who knew a ticket id could
mark it registered. Add require_admin_key (matches the rest of
the wallet-bound endpoints in this file) and verify the caller's
user owns the event the ticket belongs to.

Breaking change for any external integration that hit this
endpoint unauthed; the in-tree Quasar register page
(static/js/register.js) already sends the session admin_key via
LNbits.api.request so it keeps working.

The Nostr-transport flow at events_ticket_register (previous
commit) is the preferred call site for new callers; this HTTP
path stays for the legacy LNbits admin UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second nostr-transport handler on this branch. Returns paid + registered
counts plus the per-ticket roster (id, name, registered status, timestamp)
for one calendar event, organizer-only.

Backs the door scanner's counts strip and "scanned" list with backend
truth so a second organizer scanning on another device, an operator
switching from mobile to laptop mid-event, or a refresh in incognito
all see the same numbers instead of diverging from a per-device
localStorage cache.

Same authorisation posture as events_ticket_register: dispatcher
binds caller pubkey to wallet via AUTH_WALLET, handler verifies the
event's wallet is in the caller's wallet set. Only paid tickets land
in the response — proposed/unpaid rows are irrelevant at the door.

Webapp consumes this in aiolabs/webapp#73.
ui(admin): Tickets card above All Users' Events on the admin index
Some checks failed
lint.yml / ui(admin): Tickets card above All Users' Events on the admin index (pull_request) Failing after 0s
66d263ef14
The Tickets table is what an organiser actually scans during day-of
operations — it deserves the top slot. All Users' Events stays one
section down for the cross-tenant audit view (admin-only anyway).
feat(admin): Owner column on All Users' Events card
Some checks failed
lint.yml / feat(admin): Owner column on All Users' Events card (pull_request) Failing after 0s
3606fd9a0a
Adds the event's wallet owner (user_id) as the first column of the
admin-only All Users' Events table so cross-tenant rows are
attributable at a glance. Server-side join: GET /events/all now
resolves each event.wallet -> wallet.user and stamps the result on
the response as wallet_user_id. Frontend gets a dedicated
allUsersEventsTable.columns definition so the user's own-events
table stays unchanged.

Follow-up #22 covers letting the admin actually edit those events
once attributed.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/events!19
No description provided.