码桶

发现社区成员的开源项目

main
api-docs.yaml80.4 KB
openapi: 3.0.3
info:
  title: CF DNS Manager API
  description: |
    REST API for CF DNS Manager, a Cloudflare DNS management application
    deployed on Cloudflare Pages with Functions. Supports both client-mode
    (direct Cloudflare API token) and server-mode (JWT-based authentication
    with managed tokens stored in KV).

    ## Authentication Modes

    - **Client Mode**: Pass a Cloudflare API token directly via the
      `X-Cloudflare-Token` header. No login required.
    - **Server Mode**: Authenticate with username/password via `/api/login` to
      receive a JWT access token (15 min) and a refresh token (7 days). Use the
      access token as a Bearer token in the `Authorization` header.

    ## Rate Limiting

    All endpoints are rate-limited per IP address using a sliding window. When
    the limit is exceeded, responses return HTTP 429 with a `Retry-After`
    header indicating seconds until the window resets.

    ## CSRF Protection

    All state-changing requests (`POST`, `PUT`, `PATCH`, `DELETE`) must include
    `Content-Type: application/json` (except `/api/zones/{zoneId}/dns_import`
    which also accepts multipart form data).
  version: 26.2.7
  license:
    name: MIT

servers:
  - url: /
    description: Same-origin (Cloudflare Pages deployment)

tags:
  - name: Authentication
    description: Login, registration, token refresh, logout, and TOTP verification
  - name: Passkeys
    description: WebAuthn/FIDO2 passkey registration and authentication
  - name: Zones
    description: Cloudflare zone listing
  - name: DNS Records
    description: CRUD operations on DNS records within a zone
  - name: DNS Bulk
    description: Bulk DNS operations across multiple zones
  - name: DNS History
    description: DNS snapshot and rollback
  - name: Custom Hostnames
    description: Cloudflare for SaaS custom hostname management
  - name: Fallback Origin
    description: Custom hostname fallback origin management
  - name: Search
    description: Global DNS record search across all zones
  - name: Admin - Users
    description: User management (admin only)
  - name: Admin - Settings
    description: Per-user token/account management (all authenticated users)
  - name: Admin - App Settings
    description: Application-level settings (admin only)
  - name: Admin - Audit Log
    description: Audit log viewing and management (admin only)
  - name: Account
    description: Self-service account management (password, TOTP)
  - name: Public
    description: Unauthenticated public endpoints

