openapi: 3.1.0
info:
  title: we-fly API
  version: "1.0.0"
  description: >
    OAuth 2.0–protected REST API for uploading IGC paragliding flights
    and reading pilot data on we-fly. See https://we-fly.cloud/doc/api
    for narrative documentation.
  contact:
    name: we-fly partner support
    url: https://github.com/Djang0/we-fly/issues
servers:
  - url: https://we-fly.cloud
    description: Production
paths:
  /.well-known/oauth-authorization-server:
    get:
      summary: RFC 8414 authorization-server metadata
      tags: [discovery]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
  /api/v1:
    get:
      summary: API discovery
      tags: [discovery]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
  /api/oauth/token:
    post:
      summary: OAuth 2.0 token endpoint
      tags: [oauth]
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                grant_type:
                  type: string
                  enum:
                    - client_credentials
                    - authorization_code
                    - refresh_token
                    - urn:ietf:params:oauth:grant-type:device_code
                client_id:
                  type: string
                client_secret:
                  type: string
                code:
                  type: string
                redirect_uri:
                  type: string
                code_verifier:
                  type: string
                refresh_token:
                  type: string
                device_code:
                  type: string
                scope:
                  type: string
              required: [grant_type]
      responses:
        "200":
          description: Token issued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TokenResponse"
        "400":
          $ref: "#/components/responses/OAuthError"
        "401":
          $ref: "#/components/responses/OAuthError"
  /api/oauth/device_authorization:
    post:
      summary: RFC 8628 device authorization endpoint
      tags: [oauth]
      description: >
        Start a device-code flow. Returns a long opaque `device_code`
        the device polls /token with, and a short human-readable
        `user_code` the user types into `/oauth/device`.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                client_id:
                  type: string
                client_secret:
                  type: string
                scope:
                  type: string
      responses:
        "200":
          description: Device code issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  device_code: { type: string }
                  user_code: { type: string }
                  verification_uri: { type: string, format: uri }
                  verification_uri_complete: { type: string, format: uri }
                  expires_in: { type: integer }
                  interval: { type: integer }
                required:
                  - device_code
                  - user_code
                  - verification_uri
                  - expires_in
                  - interval
        "400":
          $ref: "#/components/responses/OAuthError"
        "401":
          $ref: "#/components/responses/OAuthError"
  /api/oauth/revoke:
    post:
      summary: RFC 7009 token revocation
      tags: [oauth]
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                token:
                  type: string
                token_type_hint:
                  type: string
                  enum: [access_token, refresh_token]
              required: [token]
      responses:
        "200":
          description: Always returned, per RFC
  /api/oauth/introspect:
    post:
      summary: RFC 7662 token introspection
      tags: [oauth]
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                token:
                  type: string
                token_type_hint:
                  type: string
              required: [token]
      responses:
        "200":
          description: Introspection result
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    properties:
                      active:
                        type: boolean
                        enum: [false]
                  - type: object
                    properties:
                      active: { type: boolean, enum: [true] }
                      scope: { type: string }
                      client_id: { type: string }
                      token_type: { type: string }
                      sub: { type: string }
                      exp: { type: integer }
                      iat: { type: integer }
  /api/v1/flights:
    get:
      summary: List the authenticated user's flights
      tags: [flights]
      security: [{ bearerAuth: [flights:read] }]
      parameters:
        - in: query
          name: page
          schema: { type: integer, minimum: 1, default: 1 }
        - in: query
          name: pageSize
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlightList"
        "401":
          $ref: "#/components/responses/BearerError"
        "403":
          $ref: "#/components/responses/BearerError"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/flights/{id}:
    get:
      summary: Flight detail
      tags: [flights]
      security: [{ bearerAuth: [flights:read] }]
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            pattern: "^[a-f0-9]{24}$"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlightDetail"
        "401":
          $ref: "#/components/responses/BearerError"
        "403":
          $ref: "#/components/responses/BearerError"
        "404":
          $ref: "#/components/responses/Error"
    delete:
      summary: Delete a flight
      tags: [flights]
      security: [{ bearerAuth: [flights:delete] }]
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            pattern: "^[a-f0-9]{24}$"
      responses:
        "204":
          description: Deleted
        "401":
          $ref: "#/components/responses/BearerError"
        "403":
          $ref: "#/components/responses/BearerError"
        "404":
          $ref: "#/components/responses/Error"
  /api/v1/flights/upload:
    post:
      summary: Upload an IGC file
      tags: [flights]
      security: [{ bearerAuth: [flights:write] }]
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        "201":
          description: New flight created
        "200":
          description: Duplicate — flight already existed
        "400":
          $ref: "#/components/responses/Error"
        "413":
          $ref: "#/components/responses/Error"
        "422":
          $ref: "#/components/responses/Error"
  /api/v1/profile:
    get:
      summary: The authenticated user's profile
      description: >
        Requires the `profile:read` scope. The `email` field is only
        populated when the token also carries `profile:email`;
        otherwise it is returned as null.
      tags: [profile]
      security: [{ bearerAuth: [profile:read] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProfileResponse"
        "401":
          $ref: "#/components/responses/BearerError"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: opaque
  schemas:
    TokenResponse:
      type: object
      properties:
        access_token: { type: string }
        token_type: { type: string, enum: [Bearer] }
        expires_in: { type: integer }
        refresh_token: { type: string }
        refresh_token_expires_in: { type: integer }
        scope: { type: string }
      required: [access_token, token_type, expires_in, scope]
    Flight:
      type: object
      properties:
        id: { type: string }
        hash: { type: string }
        flight_date: { type: string, format: date }
        date_created: { type: string, format: date-time }
        original_file_name: { type: string }
        is_tandem: { type: boolean }
        launch_type:
          type: string
          enum: [Self, Towed, Powered, Driving]
        season: { type: integer }
        scoring:
          type: object
          properties:
            distance: { type: number }
            score: { type: number }
            type: { type: string }
        flightInfo:
          type: object
          properties:
            takeoff:
              type: object
              properties:
                id: { type: string }
                lat: { type: number }
                lng: { type: number }
                name: { type: string }
                country: { type: string }
            metadata:
              type: object
              additionalProperties: true
            track:
              type: object
              properties:
                duration: { type: number }
                distance: { type: number }
    FlightList:
      type: object
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Flight" }
        pagination:
          type: object
          properties:
            page: { type: integer }
            pageSize: { type: integer }
            totalCount: { type: integer }
            totalPages: { type: integer }
    FlightDetail:
      type: object
      properties:
        data: { $ref: "#/components/schemas/Flight" }
    ProfileResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            id: { type: string }
            name: { type: string, nullable: true }
            email: { type: string, nullable: true, description: "Null unless the token carries the profile:email scope." }
            image: { type: string, nullable: true }
            publicProfile: { type: boolean }
            emailVerified: { type: boolean }
            earlyAdopter: { type: boolean }
            club: { type: string, nullable: true }
            civlId: { type: integer, nullable: true }
    Error:
      type: object
      properties:
        error: { type: string }
        error_description: { type: string }
    WebhookEnvelope:
      type: object
      description: >
        Common envelope sent to every webhook URL. Verify the
        `X-We-Fly-Signature` header (HMAC-SHA256) before trusting it.
      properties:
        event:
          type: string
          enum: [flight.uploaded, flight.deleted]
        occurredAt: { type: string, format: date-time }
        user:
          type: object
          properties:
            id: { type: string }
        flight: { type: object }
      required: [event, occurredAt, user, flight]
    WebhookFlightUploaded:
      allOf:
        - $ref: "#/components/schemas/WebhookEnvelope"
        - type: object
          properties:
            event:
              type: string
              enum: [flight.uploaded]
            flight:
              type: object
              properties:
                hash: { type: string }
                date: { type: string, format: date, nullable: true }
                distance: { type: number, nullable: true }
                score: { type: number, nullable: true }
                type: { type: string, nullable: true }
                gliderType: { type: string, nullable: true }
                durationSeconds: { type: number, nullable: true }
    WebhookFlightDeleted:
      allOf:
        - $ref: "#/components/schemas/WebhookEnvelope"
        - type: object
          properties:
            event:
              type: string
              enum: [flight.deleted]
            flight:
              type: object
              properties:
                id: { type: string }
                hash: { type: string, nullable: true }
  responses:
    Error:
      description: Error response
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    OAuthError:
      description: OAuth error response (RFC 6749 §5.2)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    BearerError:
      description: RFC 6750 bearer-auth error
      headers:
        WWW-Authenticate:
          schema: { type: string }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema: { type: integer }
        X-RateLimit-Remaining:
          schema: { type: integer }
        X-RateLimit-Reset:
          schema: { type: integer }
      content:
        application/json:
          schema:
            type: object
            properties:
              success: { type: boolean }
              error: { type: string }
              retryAfter: { type: integer }
