Ticket creation never checks the closing date — tickets sellable after the window shuts #60

Open
opened 2026-09-27 20:31:11 +00:00 by padreug · 0 comments
Owner

api_ticket_create gates on status, canceled and capacity. It does not check closing_date. The window enforcement lives only in api_get_event (views_api.py:283-298), the public detail endpoint:

is_window_open = datetime.now(timezone.utc) < closing_dt
...
if not is_window_open:
    raise HTTPException(status_code=HTTPStatus.GONE,
                        detail="Ticket closing date has passed.")

Nothing equivalent exists in the ticket endpoint — grep -n "closing_date\|is_window_open" views_api.py returns hits only in api_get_event and in create/update field handling.

So the UI hides the buy button (it reads api_get_event, which 410s), but a direct POST to the unauthenticated ticket endpoint goes straight through to pricing and ticket creation.

Observed

Found on aio-demo while probing the #59 fix. KVhCGUrGFP6UyBuEaRA5Zd ("Tech Meetup") has closing_date = 2026-05-22T12:00:00+02:00 — four months past:

POST /events/api/v1/tickets/KVhCGUrGFP6UyBuEaRA5Zd  {"quantity": 10}
→ HTTP 400  "Only 9 ticket(s) remaining for this event."

It reached the capacity check, which is downstream of where a window check would sit. A request for a quantity within capacity would have proceeded to create the ticket. I didn't run that — no reason to mint a real ticket to confirm what the control flow already shows.

Why it matters

  • Money taken for an event that has already happened, or whose sales deliberately closed.
  • closing_date is also what the conditional-event logic keys on: api_get_event cancels the event and refunds when conditional and not is_min_tickets_met and not is_window_open. A sale landing after that has run is a ticket for a cancelled event.
  • The endpoint takes no auth, so this needs no account — see #38, which covers anonymous abuse of the same endpoint from a different angle.

Fix

Extract the window computation from api_get_event into a helper and call it from both. Worth handling the same three-way fallback (closing_date or event_end_date or event_start_date) and the two date formats in one place rather than duplicating them — the parsing already has a fromisoformat / strptime fallback that shouldn't be written twice.

Upstream has no equivalent guard in its ticket endpoint either (v1.6.8's api_ticket_create checks canceled and amount_tickets < 1 only), so this is a deliberate fork addition rather than a rebase conflict — worth a row in docs/upstream-candidates.md, since it looks like something upstream would want too.

Deploy note

Existing instances have live events with past closing dates (demo has at least three). Adding the guard makes those correctly unsellable; no data change needed.

`api_ticket_create` gates on status, `canceled` and capacity. It does **not** check `closing_date`. The window enforcement lives only in `api_get_event` (`views_api.py:283-298`), the public *detail* endpoint: ```python is_window_open = datetime.now(timezone.utc) < closing_dt ... if not is_window_open: raise HTTPException(status_code=HTTPStatus.GONE, detail="Ticket closing date has passed.") ``` Nothing equivalent exists in the ticket endpoint — `grep -n "closing_date\|is_window_open" views_api.py` returns hits only in `api_get_event` and in create/update field handling. So the UI hides the buy button (it reads `api_get_event`, which 410s), but a direct POST to the unauthenticated ticket endpoint goes straight through to pricing and ticket creation. ## Observed Found on aio-demo while probing the #59 fix. `KVhCGUrGFP6UyBuEaRA5Zd` ("Tech Meetup") has `closing_date = 2026-05-22T12:00:00+02:00` — four months past: ``` POST /events/api/v1/tickets/KVhCGUrGFP6UyBuEaRA5Zd {"quantity": 10} → HTTP 400 "Only 9 ticket(s) remaining for this event." ``` It reached the *capacity* check, which is downstream of where a window check would sit. A request for a quantity within capacity would have proceeded to create the ticket. I didn't run that — no reason to mint a real ticket to confirm what the control flow already shows. ## Why it matters - Money taken for an event that has already happened, or whose sales deliberately closed. - `closing_date` is also what the **conditional-event** logic keys on: `api_get_event` cancels the event and refunds when `conditional and not is_min_tickets_met and not is_window_open`. A sale landing after that has run is a ticket for a cancelled event. - The endpoint takes no auth, so this needs no account — see #38, which covers anonymous abuse of the same endpoint from a different angle. ## Fix Extract the window computation from `api_get_event` into a helper and call it from both. Worth handling the same three-way fallback (`closing_date or event_end_date or event_start_date`) and the two date formats in one place rather than duplicating them — the parsing already has a `fromisoformat` / `strptime` fallback that shouldn't be written twice. Upstream has no equivalent guard in its ticket endpoint either (v1.6.8's `api_ticket_create` checks `canceled` and `amount_tickets < 1` only), so this is a deliberate fork addition rather than a rebase conflict — worth a row in `docs/upstream-candidates.md`, since it looks like something upstream would want too. ## Deploy note Existing instances have live events with past closing dates (demo has at least three). Adding the guard makes those correctly unsellable; no data change needed.
Sign in to join this conversation.
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#60
No description provided.