Commit graph

69 commits

Author SHA1 Message Date
7b761a1aef fix: every ticket row gets a fresh short-hash id (no payment_hash reuse)
Some checks failed
lint.yml / fix: every ticket row gets a fresh short-hash id (no payment_hash reuse) (pull_request) Failing after 0s
lint.yml / fix: every ticket row gets a fresh short-hash id (no payment_hash reuse) (push) Failing after 0s
v1.6.1-aio.2
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>
2026-05-23 22:57:00 +02:00
59068fe09d feat: multi-ticket purchases as N rows sharing one payment_hash
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>
2026-05-23 22:57:00 +02:00
36568d3eee fix: propagate CreateTicket.user_id to the persisted ticket row
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>
2026-05-23 21:10:33 +02:00
902bafe7f2 feat: POST /tickets/{event_id}/{payment_hash} polling endpoint
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>
2026-05-23 21:06:03 +02:00
ced6ca2b2b feat: organizer-side "Republish mine" button + scoped endpoint
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>
2026-05-23 21:02:36 +02:00
fa2a6e40f0 feat(ui): "Republish all" button on the admin Settings card
Surfaces the POST /republish-all endpoint added in the previous
commit. Lives in the existing admin-gated Settings card on the
events extension landing page, so the LNbits operator can trigger
the migration without curl + access tokens.

Confirm dialog before firing (the endpoint emits one Nostr event
per approved row, fine to retry but worth a click of friction).
Notification shows the republished/total count on success.

Self-closing tags expanded per the LNbits UMD rule
(webapp CLAUDE.md > LNbits + Quasar UMD gotchas) — q-separator
and q-btn would silently nest wrong otherwise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:00:30 +02:00
05593c9c3c feat: POST /republish-all admin endpoint
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>
2026-05-23 20:58:20 +02:00
b0d089d3c9 feat: also publish allow_fiat + fiat_currency in NIP-52 tags
Some checks failed
lint.yml / feat: also publish allow_fiat + fiat_currency in NIP-52 tags (pull_request) Failing after 0s
lint.yml / feat: also publish allow_fiat + fiat_currency in NIP-52 tags (push) Failing after 0s
The buyer-side webapp Purchase button needs allow_fiat to know
whether to surface the fiat method, and fiat_currency for the
conversion-preview label. Without these in the published Nostr
event, the buyer would either have to REST-fetch the LNbits event
again (defeats the inventory-sync goal) or guess.

Same backwards-compat reasoning as the four counter tags — tags
are AIO additions outside the NIP-52 spec; unknown tags are
ignored by spec-compliant clients.

- tickets_allow_fiat: "true" when the organizer enabled the fiat
  toggle. Omitted otherwise so the on-the-wire payload stays
  small for the common Lightning-only case.
