openapi: 3.1.0
info:
  title: fete API
  description: Privacy-focused event announcements and RSVPs
  version: 0.1.0
  license:
    name: GPL-3.0-or-later
    identifier: GPL-3.0-or-later

servers:
  - url: /api

paths:
  /events:
    post:
      operationId: createEvent
      summary: Create a new event
      tags:
        - events
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEventRequest"
      responses:
        "201":
          description: Event created successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateEventResponse"
        "400":
          description: Validation failed
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ValidationProblemDetail"

  /events/{eventToken}/rsvps/{rsvpToken}:
    delete:
      operationId: cancelRsvp
      summary: Cancel RSVP
      description: |
        Permanently deletes an RSVP identified by the RSVP token.
        Idempotent: returns 204 whether the RSVP existed or not.
      tags:
        - events
      parameters:
        - name: eventToken
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Event token (UUID)
        - name: rsvpToken
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: RSVP token (UUID) identifying the attendance to cancel
      responses:
        "204":
          description: >
            RSVP successfully cancelled (or was already cancelled).
            No response body.
        "500":
          description: Internal server error

  /events/{eventToken}/rsvps:
    post:
      operationId: createRsvp
      summary: Submit an RSVP for an event
      tags:
        - events
      parameters:
        - name: eventToken
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Public event token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateRsvpRequest"
      responses:
        "201":
          description: RSVP created successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateRsvpResponse"
        "400":
          description: Validation failed (e.g. blank name)
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ValidationProblemDetail"
        "404":
          description: Event not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetail"
        "409":
          description: Event has expired — RSVPs no longer accepted
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetail"

  /events/{eventToken}/attendees:
    get:
      operationId: getAttendees
      summary: Get attendee list for an event (organizer only)
      tags:
        - events
      parameters:
        - name: eventToken
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Public event token
        - name: organizerToken
          in: query
          required: true
          schema:
            type: string
            format: uuid
          description: Organizer token for authorization
      responses:
        "200":
          description: Attendee list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GetAttendeesResponse"
        "403":
          description: Invalid organizer token
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetail"
        "404":
          description: Event not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetail"

  /events/{eventToken}:
    get:
      operationId: getEvent
      summary: Get public event details by token
      tags:
        - events
      parameters:
        - name: eventToken
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Public event token
      responses:
        "200":
          description: Event found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GetEventResponse"
        "404":
          description: Event not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetail"

    patch:
      operationId: patchEvent
      summary: Update an event (currently cancel)
      description: |
        Partial update of an event resource. Currently the only supported operation
        is cancellation (setting cancelled to true). Requires the organizer token.
        Cancellation is irreversible.
      tags:
        - events
      parameters:
        - name: eventToken
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Public event token
        - name: organizerToken
          in: query
          required: true
          schema:
            type: string
            format: uuid
          description: Organizer token for authorization
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PatchEventRequest"
      responses:
        "204":
          description: Event updated successfully
        "403":
          description: Invalid organizer token
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetail"
        "404":
          description: Event not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetail"
        "409":
          description: Event is already cancelled
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetail"

components:
  schemas:
    CreateEventRequest:
      type: object
      required:
        - title
        - dateTime
        - timezone
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
          maxLength: 2000
        dateTime:
          type: string
          format: date-time
          description: Event date and time with UTC offset (ISO 8601)
          example: "2026-03-15T20:00:00+01:00"
        timezone:
          type: string
          description: IANA timezone of the organizer
          example: "Europe/Berlin"
        location:
          type: string
          maxLength: 500

    CreateEventResponse:
      type: object
      required:
        - eventToken
        - organizerToken
        - title
        - dateTime
        - timezone
      properties:
        eventToken:
          type: string
          format: uuid
          description: Public token for the event URL
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        organizerToken:
          type: string
          format: uuid
          description: Secret token for organizer access
          example: "f9e8d7c6-b5a4-3210-fedc-ba9876543210"
        title:
          type: string
          example: "Summer BBQ"
        dateTime:
          type: string
          format: date-time
          example: "2026-03-15T20:00:00+01:00"
        timezone:
          type: string
          description: IANA timezone of the organizer
          example: "Europe/Berlin"

    GetEventResponse:
      type: object
      required:
        - eventToken
        - title
        - dateTime
        - timezone
        - attendeeCount
        - cancelled
      properties:
        eventToken:
          type: string
          format: uuid
          description: Public event token
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        title:
          type: string
          description: Event title
          example: "Summer BBQ"
        description:
          type: string
          description: Event description (absent if not set)
          example: "Bring your own drinks!"
        dateTime:
          type: string
          format: date-time
          description: Event date/time with organizer's UTC offset
          example: "2026-03-15T20:00:00+01:00"
        timezone:
          type: string
          description: IANA timezone name of the organizer
          example: "Europe/Berlin"
        location:
          type: string
          description: Event location (absent if not set)
          example: "Central Park, NYC"
        attendeeCount:
          type: integer
          minimum: 0
          description: Number of confirmed attendees (attending=true)
          example: 12
        cancelled:
          type: boolean
          description: Whether the event has been cancelled
          example: false
        cancellationReason:
          type:
            - string
            - "null"
          description: Reason for cancellation, if provided
          example: null

    PatchEventRequest:
      type: object
      required:
        - cancelled
      properties:
        cancelled:
          type: boolean
          description: Set to true to cancel the event (irreversible)
          example: true
        cancellationReason:
          type: string
          maxLength: 2000
          description: Optional cancellation reason
          example: "Unfortunately the venue is no longer available."

    CreateRsvpRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          description: Guest's display name
          example: "Max Mustermann"

    CreateRsvpResponse:
      type: object
      required:
        - rsvpToken
        - name
      properties:
        rsvpToken:
          type: string
          format: uuid
          description: Token identifying this RSVP (store client-side for future updates)
          example: "d4e5f6a7-b8c9-0123-4567-890abcdef012"
        name:
          type: string
          description: Guest's display name as stored
          example: "Max Mustermann"

    GetAttendeesResponse:
      type: object
      required:
        - attendees
      properties:
        attendees:
          type: array
          items:
            $ref: "#/components/schemas/Attendee"
          example:
            - name: "Alice"
            - name: "Bob"

    Attendee:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          example: "Alice"

    ProblemDetail:
      type: object
      properties:
        type:
          type: string
          format: uri
          default: "about:blank"
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
          format: uri
      additionalProperties: true

    ValidationProblemDetail:
      allOf:
        - $ref: "#/components/schemas/ProblemDetail"
        - type: object
          properties:
            fieldErrors:
              type: array
              items:
                type: object
                required:
                  - field
                  - message
                properties:
                  field:
                    type: string
                  message:
                    type: string