# ---------------------------------------------------------------------------
# Security Schemes
# ---------------------------------------------------------------------------
components:
  securitySchemes:
    BearerJWT:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT access token obtained from `/api/login`, `/api/verify-totp`,
        `/api/passkey/login-verify`, or `/api/refresh`. Expires in 15 minutes.
    CloudflareToken:
      type: apiKey
      in: header
      name: X-Cloudflare-Token
      description: |
        A valid Cloudflare API token passed directly by the client. When
        present, the server proxies requests to the Cloudflare API using this
        token (client mode).

  # -------------------------------------------------------------------------
  # Headers
  # -------------------------------------------------------------------------
  headers:
    RetryAfter:
      description: Seconds until the rate limit window resets
      schema:
        type: integer

  # -------------------------------------------------------------------------
  # Reusable Schemas
  # -------------------------------------------------------------------------
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          description: Human-readable error message
      required:
        - error

    Success:
      type: object
      properties:
        success:
          type: boolean
          example: true

    Account:
      type: object
      properties:
        id:
          type: integer
          description: Account slot index
        name:
          type: string
          description: Display name for the account

    LoginRequest:
      type: object
      properties:
        username:
          type: string
          description: Username (defaults to "admin" if omitted)
        password:
          type: string
          description: SHA-256 hash of the user password
      required:
        - password

    LoginResponse:
      type: object
      properties:
        token:
          type: string
          description: JWT access token (15 min expiry)
        refreshToken:
          type: string
          description: JWT refresh token (7 day expiry)
        accounts:
          type: array
          items:
            $ref: '#/components/schemas/Account'
        role:
          type: string
          enum: [admin, user]
        username:
          type: string

    LoginTOTPRequired:
      type: object
      properties:
        requiresTOTP:
          type: boolean
          example: true
        username:
          type: string
      description: Returned when the user has TOTP enabled and must complete a second authentication step.

    RegisterRequest:
      type: object
      properties:
        username:
          type: string
          description: Lowercase alphanumeric username (cannot be "admin")
        password:
          type: string
          description: SHA-256 hash of the desired password
      required:
        - username
        - password

    SetupAccountRequest:
      type: object
      properties:
        username:
          type: string
        setupToken:
          type: string
          description: One-time setup token provided by admin
        password:
          type: string
          description: SHA-256 hash of the desired password
      required:
        - username
        - setupToken
        - password

    VerifyTOTPRequest:
      type: object
      properties:
        username:
          type: string
        password:
          type: string
          description: SHA-256 hash of the user password
        code:
          type: string
          description: 6-digit TOTP code
      required:
        - password
        - code

    RefreshRequest:
      type: object
      properties:
        refreshToken:
          type: string
      required:
        - refreshToken

    RefreshResponse:
      type: object
      properties:
        token:
          type: string
          description: New JWT access token (15 min expiry)
        accounts:
          type: array
          items:
            $ref: '#/components/schemas/Account'
        role:
          type: string
          enum: [admin, user]
        username:
          type: string

    LogoutRequest:
      type: object
      properties:
        refreshToken:
          type: string
          description: Refresh token to revoke (optional)

    PublicSettings:
      type: object
      properties:
        openRegistration:
          type: boolean
          description: Whether self-registration is enabled

    CloudflareResponse:
      type: object
      description: Standard Cloudflare API v4 envelope
      properties:
        success:
          type: boolean
        errors:
          type: array
          items:
            type: object
            properties:
              code:
                type: integer
              message:
                type: string
        messages:
          type: array
          items:
            type: object
        result:
          description: Response payload (varies by endpoint)
        result_info:
          type: object
          properties:
            page:
              type: integer
            per_page:
              type: integer
            count:
              type: integer
            total_count:
              type: integer

    DnsRecord:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - A
            - AAAA
            - CNAME
            - TXT
            - MX
            - NS
            - SRV
            - CAA
            - PTR
            - SPF
            - LOC
            - NAPTR
            - CERT
            - DNSKEY
            - DS
            - HTTPS
            - SSHFP
            - SVCB
            - TLSA
            - URI
        name:
          type: string
          maxLength: 253
        content:
          type: string
          maxLength: 4096
        ttl:
          type: integer
          minimum: 1
          description: TTL in seconds (1 = automatic)
        proxied:
          type: boolean
          description: Only applicable to A, AAAA, and CNAME records
        priority:
          type: integer
          minimum: 0
          maximum: 65535
          description: Priority for MX and SRV records
        data:
          type: object
          description: Structured data for SRV, CAA, and similar record types
        comment:
          type: string

    DnsRecordInput:
      type: object
      properties:
        type:
          type: string
          enum:
            - A
            - AAAA
            - CNAME
            - TXT
            - MX
            - NS
            - SRV
            - CAA
            - PTR
            - SPF
            - LOC
            - NAPTR
            - CERT
            - DNSKEY
            - DS
            - HTTPS
            - SSHFP
            - SVCB
            - TLSA
            - URI
        name:
          type: string
          maxLength: 253
        content:
          type: string
          maxLength: 4096
        ttl:
          type: integer
          minimum: 1
        proxied:
          type: boolean
        priority:
          type: integer
          minimum: 0
          maximum: 65535
        comment:
          type: string
      required:
        - type
        - name
        - content

    DnsBulkImportRequest:
      type: object
      properties:
        records:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/DnsRecordInput'
      required:
        - records

    DnsBulkImportResponse:
      type: object
      properties:
        success:
          type: boolean
        created:
          type: integer
        total:
          type: integer
        errors:
          type: array
          items:
            type: object
            properties:
              index:
                type: integer
              error:
                type: string

    Snapshot:
      type: object
      properties:
        key:
          type: string
          description: KV key for this snapshot
        timestamp:
          type: string
          format: date-time
        username:
          type: string
        action:
          type: string

    SnapshotFull:
      type: object
      properties:
        timestamp:
          type: string
          format: date-time
        username:
          type: string
        action:
          type: string
        records:
          type: array
          items:
            $ref: '#/components/schemas/DnsRecord'

    SnapshotListResponse:
      type: object
      properties:
        snapshots:
          type: array
          items:
            $ref: '#/components/schemas/Snapshot'
        total:
          type: integer
        page:
          type: integer
        per_page:
          type: integer
        total_pages:
          type: integer

    RollbackRequest:
      type: object
      properties:
        snapshotKey:
          type: string
      required:
        - snapshotKey

    RollbackResponse:
      type: object
      properties:
        success:
          type: boolean
        results:
          type: object
          properties:
            deleted:
              type: integer
            created:
              type: integer
            updated:
              type: integer
            errors:
              type: array
              items:
                type: object

    DnsBatchRequest:
      type: object
      description: Cloudflare DNS records batch API payload
      properties:
        deletes:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
        posts:
          type: array
          items:
            $ref: '#/components/schemas/DnsRecordInput'

    CustomHostnameInput:
      type: object
      properties:
        hostname:
          type: string
        ssl:
          type: object
          properties:
            method:
              type: string
            type:
              type: string
        custom_metadata:
          type: object

    FallbackOriginInput:
      type: object
      properties:
        origin:
          type: string
          description: Fallback origin hostname
      required:
        - origin

    BulkOperationRequest:
      type: object
      properties:
        operation:
          type: string
          enum: [create, delete_matching]
        zones:
          oneOf:
            - type: string
              enum: [all]
            - type: array
              items:
                type: string
              maxItems: 100
        record:
          type: object
          properties:
            type:
              type: string
            name:
              type: string
            content:
              type: string
            ttl:
              type: integer
            proxied:
              type: boolean
      required:
        - operation
        - zones
        - record

    BulkOperationResult:
      type: object
      properties:
        zoneId:
          type: string
        zoneName:
          type: string
        success:
          type: boolean
        count:
          type: integer
        errors:
          type: array
          items:
            type: string

    BulkOperationResponse:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/BulkOperationResult'

    SearchResponse:
      type: object
      properties:
        results:
          type: array
          items:
            allOf:
              - $ref: '#/components/schemas/DnsRecord'
              - type: object
                properties:
                  zoneName:
                    type: string
                  zoneId:
                    type: string
        total:
          type: integer
        zonesSearched:
          type: integer

    User:
      type: object
      properties:
        username:
          type: string
        role:
          type: string
          enum: [admin, user]
        status:
          type: string
          enum: [active, pending]
        createdAt:
          type: string
          format: date-time
          nullable: true
        hasSetupToken:
          type: boolean
        allowedZones:
          type: array
          items:
            type: string

    CreateUserRequest:
      type: object
      properties:
        username:
          type: string
          description: Lowercase alphanumeric, hyphens, underscores only
        role:
          type: string
          enum: [admin, user]
          default: user
        allowedZones:
          type: array
          items:
            type: string
          description: Zone names the user may access (empty = all zones)
      required:
        - username

    CreateUserResponse:
      type: object
      properties:
        success:
          type: boolean
        username:
          type: string
        setupToken:
          type: string
          description: One-time token the user needs to set their password

    UpdateUserRequest:
      type: object
      properties:
        username:
          type: string
        role:
          type: string
          enum: [admin, user]
        resetSetupToken:
          type: boolean
          description: If true, regenerates setup token and resets user to pending
        allowedZones:
          type: array
          items:
            type: string
      required:
        - username

    AppSettings:
      type: object
      properties:
        openRegistration:
          type: boolean
          description: Whether self-registration is enabled
        webhookUrl:
          type: string
          description: Webhook URL for event notifications

    AppSettingsUpdate:
      type: object
      properties:
        openRegistration:
          type: boolean
        webhookUrl:
          type: string

    AuditEntry:
      type: object
      properties:
        timestamp:
          type: string
          format: date-time
        username:
          type: string
        action:
          type: string
        detail:
          type: string

    AuditLogResponse:
      type: object
      properties:
        entries:
          type: array
          items:
            $ref: '#/components/schemas/AuditEntry'
        total:
          type: integer
        page:
          type: integer
        perPage:
          type: integer
        totalPages:
          type: integer

    PasskeyCredential:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        createdAt:
          type: string
          format: date-time

    ChangePasswordRequest:
      type: object
      properties:
        currentPassword:
          type: string
          description: SHA-256 hash of the current password
        newPassword:
          type: string
          description: SHA-256 hash of the new password
      required:
        - currentPassword
        - newPassword

    TOTPSetupResponse:
      type: object
      properties:
        secret:
          type: string
          description: Base32-encoded TOTP secret
        uri:
          type: string
          description: otpauth:// URI for authenticator app provisioning

    TOTPConfirmRequest:
      type: object
      properties:
        code:
          type: string
          description: 6-digit TOTP code from the authenticator app
      required:
        - code

    TOTPDisableRequest:
      type: object
      properties:
        code:
          type: string
          description: 6-digit TOTP code to confirm disabling
      required:
        - code

    SettingsAccountList:
      type: object
      properties:
        accounts:
          type: array
          items:
            type: object
            properties:
              id:
                type: integer
              name:
                type: string
              source:
                type: string

    SaveTokenRequest:
      type: object
      properties:
        token:
          type: string
          description: Cloudflare API token
        accountIndex:
          type: integer
          description: Account slot index (default 0)
        name:
          type: string
          description: Display name for the account
        user:
          type: string
          description: Target username (admin only, to manage other users' tokens)
      required:
        - token

    RateLimitError:
      type: object
      properties:
        error:
          type: string
          example: Too many requests. Please try again later.

  # -------------------------------------------------------------------------
  # Reusable Responses
  # -------------------------------------------------------------------------
  responses:
    Unauthorized:
      description: Authentication required or invalid credentials
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    BadRequest:
      description: Invalid request body or parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Conflict:
      description: Resource already exists
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    TooManyRequests:
      description: Rate limit exceeded
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitError'
    ServerError:
      description: Internal server error (typically KV storage not configured)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

# ===========================================================================
# Paths
# ===========================================================================
paths:

  # -------------------------------------------------------------------------
  # Authentication
  # -------------------------------------------------------------------------
  /api/login:
    post:
      tags: [Authentication]
      summary: Password login
      description: |
        Authenticate with username and password. If the user has TOTP enabled,
        returns `requiresTOTP: true` instead of tokens, and the client must
        follow up with `/api/verify-totp`.

        The password field must contain the SHA-256 hex hash of the raw
        password (hashing is done client-side).

        Account lockout: after 5 failed attempts, the account is locked for 15
        minutes.
      operationId: login
      security: []
      x-rate-limit:
        max: 10
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: |
            Login successful. Returns JWT tokens and account list. If the user
            has TOTP enabled, returns `requiresTOTP: true` instead.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/LoginResponse'
                  - $ref: '#/components/schemas/LoginTOTPRequired'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Invalid username or password
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Account pending setup
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - type: object
                    properties:
                      needsSetup:
                        type: boolean
        '429':
          description: Account locked due to too many failed attempts
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  lockedUntil:
                    type: integer
                    description: Seconds until lockout expires

  /api/register:
    post:
      tags: [Authentication]
      summary: Self-registration
      description: |
        Register a new user account. Only available when open registration is
        enabled in app settings. The `admin` username is reserved and cannot be
        used. Username must be lowercase alphanumeric with hyphens and
        underscores.
      operationId: register
      security: []
      x-rate-limit:
        max: 5
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
      responses:
        '200':
          description: Registration successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          description: Registration is disabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /api/setup-account:
    post:
      tags: [Authentication]
      summary: Set up invited user account
      description: |
        Allows a newly invited user to set their password using the one-time
        setup token generated by an admin. The admin username cannot be set up
        this way.
      operationId: setupAccount
      security: []
      x-rate-limit:
        max: 5
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetupAccountRequest'
      responses:
        '200':
          description: Account setup successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          description: Invalid setup token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /api/verify-totp:
    post:
      tags: [Authentication]
      summary: Verify TOTP after login
      description: |
        Complete the two-factor authentication flow when the login endpoint
        returns `requiresTOTP: true`. Re-authenticates the password and
        verifies the TOTP code, then issues JWT tokens.
      operationId: verifyTotp
      security: []
      x-rate-limit:
        max: 5
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyTOTPRequest'
      responses:
        '200':
          description: TOTP verification successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Invalid credentials or TOTP code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Account pending setup
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Account locked
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/refresh:
    post:
      tags: [Authentication]
      summary: Refresh JWT access token
      description: |
        Exchange a valid refresh token for a new short-lived access token. The
        refresh token is validated and checked against the revocation list.
        Returns a new access token, account list, role, and username.
      operationId: refreshToken
      security: []
      x-rate-limit:
        max: 20
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefreshRequest'
      responses:
        '200':
          description: Token refreshed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RefreshResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Invalid, expired, or revoked refresh token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/logout:
    post:
      tags: [Authentication]
      summary: Logout and revoke refresh token
      description: |
        Logs the user out. If a refresh token is provided in the body, it is
        added to the revocation list (with 7-day TTL matching refresh token
        lifetime). Requires a valid Bearer JWT.
      operationId: logout
      security:
        - BearerJWT: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LogoutRequest'
      responses:
        '200':
          description: Logout successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Success'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # -------------------------------------------------------------------------
  # Passkeys (WebAuthn)
  # -------------------------------------------------------------------------
  /api/passkey/register-options:
    post:
      tags: [Passkeys]
      summary: Get passkey registration options
      description: |
        Generate WebAuthn registration options for the authenticated user.
        Returns a challenge and credential creation options. The challenge is
        stored in KV with a 5-minute TTL.

        **Note**: This endpoint uses GET semantics but is listed as POST in the
        route specification; the actual implementation exports `onRequestGet`.
      operationId: passkeyRegisterOptions
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 10
        windowSeconds: 60
      responses:
        '200':
          description: WebAuthn registration options
          content:
            application/json:
              schema:
                type: object
                description: PublicKeyCredentialCreationOptionsJSON from @simplewebauthn/server
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /api/passkey/register-verify:
    post:
      tags: [Passkeys]
      summary: Verify passkey registration
      description: |
        Verify the WebAuthn registration response from the authenticator and
        store the credential. The stored challenge is consumed during
        verification.
      operationId: passkeyRegisterVerify
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 10
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: RegistrationResponseJSON from @simplewebauthn/browser
              properties:
                credentialName:
                  type: string
                  description: Optional display name for the passkey
      responses:
        '200':
          description: Passkey registered successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  credential:
                    $ref: '#/components/schemas/PasskeyCredential'
        '400':
          description: Verification failed
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  message:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /api/passkey/login-options:
    post:
      tags: [Passkeys]
      summary: Get passkey login options
      description: |
        Generate WebAuthn authentication options for a given username. Returns
        allowed credentials and a challenge. No authentication required.
      operationId: passkeyLoginOptions
      security: []
      x-rate-limit:
        max: 10
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                username:
                  type: string
              required:
                - username
      responses:
        '200':
          description: WebAuthn authentication options
          content:
            application/json:
              schema:
                type: object
                description: PublicKeyCredentialRequestOptionsJSON from @simplewebauthn/server
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: No passkeys registered for this user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /api/passkey/login-verify:
    post:
      tags: [Passkeys]
      summary: Verify passkey login
      description: |
        Verify the WebAuthn authentication response and issue JWT tokens.
        No prior authentication required. On success, returns the same
        response shape as `/api/login`.
      operationId: passkeyLoginVerify
      security: []
      x-rate-limit:
        max: 10
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: AuthenticationResponseJSON from @simplewebauthn/browser
      responses:
        '200':
          description: Passkey authentication successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
        '401':
          description: Passkey authentication failed
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  message:
                    type: string
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /api/passkey/credentials:
    get:
      tags: [Passkeys]
      summary: List passkey credentials
      description: |
        List all passkey credentials for the authenticated user. Returns safe
        fields only (no public keys).
      operationId: listPasskeyCredentials
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 10
        windowSeconds: 60
      responses:
        '200':
          description: Passkey credential list
          content:
            application/json:
              schema:
                type: object
                properties:
                  credentials:
                    type: array
                    items:
                      $ref: '#/components/schemas/PasskeyCredential'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      tags: [Passkeys]
      summary: Delete a passkey credential
      description: |
        Delete a specific passkey credential by its credential ID.
      operationId: deletePasskeyCredential
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 10
        windowSeconds: 60
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
          description: Credential ID to delete
      responses:
        '200':
          description: Credential deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  remaining:
                    type: integer
                    description: Number of remaining passkey credentials
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # -------------------------------------------------------------------------
  # Public Settings
  # -------------------------------------------------------------------------
  /api/public-settings:
    get:
      tags: [Public]
      summary: Get public application settings
      description: |
        Returns non-sensitive application settings needed by the login page
        (e.g., whether open registration is enabled). No authentication required.
      operationId: getPublicSettings
      security: []
      responses:
        '200':
          description: Public settings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicSettings'

  # -------------------------------------------------------------------------
  # Zones
  # -------------------------------------------------------------------------
  /api/zones:
    get:
      tags: [Zones]
      summary: List Cloudflare zones
      description: |
        Retrieve the list of zones from the Cloudflare API. For non-admin
        users, the list is filtered based on their `allowedZones` setting.
        Returns up to 50 zones per page.
      operationId: listZones
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      responses:
        '200':
          description: Cloudflare zones list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudflareResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # -------------------------------------------------------------------------
  # DNS Records
  # -------------------------------------------------------------------------
  /api/zones/{zoneId}/dns_records:
    get:
      tags: [DNS Records]
      summary: List DNS records
      description: |
        List all DNS records for a zone. Returns up to 100 records per page.
        Proxies the Cloudflare API response directly.
      operationId: listDnsRecords
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: zoneId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: DNS records list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudflareResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags: [DNS Records]
      summary: Create a DNS record
      description: |
        Create a new DNS record in the specified zone. Input is validated
        server-side before forwarding to the Cloudflare API. A DNS snapshot is
        saved before the mutation.
      operationId: createDnsRecord
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: zoneId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DnsRecordInput'
      responses:
        '200':
          description: DNS record created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudflareResponse'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  errors:
                    type: array
                    items:
                      type: object
                      properties:
                        message:
                          type: string
                  messages:
                    type: array
                    items:
                      type: object
                  result:
                    nullable: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    patch:
      tags: [DNS Records]
      summary: Update a DNS record
      description: |
        Update an existing DNS record. The record ID must be provided as a
        query parameter (`?id=...`). Input is validated server-side. A DNS
        snapshot is saved before the mutation.
      operationId: updateDnsRecord
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: zoneId
          in: path
          required: true
          schema:
            type: string
        - name: id
          in: query
          required: true
          description: DNS record ID to update
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DnsRecordInput'
      responses:
        '200':
          description: DNS record updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudflareResponse'
        '400':
          description: Missing record ID or validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      tags: [DNS Records]
      summary: Delete a DNS record
      description: |
        Delete a DNS record by ID (provided as query parameter `?id=...`). A
        DNS snapshot is saved before the deletion.
      operationId: deleteDnsRecord
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: zoneId
          in: path
          required: true
          schema:
            type: string
        - name: id
          in: query
          required: true
          description: DNS record ID to delete
          schema:
            type: string
      responses:
        '200':
          description: DNS record deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudflareResponse'
        '400':
          description: Missing record ID
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # -------------------------------------------------------------------------
  # DNS Import / Export
  # -------------------------------------------------------------------------
  /api/zones/{zoneId}/dns_import:
    post:
      tags: [DNS Records]
      summary: Bulk import DNS records
      description: |
        Import multiple DNS records into a zone. Supports two modes:

        - **JSON mode** (`Content-Type: application/json`): Send a JSON body
          with `{ records: [...] }`. Maximum 100 records per import. Each
          record is validated and created sequentially. Returns a summary with
          created count and per-record errors.

        - **BIND file mode** (`Content-Type: multipart/form-data`): Upload a
          BIND zone file. The request is proxied directly to the Cloudflare
          DNS import API.

        A DNS snapshot is saved before the import (JSON mode only).
      operationId: importDnsRecords
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: zoneId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DnsBulkImportRequest'
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: BIND zone file
      responses:
        '200':
          description: Import results
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/DnsBulkImportResponse'
                  - $ref: '#/components/schemas/CloudflareResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/zones/{zoneId}/dns_export:
    get:
      tags: [DNS Records]
      summary: Export DNS records as BIND zone file
      description: |
        Export all DNS records for a zone in BIND format. The response is a
        plain-text file with a `Content-Disposition` header for download.
      operationId: exportDnsRecords
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: zoneId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: BIND zone file
          content:
            text/plain:
              schema:
                type: string
          headers:
            Content-Disposition:
              schema:
                type: string
                example: 'attachment; filename="dns_records_{zoneId}.txt"'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # -------------------------------------------------------------------------
  # DNS History (Snapshots)
  # -------------------------------------------------------------------------
  /api/zones/{zoneId}/dns_history:
    get:
      tags: [DNS History]
      summary: List DNS snapshots or get full snapshot
      description: |
        List paginated DNS snapshots for a zone. If `?full={key}` is provided,
        returns the complete snapshot including all DNS records (for rollback
        preview).
      operationId: listDnsHistory
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: zoneId
          in: path
          required: true
          schema:
            type: string
        - name: full
          in: query
          description: KV key of a specific snapshot to retrieve in full
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
        - name: per_page
          in: query
          schema:
            type: integer
            default: 10
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Snapshot list or full snapshot
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/SnapshotListResponse'
                  - type: object
                    properties:
                      snapshot:
                        $ref: '#/components/schemas/SnapshotFull'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Snapshot not found (when using ?full=)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags: [DNS History]
      summary: Rollback to a DNS snapshot
      description: |
        Roll back a zone's DNS records to a previous snapshot state. Computes
        a diff between current records and the target snapshot, then applies
        deletions, creations, and updates as needed. A new snapshot of the
        current state is saved before rollback.
      operationId: rollbackDnsHistory
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: zoneId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RollbackRequest'
      responses:
        '200':
          description: Rollback results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RollbackResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          description: Failed to fetch current DNS records from Cloudflare
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # -------------------------------------------------------------------------
  # DNS Batch
  # -------------------------------------------------------------------------
  /api/zones/{zoneId}/dns_batch:
    post:
      tags: [DNS Records]
      summary: Batch DNS operations
      description: |
        Perform batch create and delete operations on DNS records within a
        single zone. Proxied to the Cloudflare DNS records batch API.
      operationId: batchDnsRecords
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: zoneId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DnsBatchRequest'
      responses:
        '200':
          description: Batch operation results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudflareResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # -------------------------------------------------------------------------
  # Custom Hostnames (Cloudflare for SaaS)
  # -------------------------------------------------------------------------
  /api/zones/{zoneId}/custom_hostnames:
    get:
      tags: [Custom Hostnames]
      summary: List custom hostnames
      description: |
        List all custom hostnames for a zone. Returns up to 100 custom
        hostnames. Proxied to the Cloudflare API.
      operationId: listCustomHostnames
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: zoneId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Custom hostnames list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudflareResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags: [Custom Hostnames]
      summary: Create a custom hostname
      description: |
        Create a new custom hostname in the specified zone. Proxied to the
        Cloudflare API.
      operationId: createCustomHostname
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: zoneId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomHostnameInput'
      responses:
        '200':
          description: Custom hostname created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudflareResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    patch:
      tags: [Custom Hostnames]
      summary: Update a custom hostname
      description: |
        Update an existing custom hostname by ID (provided as query parameter
        `?id=...`). Proxied to the Cloudflare API.
      operationId: updateCustomHostname
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: zoneId
          in: path
          required: true
          schema:
            type: string
        - name: id
          in: query
          required: true
          description: Custom hostname ID to update
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomHostnameInput'
      responses:
        '200':
          description: Custom hostname updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudflareResponse'
        '400':
          description: Missing hostname ID
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      tags: [Custom Hostnames]
      summary: Delete a custom hostname
      description: |
        Delete a custom hostname by ID (provided as query parameter `?id=...`).
        Proxied to the Cloudflare API.
      operationId: deleteCustomHostname
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: zoneId
          in: path
          required: true
          schema:
            type: string
        - name: id
          in: query
          required: true
          description: Custom hostname ID to delete
          schema:
            type: string
      responses:
        '200':
          description: Custom hostname deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudflareResponse'
        '400':
          description: Missing hostname ID
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # -------------------------------------------------------------------------
  # Fallback Origin
  # -------------------------------------------------------------------------
  /api/zones/{zoneId}/fallback_origin:
    get:
      tags: [Fallback Origin]
      summary: Get fallback origin
      description: |
        Retrieve the current fallback origin configuration for custom
        hostnames in the specified zone.
      operationId: getFallbackOrigin
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: zoneId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Fallback origin configuration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudflareResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    put:
      tags: [Fallback Origin]
      summary: Set fallback origin
      description: |
        Set or update the fallback origin for custom hostnames in the
        specified zone.
      operationId: setFallbackOrigin
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: zoneId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FallbackOriginInput'
      responses:
        '200':
          description: Fallback origin updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudflareResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # -------------------------------------------------------------------------
  # DNS Bulk (Cross-Zone)
  # -------------------------------------------------------------------------
  /api/dns_bulk:
    post:
      tags: [DNS Bulk]
      summary: Bulk DNS operations across zones
      description: |
        Perform DNS operations across multiple zones simultaneously. Supports
        two operations:

        - **create**: Create the same DNS record in all specified zones. The
          record name is automatically expanded to the FQDN for each zone.
        - **delete_matching**: Delete records matching the given criteria
          (type, name, content) from all specified zones. Any filter field can
          be omitted to match all values.

        The `zones` field can be `"all"` (all zones accessible to the user) or
        an array of zone IDs (max 100). Non-admin users are restricted to
        their allowed zones.
      operationId: bulkDnsOperation
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkOperationRequest'
      responses:
        '200':
          description: Bulk operation results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkOperationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          description: Failed to fetch zones from Cloudflare
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # -------------------------------------------------------------------------
  # Search
  # -------------------------------------------------------------------------
  /api/search:
    get:
      tags: [Search]
      summary: Global DNS record search
      description: |
        Search DNS records across all accessible zones. The query is matched
        against record name, content, and type. Results are capped at 100.
        Non-admin users only see results from their allowed zones.
      operationId: searchDnsRecords
      security:
        - BearerJWT: []
        - CloudflareToken: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      parameters:
        - name: q
          in: query
          required: true
          description: Search query (minimum 2 characters)
          schema:
            type: string
            minLength: 2
      responses:
        '200':
          description: Search results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'
        '400':
          description: Search query too short
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  # -------------------------------------------------------------------------
  # Admin - Users
  # -------------------------------------------------------------------------
  /api/admin/users:
    get:
      tags: [Admin - Users]
      summary: List all users
      description: |
        List all users including the built-in admin account. Returns username,
        role, status, creation date, whether a setup token exists, and allowed
        zones for each user. Admin only.
      operationId: listUsers
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 20
        windowSeconds: 60
      responses:
        '200':
          description: User list
          content:
            application/json:
              schema:
                type: object
                properties:
                  users:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags: [Admin - Users]
      summary: Create a new user
      description: |
        Create a new user with a setup token (no password). The setup token
        must be shared with the user who can then use `/api/setup-account` to
        set their password. The `admin` username is reserved. Admin only.
      operationId: createUser
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 20
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
      responses:
        '200':
          description: User created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateUserResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    put:
      tags: [Admin - Users]
      summary: Update a user
      description: |
        Update user properties (role, allowed zones). Optionally reset the
        setup token which puts the user back to pending status. The admin user
        cannot be modified. Admin only.
      operationId: updateUser
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 20
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateUserRequest'
      responses:
        '200':
          description: User updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  setupToken:
                    type: string
                    description: Present only when resetSetupToken was true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      tags: [Admin - Users]
      summary: Delete a user
      description: |
        Delete a user account and all associated tokens. The admin user
        cannot be deleted. Admin only.
      operationId: deleteUser
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 20
        windowSeconds: 60
      parameters:
        - name: username
          in: query
          required: true
          description: Username to delete
          schema:
            type: string
      responses:
        '200':
          description: User deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Success'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  # -------------------------------------------------------------------------
  # Admin - Settings (per-user token management)
  # -------------------------------------------------------------------------
  /api/admin/settings:
    get:
      tags: [Admin - Settings]
      summary: List managed accounts / tokens
      description: |
        List the authenticated user's configured Cloudflare API token slots.
        Admins can query other users' tokens by passing `?user={username}`.
        If `?retrieve={index}` is provided, returns the actual token value
        for that slot. All authenticated users can access this endpoint for
        their own tokens.
      operationId: listSettings
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 20
        windowSeconds: 60
      parameters:
        - name: user
          in: query
          description: Target username (admin only)
          schema:
            type: string
        - name: retrieve
          in: query
          description: Account index to retrieve the actual token value
          schema:
            type: integer
      responses:
        '200':
          description: Account list or token value
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/SettingsAccountList'
                  - type: object
                    properties:
                      token:
                        type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Token slot not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags: [Admin - Settings]
      summary: Save a Cloudflare API token
      description: |
        Save or update a Cloudflare API token in the user's token storage.
        The token is verified against the Cloudflare API before saving.
        Admins can manage other users' tokens by including a `user` field.
      operationId: saveToken
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 20
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SaveTokenRequest'
      responses:
        '200':
          description: Token saved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  id:
                    type: integer
        '400':
          description: Token missing or invalid/inactive
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      tags: [Admin - Settings]
      summary: Remove a Cloudflare API token
      description: |
        Remove a Cloudflare API token from the user's token storage by account
        index. Admins can manage other users' tokens by passing `?user=`.
      operationId: deleteToken
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 20
        windowSeconds: 60
      parameters:
        - name: index
          in: query
          description: Account index to remove (default 0)
          schema:
            type: integer
            default: 0
        - name: user
          in: query
          description: Target username (admin only)
          schema:
            type: string
      responses:
        '200':
          description: Token removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  # -------------------------------------------------------------------------
  # Admin - App Settings
  # -------------------------------------------------------------------------
  /api/admin/app-settings:
    get:
      tags: [Admin - App Settings]
      summary: Get application settings
      description: |
        Retrieve application-level settings including open registration toggle
        and webhook URL. Admin only.
      operationId: getAppSettings
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 20
        windowSeconds: 60
      responses:
        '200':
          description: Application settings
          content:
            application/json:
              schema:
                type: object
                properties:
                  settings:
                    $ref: '#/components/schemas/AppSettings'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    put:
      tags: [Admin - App Settings]
      summary: Update application settings
      description: |
        Update application-level settings. Only known keys (`openRegistration`,
        `webhookUrl`) are accepted. Admin only.
      operationId: updateAppSettings
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 20
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppSettingsUpdate'
      responses:
        '200':
          description: Settings updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  settings:
                    $ref: '#/components/schemas/AppSettings'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  # -------------------------------------------------------------------------
  # Admin - Audit Log
  # -------------------------------------------------------------------------
  /api/admin/audit-log:
    get:
      tags: [Admin - Audit Log]
      summary: Get audit log
      description: |
        Retrieve the paginated audit log. Maximum 500 entries are retained.
        Admin only.
      operationId: getAuditLog
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 20
        windowSeconds: 60
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: per_page
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
      responses:
        '200':
          description: Audit log entries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditLogResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      tags: [Admin - Audit Log]
      summary: Clear audit log
      description: |
        Delete all audit log entries. Admin only.
      operationId: clearAuditLog
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 20
        windowSeconds: 60
      responses:
        '200':
          description: Audit log cleared
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Success'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  # -------------------------------------------------------------------------
  # Account - Password
  # -------------------------------------------------------------------------
  /api/account/password:
    post:
      tags: [Account]
      summary: Change password
      description: |
        Change the authenticated user's password. Requires the current
        password for verification. The admin user's password is managed via
        environment variables and cannot be changed through this endpoint.
        Both passwords should be SHA-256 hashes (client-side hashing).
      operationId: changePassword
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChangePasswordRequest'
      responses:
        '200':
          description: Password updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
        '400':
          description: Missing fields or admin user cannot change password
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Current password is incorrect
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  # -------------------------------------------------------------------------
  # Account - TOTP
  # -------------------------------------------------------------------------
  /api/account/totp:
    get:
      tags: [Account]
      summary: Generate TOTP setup secret
      description: |
        Generate a new TOTP secret and store it as pending (10-minute TTL).
        Returns the base32 secret and an `otpauth://` URI for provisioning an
        authenticator app. The user must confirm by calling POST with a valid
        code.
      operationId: getTotpSetup
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      responses:
        '200':
          description: TOTP setup secret and URI
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TOTPSetupResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags: [Account]
      summary: Confirm TOTP setup
      description: |
        Confirm TOTP setup by verifying a code generated from the pending
        secret. On success, the TOTP secret is persisted and two-factor
        authentication is enabled for the user.
      operationId: confirmTotpSetup
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TOTPConfirmRequest'
      responses:
        '200':
          description: TOTP enabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Success'
        '400':
          description: Missing code, invalid code, or no pending setup
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      tags: [Account]
      summary: Disable TOTP
      description: |
        Disable TOTP two-factor authentication. Requires a valid TOTP code
        to confirm the action.
      operationId: disableTotp
      security:
        - BearerJWT: []
      x-rate-limit:
        max: 30
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TOTPDisableRequest'
      responses:
        '200':
          description: TOTP disabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Success'
        '400':
          description: Missing code or TOTP not enabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Invalid TOTP code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'