- tickets_fiat_currency: only emitted when allow_fiat is on
  (otherwise it'd be ambiguous what the value represents).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:37:19 +02:00
edf1493e0c feat: publish ticket counts in NIP-52 tags + republish on sale
Some checks failed
lint.yml / feat: publish ticket counts in NIP-52 tags + republish on sale (pull_request) Failing after 0s
Inventory sync over Nostr, mirroring how nostrmarket republishes
kind 30018 product events when stock changes. Connected webapp /
other-client subscriptions pick up the new state via their existing
relay subscription — no REST polling needed.

build_nip52_event grows four AIO custom tags on every published
kind 31922/31923 event:
- tickets_available — current remaining (omitted when amount_tickets
  is 0, the schema's "unlimited" sentinel, so clients can tell the
  difference between unlimited and sold-out)
- tickets_sold — running count, always emitted (clients derive
  original_capacity = available + sold for progress bars)
- tickets_price — price_per_ticket (0 means free)
- tickets_currency — the currency string

Tags are AIO additions outside the NIP-52 spec; spec-compliant
clients MUST ignore unknown tags so this stays backwards-compatible.

set_ticket_paid calls publish_or_delete_nostr_event after the
counter update so the new state lands on relays. The whole sequence
(counter update + republish) is wrapped in a per-event-id asyncio
lock to address the existing # todo: lock and to ensure two paid
invoices for the same event can't reorder the published state.

Failures inside the Nostr publish are logged + swallowed by the
existing wrapper, so a relay outage can never break the payment
flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:31:56 +02:00
814581f307 feat: expose GET /tickets/user/{user_id} endpoint
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>
2026-05-23 20:30:03 +02:00
27cc8d2f1c chore: rebase onto upstream v1.6.1 + bump to v1.6.1-aio.1
Some checks failed
lint.yml / chore: rebase onto upstream v1.6.1 + bump to v1.6.1-aio.1 (push) Failing after 0s
v1.6.1-aio.1
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>
2026-05-22 09:24:35 +02:00
b576a490d2 refactor: move fork-only migrations to migrations_fork.py
`migrations.py` now matches upstream v1.3.0 exactly. Every aio-only
schema delta (the old m007-m011: user_id, status, nostr_event_id +
created_at, settings table, location + categories) moves into a
single `m001_aio_event_schema` function in `migrations_fork.py`,
tracked under `events_fork` in `dbversions` by the loader added in
aiolabs/lnbits@ae997181.

Idempotency guards on every ADD COLUMN / CREATE TABLE let the
squashed migration no-op cleanly on dev DBs that already ran the
old m007-m011 — schema lands identical from either path.

Why now: aiolabs/lnbits#8. We're about to rebase events onto
upstream v1.6.1 which adds its own m007_add_allow_fiat. With this
move done first, migrations.py stays a fast-forward on rebase and
our fork-only schema lives in a separate file that never collides.

Requires aiolabs/lnbits @ ae997181 or later for the extension_fork
loader. Running on an upstream lnbits without the loader patch
will NOT apply the fork schema — but the aiolabs deploy fleet
already pulls from aiolabs/lnbits, so this is the only host we
ship to.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:22:46 +02:00
16eb68d080 feat: public auto_approve probe + bump to v1.3.0-aio.5
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>
2026-05-22 09:22:34 +02:00
0dc2dcc35f fix: gate event edits through the approval workflow
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>
2026-05-22 09:22:16 +02:00
df4775126f feat: support optional start/end time on events
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>
2026-05-22 09:21:30 +02:00
6aa280680e feat: add NIP-52 Nostr publish + sync of calendar events
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>
2026-05-22 09:20:00 +02:00
c7e95c5452 feat: add event approval workflow with admin UI
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>
2026-05-22 09:19:21 +02:00
dfabcb8f54 feat: support optional user_id ticket identifier
Add an alternative ticket identifier scheme: instead of (name, email),
external integrations can issue tickets bound to an LNbits user_id.

- m007 adds the user_id column on events.ticket
- CreateTicket validator enforces exactly one identifier scheme per ticket
- Ticket / PublicTicket: name, email, user_id all Optional
- _parse_ticket_row reverses the empty-string sentinel used to keep the
  NOT NULL name/email columns satisfied when user_id is the identifier
- POST /tickets/{event_id} dispatches to _create_user_id_ticket vs
  _create_named_ticket based on the supplied identifier
- New GET /tickets/user/{user_id} returns tickets for a given user

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:17:04 +02:00
dni ⚡
4bf867eef0
feat: add resend email button to ticket list (#51)
- resending only possible when ticket is paid.
2026-05-13 11:30:14 +02:00
Arc
6768b78c6f Custom subject and body 2026-05-08 19:14:07 +01:00
dni ⚡
0824b1120b
feat: add paid/registered badge to ticket page (#49)
some visual verification on the ticket page that it is paid / checked
in.
2026-05-07 17:06:38 +02:00
Arc
32c230957e fix: if sats and fiat checkout conversion currency 2026-05-07 14:34:22 +01:00
Arc
680b035ec9
feat: add fiat checkout and nostr + email notification (#50)
* feat: fiat and email/nostr notifications

* make n bake
2026-05-07 12:31:32 +01:00
dni ⚡
4afc78d44d
feat: register public page saves to localstorage (#48)
* 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
2026-05-05 10:45:14 +02:00
Tiago Vasconcelos
9e477ac959
feat: make events dynamic (#43)
---------

Co-authored-by: dni <office@dnilabs.com>
2026-05-04 17:01:53 +02:00
dni ⚡
f06bd9a668
chore: prepare release, fix lint and uv warnings (#44)
Some checks failed
/ release (push) Has been cancelled
/ pullrequest (push) Has been cancelled
v1.2.1
2026-04-15 17:37:34 +02:00
PatMulligan
78433a7d85
Fix: SQLite migration syntax error in m006 (#41)
Ran into this issue on my lnbits 1.4 on NixOS using the flake

- Fix m006_add_extra_fields migration that fails on SQLite with syntax error
- Split multi-column ALTER TABLE into separate statements (SQLite doesn't support adding multiple columns in one statement)
2026-04-15 17:30:34 +02:00
DoktorShift
1dd6f8b67e
docs: changes to more pages (#42)
* Changes to more pages
* Update description.md

---------

Co-authored-by: dni  <office@dnilabs.com>
2026-01-28 17:22:09 +01:00
Tiago Vasconcelos
42de6d4791
feat: add promo codes and conditional events (#40)
Some checks failed
/ release (push) Has been cancelled
/ pullrequest (push) Has been cancelled
v1.2.0
* add extra column
* add conditional events
* refunds
* conditional events working
* adding promo codes
* promo codes logic

---------

Co-authored-by: dni  <office@dnilabs.com>
2025-12-09 10:48:00 +00:00
arbadacarba
ee70c300f6
Fix typos (#39) 2025-12-09 10:28:48 +01:00
Tiago Vasconcelos
ae827a6545
fix: QR copy button (#38)
Some checks failed
/ release (push) Has been cancelled
/ pullrequest (push) Has been cancelled
v1.1.0
* fix QR copy button
* fixup poetry
* rc6 and chore update

---------

Co-authored-by: dni  <office@dnilabs.com>
2025-09-04 07:10:49 +02:00
Tiago Vasconcelos
7aeba1eeb4
Update to use uv (#37)
---------

Co-authored-by: dni  <office@dnilabs.com>
2025-08-22 16:54:51 +02:00
dni ⚡
c729ef17a6
fix: 1.0.0-rc5
Some checks failed
/ release (push) Has been cancelled
/ pullrequest (push) Has been cancelled
v1.0.0
2024-10-22 10:49:52 +02:00
dni ⚡
6714dcddc7
feat: update to lnbits 1.0.0 (#36) 2024-10-11 13:52:39 +02:00
dni ⚡
9ca714d878
fix: fetch incoming payment (#35)
did not work for internal payment
2024-09-03 16:35:42 +02:00
dni ⚡
400b39211d
feat: code quality (#34)
* feat: code quality
2024-08-29 12:18:49 +02:00
Arc
3df2a56ca2
Merge pull request #30 from lnbits/advanceddescription
added video
2024-05-17 17:39:51 +01:00
benarc
ea3a60ecd4 Added video 2024-05-17 17:39:11 +01:00
benarc
57f40b9790 Merge remote-tracking branch 'origin/main' into advanceddescription 2024-05-17 17:38:28 +01:00
Vlad Stan
9c82d9e2df chore: bump min_lnbits_version
Some checks failed
/ release (push) Has been cancelled
/ pullrequest (push) Has been cancelled
v0.2.0
2024-05-14 11:37:27 +03:00
Arc
c24f5ddb84
Merge pull request #29 from lnbits/advanceddescription
Added extended description
2024-05-06 12:42:45 +01:00
Tiago Vasconcelos
082f5e7488
Check payment (#28)
Hotfix the check payment when using fiat tickets
2024-05-06 12:41:35 +01:00
benarc
1b1cf72e17 Added extended description 2024-04-30 15:36:05 +01:00
dni ⚡
b985304384
fix: properly start/stop tasks (#27)
https://github.com/lnbits/lnbits/issues/2411
2024-04-15 12:53:16 +02:00
Tiago Vasconcelos
662587dbf2
fix timestamp on postgres (#26)
Some checks failed
/ release (push) Has been cancelled
/ pullrequest (push) Has been cancelled
v0.1.6
2024-03-07 12:06:59 +02:00
Arc
4f5fe8035d
Merge pull request #25 from lnbits/add_image_banner
Some checks failed
/ release (push) Has been cancelled
/ pullrequest (push) Has been cancelled
v0.1.5
allow for an image banner
2024-02-17 14:05:58 +00:00
benarc
38951a7ebe Merge branch 'main' into add_image_banner 2024-02-17 14:04:42 +00:00
Tiago Vasconcelos
07d2f59bc3
Refactor events extensions (#21)
Some checks failed
/ release (push) Has been cancelled
/ pullrequest (push) Has been cancelled
v0.1.4
* require admin key
* remove log
* only show paid tickets
* purge tickets
2024-02-05 08:50:39 +01:00
Tiago Vasconcelos
ae8930f884 allow for an image banner 2024-01-31 13:15:52 +00:00
Tiago Vasconcelos
f468183631
Sanitize/Validate name field (#20)
* 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>
2024-01-26 14:30:14 +00:00