Mirrors the events_list_event_tickets nostr-transport RPC for callers
that don't hold a raw user prvkey (the webapp post-#9, in particular —
useTicketScanner.refreshStats now has a working HTTP path). Auth:
wallet admin_key + the event's wallet must be in the caller's wallet
set, matching the register endpoint's owner check.
Without this endpoint the activities scanner page loaded its initial
counts (via no-op fallbacks) but every post-scan refreshStats returned
404, leaving the Scanned counter stuck at 0 even though registrations
landed correctly. Surfaced by aio-demo manual test on 2026-06-03.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
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>
Previous commit reused the LNbits invoice payment_hash as the
first row's id, so a 3-ticket purchase ended up with one 64-hex
id and two short-hash ids — inconsistent and noisy in My Tickets.
Switch every row to urlsafe_short_hash. The shared payment_hash
column is the join key for invoice lookups (poll endpoint, ws
notifier, on_invoice_paid); rows never need to BE the payment
hash, they only need to point at it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the previous "one row, N seats via extra.quantity" model
with proper one-row-per-attendee semantics. Each attendee gets a
unique scannable id; the door PUT /register/{ticket_id} marks
them registered independently — so a buyer can purchase 3 tickets,
hand 2 QRs to friends arriving separately, and each attendee can
enter on their own schedule.
Schema (migrations_fork.py m002):
- ticket.payment_hash: new TEXT column shared across all rows of
a multi-ticket purchase. Backfilled `payment_hash = id` for
pre-migration rows (id WAS the payment_hash by invariant).
Wire:
- TicketPaymentRequest grows `ticket_ids: list[str]` so the
webapp gets every scannable id back in the create response.
- POST /tickets/{event_id}/{payment_hash} polling endpoint now
reports `ticket_ids` (every row) + keeps `ticket_id` for
back-compat.
- api_ticket_create loops quantity times; the first row reuses
payment_hash as id (preserves legacy `id == payment_hash`
invariant for single-ticket purchases), the rest get
urlsafe_short_hash() uuids.
Payment flow:
- on_invoice_paid fetches all rows by payment_hash and marks each
paid via set_ticket_paid, which now increments event.sold by 1
per row (was N per row via extra.quantity — simpler now). The
per-event asyncio lock still serializes counter + republish so
concurrent multi-ticket purchases for the same event don't
reorder the published Nostr state.
- Each paid row triggers its own send_ticket_notification_in_
background call — no-op for buyers without nostr_identifier /
email, useful when the buyer set those on the row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
api_ticket_create accepted user_id in the CreateTicket request body
(its root_validator even requires user_id XOR name+email), but
dropped it on the way to crud.create_ticket — tickets ended up
with user_id = NULL and the new GET /tickets/user/{id} endpoint
returned an empty list for every webapp buyer.
Pull data.user_id alongside name/email and forward it to
create_ticket. Backfilling existing rows is left to the operator
(deployment-specific data fix); fresh purchases starting from this
commit are correctly attributed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The webapp's useTicketPurchase polls this every 2s after firing
Pay with Wallet (or after presenting the QR) to confirm payment
before advancing to the ticket-QR success state. Without this
endpoint the post-payment poll loop returns 404 indefinitely and
the buyer never sees their ticket land — even though set_ticket_paid
fired on the invoice listener and the row is correctly marked paid
in the DB.
Returns {paid: bool, ticket_id?: str}. A missing or cross-event
ticket returns paid: false rather than 404 so the poll loop doesn't
need to special-case the not-yet-created race.
The WebSocket at /tickets/ws/{payment_hash} is more efficient for
push notifications — this POST is the fallback for clients that
can't open a relay-side socket.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The admin /republish-all hits every approved event regardless of
owner — useful for the catalog migration, but heavy. Organizers
who want to re-emit just THEIR own events (e.g. after the AIO
publisher gained the tickets_* tags and an organizer's events
should pick them up) need a lighter knob.
Backend: new POST /republish-mine wallet-scoped via require_admin_key,
mirrors api_tickets's `all_wallets=true` shape so the page can
re-emit across every wallet the user owns. Filters to approved +
non-canceled rows.
UI: "Republish mine" button alongside "New Event" so every
logged-in user sees it (no isAdmin gate). Loading state +
confirm dialog + success count notification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Loops over approved events and re-emits each NIP-52 calendar event.
Useful as a one-shot migration after the publisher's tag set
changes (e.g. the tickets_* tag rollout introduced in this PR) so
existing events on a deployed instance pick up the new metadata
without each organizer having to edit and save.
Gated by check_admin (LNbits instance admin), errors swallowed
per-event inside the publisher so one bad row doesn't block the
rest. Returns a count summary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The webapp My Tickets view + the owned-ticket badges in the
activities feed both rely on this endpoint to enumerate a buyer's
tickets across all events. The CRUD function already existed
(`get_tickets_by_user_id`); just expose it.
Auth: Bearer access token (the same shape the webapp already sends
to other LNbits endpoints). The path param must match the token-
bound user.id — users can only enumerate their own tickets, not
anyone else's by ID-guessing.
Returns full `Ticket` rows rather than `PublicTicket` because the
owner needs the payment_hash (for the QR) + the `extra` envelope
(for refund / promo / notification state) in My Tickets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rebases the aio fork onto upstream v1.6.1 (4bf867e), pulling in:
- fiat checkout + email/Nostr DM ticket notifications (PR #50)
- currency-conversion fix (v1.5.0)
- custom notification subject/body (v1.6.0)
- resend-email button on the ticket list (PR #51)
Notable merges:
- views_api.api_event_update keeps the explicit-field-list gating from
the aio.4 security fix, with allow_fiat + fiat_currency added so an
owner editing a fiat-enabled event keeps the fiat config.
- models.PublicEvent now exposes both upstream's fiat fields and our
location / categories / status fields.
- migrations.py reverts to byte-identical to upstream v1.6.1 (no aio
entries); fork schema lives in migrations_fork.py (per aiolabs/lnbits#8).
- Lint reformatted with black + ruff to match upstream style.
Contributors entry adds `padreug` (aio fork maintainer).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add GET /events/api/v1/events/settings/public — invoice-key-gated
(anyone with a wallet) — returning just `{ auto_approve }`. The webapp
needs this to render accurate edit-flow copy without forcing every
event creator to also be an LNbits admin.
The admin-only GET /settings stays the source of truth for the full
EventsSettings payload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
event_start_date / event_end_date now accept either YYYY-MM-DD (date-only)
or YYYY-MM-DDTHH:MM (ISO datetime). The NIP-52 publisher switches kind
on the "T" delimiter: kind 31922 (date-based, YYYY-MM-DD start/end) when
absent, kind 31923 (time-based, unix-timestamp start/end + day-granularity
D tags) when present. Delete events match the original publish kind.
Closing-date parsing accepts both formats. The LNbits admin form gains
optional HH:MM inputs alongside each date picker; they fold into the
wire-format string on submit and split back on edit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Approved events are mirrored to Nostr as NIP-52 calendar events (kind
31922) signed by the wallet owner's pubkey, and incoming kind 31922/31923
events from subscribed relays are synced into the local DB so events
created on other LNbits instances or Nostr clients show up locally.
- m009 stores nostr_event_id + nostr_event_created_at on each event
(used for replaceable updates and NIP-09 deletes); m011 adds location
+ JSON-encoded categories list (NIP-52 location/`t` tags).
- models: Event/PublicEvent/CreateEvent gain location, categories,
nostr_event_id, nostr_event_created_at; parse_categories validator
decodes the JSON column on read.
- nostr/{event,nostr_client}.py: Schnorr signing, websocket relay client,
and a NostrEvent model (publish-only and subscribe variants).
- nostr_publisher.py: build/sign NIP-52 kind 31922 events and NIP-09
delete events; publish via the relay client.
- nostr_sync.py: subscribe to kinds 31922/31923, dedupe by nostr_event_id
/ d-tag, upsert Events; auto-approves discovered Nostr events since
they're already public.
- nostr_hooks.py: thin bridge that views_api handlers call to publish
or delete a NIP-52 event for a given local event. Lives in its own
module to keep `from . import nostr_client` out of the view layer
and avoid the views_api -> publisher import cycle.
- views_api: hooks publish_or_delete_nostr_event into create-on-approved,
update-when-already-published, cancel (delete), delete (delete), and
approve (publish).
- __init__.py: 3-task lifespan — wait_for_paid_invoices (upstream),
NostrClient bootstrap, and the NIP-52 sync loop. Module-level
nostr_client global is set by the bootstrap and read dynamically by
publish_or_delete_nostr_event so the import order works regardless of
whether nostrclient is up at startup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Non-admin event submissions now land in a "proposed" queue that LNbits
admins review before the event becomes ticketable and publicly listed.
- m008 adds events.events.status (proposed/approved/rejected); m010 seeds
an events.settings singleton row with the auto_approve toggle.
- Models: Event/CreateEvent.status, EventsSettings, optional date fields
with sensible defaults (closing_date defaults to event_end_date which
defaults to event_start_date), PublicEvent.status surfaces the workflow
state on the public endpoint.
- crud: get_all/public/pending_events for the admin views; get/update_settings
for the auto_approve toggle; create_event auto-fills missing date defaults.
- views_api:
* POST /api/v1/events accepts wallet invoice keys so anyone can submit;
handler stamps status="proposed" for non-admins when auto_approve is off
* /public, /all, /pending, /settings (GET+PUT), /{id}/{approve,reject},
/{id}/tickets endpoints; literal-prefix routes declared before /{event_id}
so FastAPI matches them correctly
* Public GET /{event_id} bypasses sold-out / closing-window gates for
proposed/rejected events and returns the trimmed PublicEvent so the SFC
can render a "pending approval" banner
* POST /tickets/{event_id} rejects when event.status != "approved"
- Frontend: index.vue gains an admin Settings card, Pending Approvals list,
status badge column and approve/reject row actions, plus an All Users'
Events admin table; index.js gains the data + methods + an isAdmin probe
via GET /events/all; display.vue shows pending/rejected banners and
hides the Buy Ticket form unless status === "approved".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: register public page saves to localstorage
previsously it fetched all tickets without much information. now it
saves the full scanned ticket after it was scanned, so it can be checked
by some1 without a login
* add last scan
* short id
* prettier
* escape name
* add email pydantic validation (API)
* format prettier
* don't allow slash on email also
* make regex const
* use string literals
* make get ticket a POST
* email regex
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>