diff --git a/app.py b/app.py index b1fdbb1..f7d0233 100644 --- a/app.py +++ b/app.py @@ -128,10 +128,6 @@ def create_app() -> "Quart": from bp.ticket_admin.routes import register as register_ticket_admin app.register_blueprint(register_ticket_admin()) - # Internal API (server-to-server, CSRF-exempt) - from events_api import register as register_events_api - app.register_blueprint(register_events_api()) - return app diff --git a/events_api.py b/events_api.py deleted file mode 100644 index 2e93186..0000000 --- a/events_api.py +++ /dev/null @@ -1,198 +0,0 @@ -""" -Internal JSON API for the events app. - -These endpoints are called by other apps (cart) over HTTP. -They are CSRF-exempt because they are server-to-server calls. -""" -from __future__ import annotations - -from quart import Blueprint, g, request, jsonify -from sqlalchemy import select, update, func -from sqlalchemy.orm import selectinload - -from models.calendars import CalendarEntry, Calendar, Ticket -from shared.browser.app.csrf import csrf_exempt - - -def register() -> Blueprint: - bp = Blueprint("events_api", __name__, url_prefix="/internal/events") - - @bp.get("/calendar-entries") - @csrf_exempt - async def calendar_entries(): - """ - Return pending calendar entries for a user/session. - Used by the cart app to display calendar items in the cart. - - Query params: user_id, session_id, state (default: pending) - """ - user_id = request.args.get("user_id", type=int) - session_id = request.args.get("session_id") - state = request.args.get("state", "pending") - - filters = [ - CalendarEntry.deleted_at.is_(None), - CalendarEntry.state == state, - ] - if user_id is not None: - filters.append(CalendarEntry.user_id == user_id) - elif session_id: - filters.append(CalendarEntry.session_id == session_id) - else: - return jsonify([]) - - result = await g.s.execute( - select(CalendarEntry) - .where(*filters) - .options(selectinload(CalendarEntry.calendar)) - .order_by(CalendarEntry.start_at.asc()) - ) - entries = result.scalars().all() - - return jsonify([ - { - "id": e.id, - "name": e.name, - "cost": float(e.cost) if e.cost else 0, - "state": e.state, - "start_at": e.start_at.isoformat() if e.start_at else None, - "end_at": e.end_at.isoformat() if e.end_at else None, - "calendar_name": e.calendar.name if e.calendar else None, - "calendar_slug": e.calendar.slug if e.calendar else None, - } - for e in entries - ]) - - @bp.post("/adopt") - @csrf_exempt - async def adopt(): - """ - Adopt anonymous calendar entries for a user. - Called by the cart app after login. - - Body: {"user_id": int, "session_id": str} - """ - data = await request.get_json() or {} - user_id = data.get("user_id") - session_id = data.get("session_id") - - if not user_id or not session_id: - return jsonify({"ok": False, "error": "user_id and session_id required"}), 400 - - # Soft-delete existing user entries - await g.s.execute( - update(CalendarEntry) - .where( - CalendarEntry.deleted_at.is_(None), - CalendarEntry.user_id == user_id, - ) - .values(deleted_at=func.now()) - ) - - # Adopt anonymous entries - cal_result = await g.s.execute( - select(CalendarEntry).where( - CalendarEntry.deleted_at.is_(None), - CalendarEntry.session_id == session_id, - ) - ) - for entry in cal_result.scalars().all(): - entry.user_id = user_id - - return jsonify({"ok": True}) - - @bp.get("/entry/") - @csrf_exempt - async def entry_detail(entry_id: int): - """ - Return entry details for order display. - Called by the cart app when showing order items. - """ - result = await g.s.execute( - select(CalendarEntry) - .where(CalendarEntry.id == entry_id) - .options(selectinload(CalendarEntry.calendar)) - ) - entry = result.scalar_one_or_none() - - if not entry: - return jsonify(None), 404 - - return jsonify({ - "id": entry.id, - "name": entry.name, - "cost": float(entry.cost) if entry.cost else 0, - "state": entry.state, - "start_at": entry.start_at.isoformat() if entry.start_at else None, - "end_at": entry.end_at.isoformat() if entry.end_at else None, - "calendar_name": entry.calendar.name if entry.calendar else None, - "calendar_slug": entry.calendar.slug if entry.calendar else None, - }) - - @bp.get("/tickets") - @csrf_exempt - async def tickets(): - """ - Return tickets for a user/session. - Query params: user_id, session_id, order_id, state - """ - user_id = request.args.get("user_id", type=int) - session_id = request.args.get("session_id") - order_id = request.args.get("order_id", type=int) - state = request.args.get("state") - - filters = [] - if order_id is not None: - filters.append(Ticket.order_id == order_id) - elif user_id is not None: - filters.append(Ticket.user_id == user_id) - elif session_id: - filters.append(Ticket.session_id == session_id) - else: - return jsonify([]) - - if state: - filters.append(Ticket.state == state) - - result = await g.s.execute( - select(Ticket) - .where(*filters) - .options( - selectinload(Ticket.entry).selectinload(CalendarEntry.calendar), - selectinload(Ticket.ticket_type), - ) - .order_by(Ticket.created_at.desc()) - ) - tix = result.scalars().all() - - return jsonify([ - { - "id": t.id, - "code": t.code, - "state": t.state, - "entry_name": t.entry.name if t.entry else None, - "entry_start_at": t.entry.start_at.isoformat() if t.entry and t.entry.start_at else None, - "calendar_name": t.entry.calendar.name if t.entry and t.entry.calendar else None, - "ticket_type_name": t.ticket_type.name if t.ticket_type else None, - "ticket_type_cost": float(t.ticket_type.cost) if t.ticket_type and t.ticket_type.cost else None, - "checked_in_at": t.checked_in_at.isoformat() if t.checked_in_at else None, - } - for t in tix - ]) - - @bp.post("/tickets//checkin") - @csrf_exempt - async def checkin(code: str): - """ - Check in a ticket by code. - Used by admin check-in interface. - """ - from .bp.tickets.services.tickets import checkin_ticket - - success, error = await checkin_ticket(g.s, code) - if not success: - return jsonify({"ok": False, "error": error}), 400 - - return jsonify({"ok": True}) - - return bp diff --git a/shared b/shared index 7ee8638..e83df2f 160000 --- a/shared +++ b/shared @@ -1 +1 @@ -Subproject commit 7ee8638d6e41de1f58aadd1f108cd7de8e920d07 +Subproject commit e83df2f742c8b64e7b4a6bc218fd1bd0a9637d21