openapi: '3.0.3'
info:
  title: IRDB — IP Reputation Database
  version: '1.0.0'
  description: |
    Self-hosted IP reputation service: ingest abuse reports, distribute tailored block lists.

    ## Versioning

    Single major version `v1`. Changes within `v1` are additive only — new endpoints, new optional fields, new optional query params. Breaking changes ship as `v2`.

    ## Endpoint groups

    - **Public**: machine clients (reporters, consumers).
    - **Admin**: UI BFF + admin-kind tokens. RBAC enforced server-side.
    - **Auth**: UI BFF only — bridges browser auth to user records. Marked `x-internal: true`.
    - **Internal jobs**: not in this spec. Scheduler-only, network-restricted.

    ## Authentication

    Bearer token in the `Authorization` header. Four token kinds: `reporter`, `consumer`, `admin`, `service`. See `doc/auth-flows.md`.

    ## Errors

    Uniform envelope: `{"error":"<code>","details":{...}}`. Validation errors include `details`. Authentication failures return `401` `unauthorized`. Authorization failures return `403`.

    ## Rate limiting

    Public endpoints: 60 req/s/token (configurable). On exhaustion: `429` with `Retry-After: 1`.
  license:
    name: TBD
servers:
  - url: http://localhost:8081
    description: Default compose deployment
  - url: https://reputation-api.example.com
    description: Production (replace hostname)
tags:
  - name: Public
    description: 'Machine clients: reporters and blocklist consumers.'
  - name: Admin
    description: UI BFF + admin-kind tokens.
  - name: Auth
    description: UI BFF only. Service token required, no impersonation.
security:
  - BearerAuth: []
paths:
  '/api/v1/report':
    post:
      tags:
        - Public
      summary: Submit an abuse report
      description: |
        Token kind: `reporter`. Rate limit: 60 req/s per token (configurable).
        Returns `202 Accepted` on success.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              '$ref': '#/components/schemas/ReportRequest'
      responses:
        '202':
          description: Report accepted
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/ReportResponse'
        '400':
          description: Validation failed
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/Error'
        '401':
          description: Bad / wrong-kind token
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/Error'
        '429':
          description: Rate limited
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds to wait before retrying.
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/Error'
  '/api/v1/blocklist':
    get:
      tags:
        - Public
      summary: Pull a tailored blocklist
      description: |
        Token kind: `consumer`. The consumer's bound policy decides which IPs/CIDRs land in the output.
        Cached internally for 30 s per consumer. Honour `If-None-Match` to skip retransfer.
        `?format=json` returns structured rows; default is `text/plain`, one entry per line.
      security:
        - BearerAuth: []
      parameters:
        - name: format
          in: query
          schema:
            type: string
            enum:
              - text
              - json
            default: text
        - name: If-None-Match
          in: header
          schema:
            type: string
      responses:
        '200':
          description: Current blocklist
          headers:
            ETag:
              schema:
                type: string
            X-Blocklist-Generated-At:
              schema:
                type: string
                format: date-time
            X-Blocklist-Entries:
              schema:
                type: integer
            X-Blocklist-Policy:
              schema:
                type: string
          content:
            'text/plain':
              schema:
                type: string
                example: |
                  203.0.113.42
                  198.51.100.0/24
            'application/json':
              schema:
                '$ref': '#/components/schemas/BlocklistJson'
        '304':
          description: Not modified — body matches `If-None-Match`
        '401':
          description: Bad / wrong-kind token
  '/api/v1/admin/me':
    get:
      tags:
        - Admin
      summary: Current acting identity
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      responses:
        '200':
          description: Identity info
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/User'
  '/api/v1/admin/ips':
    get:
      tags:
        - Admin
      summary: Search IPs
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: q
          in: query
          schema:
            type: string
        - name: category
          in: query
          schema:
            type: string
        - name: min_score
          in: query
          schema:
            type: number
            format: float
        - name: max_score
          in: query
          schema:
            type: number
            format: float
        - name: country
          in: query
          schema:
            type: string
        - name: asn
          in: query
          schema:
            type: integer
        - name: status
          in: query
          schema:
            type: string
            enum:
              - scored
              - manual
              - allowlisted
              - clean
        - '$ref': '#/components/parameters/Page'
        - '$ref': '#/components/parameters/PageSize'
      responses:
        '200':
          description: Page of IPs
          content:
            'application/json':
              schema:
                type: object
                properties:
                  page:
                    type: integer
                    minimum: 1
                    example: 1
                  page_size:
                    type: integer
                    minimum: 1
                    maximum: 200
                    example: 50
                  total:
                    type: integer
                    minimum: 0
                    example: 1284
                  items:
                    type: array
                    items:
                      type: object
  '/api/v1/admin/ips/countries':
    get:
      tags:
        - Admin
      summary: Country code dropdown source
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      responses:
        '200':
          description: '`[{code, count}]`'
          content:
            'application/json':
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        code:
                          type: string
                        count:
                          type: integer
  '/api/v1/admin/ips/{ip}':
    get:
      tags:
        - Admin
      summary: IP detail
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: ip
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: IP detail
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/IpDetail'
        '404':
          description: Invalid IP / not found
  '/api/v1/admin/stats/dashboard':
    get:
      tags:
        - Admin
      summary: Dashboard stats (30s cached)
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      responses:
        '200':
          description: Dashboard payload
          content:
            'application/json':
              schema:
                type: object
  '/api/v1/admin/manual-blocks':
    get:
      tags:
        - Admin
      summary: List manual blocks
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      responses:
        '200':
          description: list
    post:
      tags:
        - Admin
      summary: Create manual block
      description: Operator+ role required. `kind=ip` requires `ip`; `kind=subnet` requires `cidr`.
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              type: object
              properties:
                kind:
                  type: string
                  enum:
                    - ip
                    - subnet
                ip:
                  type: string
                cidr:
                  type: string
                reason:
                  type: string
                expires_at:
                  type: string
                  format: date-time
      responses:
        '201':
          description: Created
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/ManualBlock'
  '/api/v1/admin/manual-blocks/{id}':
    get:
      tags:
        - Admin
      summary: Show manual block
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: manual block
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/ManualBlock'
    delete:
      tags:
        - Admin
      summary: Delete manual block
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '204':
          description: Deleted
  '/api/v1/admin/allowlist':
    get:
      tags:
        - Admin
      summary: List allowlist
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      responses:
        '200':
          description: list
    post:
      tags:
        - Admin
      summary: Create allowlist entry
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              type: object
              properties:
                kind:
                  type: string
                  enum:
                    - ip
                    - subnet
                ip:
                  type: string
                cidr:
                  type: string
                reason:
                  type: string
      responses:
        '201':
          description: Created
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/AllowlistEntry'
  '/api/v1/admin/allowlist/{id}':
    get:
      tags:
        - Admin
      summary: Show allowlist entry
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: allowlist entry
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/AllowlistEntry'
    delete:
      tags:
        - Admin
      summary: Delete allowlist entry
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '204':
          description: Deleted
  '/api/v1/admin/reporters':
    get:
      tags:
        - Admin
      summary: List reporters
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      responses:
        '200':
          description: list
    post:
      tags:
        - Admin
      summary: Create reporter
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              '$ref': '#/components/schemas/Reporter'
      responses:
        '201':
          description: Created
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/Reporter'
  '/api/v1/admin/reporters/{id}':
    get:
      tags:
        - Admin
      summary: Show reporter
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: reporter
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/Reporter'
    patch:
      tags:
        - Admin
      summary: Update reporter
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        content:
          'application/json':
            schema:
              '$ref': '#/components/schemas/Reporter'
      responses:
        '200':
          description: Updated
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/Reporter'
    delete:
      tags:
        - Admin
      summary: Soft-delete reporter
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '204':
          description: Deleted
        '409':
          description: Has reports — flagged inactive instead.
  '/api/v1/admin/consumers':
    get:
      tags:
        - Admin
      summary: List consumers
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      responses:
        '200':
          description: list
    post:
      tags:
        - Admin
      summary: Create consumer
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              '$ref': '#/components/schemas/Consumer'
      responses:
        '201':
          description: Created
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/Consumer'
  '/api/v1/admin/consumers/{id}':
    get:
      tags:
        - Admin
      summary: Show consumer
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: consumer
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/Consumer'
    patch:
      tags:
        - Admin
      summary: Update consumer
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        content:
          'application/json':
            schema:
              '$ref': '#/components/schemas/Consumer'
      responses:
        '200':
          description: Updated
    delete:
      tags:
        - Admin
      summary: Soft-delete consumer
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '204':
          description: Deleted
  '/api/v1/admin/tokens':
    get:
      tags:
        - Admin
      summary: List tokens
      description: Service tokens are filtered out unconditionally.
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      responses:
        '200':
          description: list
    post:
      tags:
        - Admin
      summary: Create token
      description: Returns the raw token ONCE in `raw_token`. Service tokens are not creatable here.
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              type: object
              properties:
                kind:
                  type: string
                  enum:
                    - reporter
                    - consumer
                    - admin
                reporter_id:
                  type: integer
                consumer_id:
                  type: integer
                role:
                  type: string
                  enum:
                    - viewer
                    - operator
                    - admin
                expires_at:
                  type: string
                  format: date-time
      responses:
        '201':
          description: Created
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/TokenCreated'
  '/api/v1/admin/tokens/{id}':
    delete:
      tags:
        - Admin
      summary: Revoke token
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '204':
          description: Revoked
  '/api/v1/admin/tokens/{id}/purge':
    delete:
      tags:
        - Admin
      summary: Permanently delete a revoked token
      description: |
        Hard-deletes the row. Requires the token to be already revoked
        (`revoked_at` set); active tokens return 409 to keep "revoke first,
        then prune" the only path that removes data. Service tokens cannot
        be deleted.
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '204':
          description: Deleted
        '403':
          description: Service token; not deletable via API
        '404':
          description: Not found
        '409':
          description: Token still active; revoke it first
  '/api/v1/admin/categories':
    get:
      tags:
        - Admin
      summary: List categories
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      responses:
        '200':
          description: list
    post:
      tags:
        - Admin
      summary: Create category
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              '$ref': '#/components/schemas/Category'
      responses:
        '201':
          description: Created
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/Category'
  '/api/v1/admin/categories/{id}':
    get:
      tags:
        - Admin
      summary: Show category
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: category
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/Category'
    patch:
      tags:
        - Admin
      summary: Update category
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        content:
          'application/json':
            schema:
              '$ref': '#/components/schemas/Category'
      responses:
        '200':
          description: Updated
    delete:
      tags:
        - Admin
      summary: Hard-delete category (refused if in use)
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '204':
          description: Deleted
        '409':
          description: Category in use; soft-delete via PATCH `is_active=false`.
  '/api/v1/admin/policies':
    get:
      tags:
        - Admin
      summary: List policies
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      responses:
        '200':
          description: list
    post:
      tags:
        - Admin
      summary: Create policy
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              '$ref': '#/components/schemas/Policy'
      responses:
        '201':
          description: Created
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/Policy'
  '/api/v1/admin/policies/{id}':
    get:
      tags:
        - Admin
      summary: Show policy
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: policy
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/Policy'
    patch:
      tags:
        - Admin
      summary: Update policy (replaces thresholds wholesale when present)
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        content:
          'application/json':
            schema:
              '$ref': '#/components/schemas/Policy'
      responses:
        '200':
          description: Updated
    delete:
      tags:
        - Admin
      summary: Delete policy (refused if used by consumers)
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '204':
          description: Deleted
        '409':
          description: Policy in use
  '/api/v1/admin/policies/{id}/preview':
    get:
      tags:
        - Admin
      summary: Preview policy (count + sample)
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Preview
  '/api/v1/admin/users':
    get:
      tags:
        - Admin
      summary: List users (Admin)
      description: |
        Roster of every IRDB user (OIDC + the single local-admin row).
        Used by the admin **users** page to disable / re-enable accounts
        (SEC_REVIEW F11).
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - '$ref': '#/components/parameters/Page'
        - '$ref': '#/components/parameters/PageSize'
      responses:
        '200':
          description: User page
          content:
            'application/json':
              schema:
                type: object
                properties:
                  page:
                    type: integer
                  page_size:
                    type: integer
                  total:
                    type: integer
                  items:
                    type: array
                    items:
                      '$ref': '#/components/schemas/UserRecord'
  '/api/v1/admin/users/{id}':
    get:
      tags:
        - Admin
      summary: Get user (Admin)
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: User record
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/UserRecord'
        '404':
          description: Not found
    patch:
      tags:
        - Admin
      summary: Disable / enable user (Admin)
      description: |
        SEC_REVIEW F11. Toggles `disabled_at` on the user row. A disabled
        user cannot complete OIDC or local sign-in and cannot be
        impersonated via the service token (impersonation 403s with
        `user_disabled`).

        Refused with 409 when:
          - the target id matches the acting user (`cannot_disable_self`),
          - the target is the local-admin row (`cannot_disable_local_admin`).
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              type: object
              required: [disabled]
              properties:
                disabled:
                  type: boolean
      responses:
        '200':
          description: Updated record
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/UserRecord'
        '400':
          description: Validation error
        '404':
          description: Not found
        '409':
          description: Refused (self-disable or local-admin)
          content:
            'application/json':
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum:
                      - cannot_disable_self
                      - cannot_disable_local_admin
  '/api/v1/admin/audit-log':
    get:
      tags:
        - Admin
      summary: Filtered audit log
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: actor_kind
          in: query
          schema:
            type: string
            enum:
              - user
              - admin-token
              - reporter
              - consumer
              - system
        - name: actor_id
          in: query
          schema:
            type: integer
        - name: actor_via
          in: query
          description: |
            SEC_REVIEW F11. Filter rows by upstream auth path. `oidc` and
            `local` further split rows that already have `actor_kind=user`
            (impersonation), so an auditor can scope a review to local-admin
            actions vs OIDC-user actions without joining the users table.
          schema:
            type: string
            enum:
              - oidc
              - local
              - admin-token
              - service
              - reporter
              - consumer
              - system
        - name: action
          in: query
          schema:
            type: string
        - name: entity_type
          in: query
          schema:
            type: string
        - name: entity_id
          in: query
          schema:
            type: string
        - name: subject_kind
          in: query
          description: |
            With `subject_id`, returns rows where the (kind, id) pair matches
            either the audit row's target OR its actor. Useful for per-entity
            detail pages that want both admin actions on the entity and
            events emitted by it (`report.received`, `blocklist.requested`).
            Both halves must be supplied together; otherwise 400.
          schema:
            type: string
        - name: subject_id
          in: query
          schema:
            type: string
        - name: from
          in: query
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          schema:
            type: string
            format: date-time
        - '$ref': '#/components/parameters/Page'
        - '$ref': '#/components/parameters/PageSize'
      responses:
        '200':
          description: Audit page
          content:
            'application/json':
              schema:
                type: object
                properties:
                  page:
                    type: integer
                    minimum: 1
                    example: 1
                  page_size:
                    type: integer
                    minimum: 1
                    maximum: 200
                    example: 50
                  total:
                    type: integer
                    minimum: 0
                    example: 1284
                  items:
                    type: array
                    items:
                      '$ref': '#/components/schemas/AuditEntry'
  '/api/v1/admin/jobs/status':
    get:
      tags:
        - Admin
      summary: Jobs status (Viewer)
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      responses:
        '200':
          description: Jobs status
          content:
            'application/json':
              schema:
                type: object
                properties:
                  now:
                    type: string
                    format: date-time
                  jobs:
                    type: object
                    additionalProperties:
                      '$ref': '#/components/schemas/JobStatus'
  '/api/v1/admin/jobs/trigger/{name}':
    post:
      tags:
        - Admin
      summary: Manually trigger a job (Admin)
      description: 'Whitelisted params: `full`, `max_rows`, `reenrich`. Other body fields are dropped.'
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
        - name: name
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          'application/json':
            schema:
              type: object
              properties:
                full:
                  type: boolean
                max_rows:
                  type: integer
                reenrich:
                  type: boolean
      responses:
        '200':
          description: Job ran
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/JobOutcome'
        '404':
          description: Unknown job
        '409':
          description: Lock held; status=`skipped_locked`
        '412':
          description: refresh-geoip without credential
  '/api/v1/admin/config':
    get:
      tags:
        - Admin
      summary: Effective config (secrets masked)
      description: Admin only.
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      responses:
        '200':
          description: Config sections
          content:
            'application/json':
              schema:
                type: object
                properties:
                  sections:
                    type: object
                    additionalProperties:
                      type: object
  '/api/v1/admin/app-settings':
    get:
      tags:
        - Admin
      summary: Runtime feature flags (Admin)
      description: |
        Returns the runtime-mutable feature flags. Currently exposes the
        audit-emission toggles for high-volume public endpoints
        (`audit_report_received_enabled`, `audit_blocklist_request_enabled`).
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      responses:
        '200':
          description: Current toggle state
          content:
            'application/json':
              schema:
                type: object
                additionalProperties:
                  type: boolean
    patch:
      tags:
        - Admin
      summary: Update runtime feature flags (Admin)
      description: |
        Updates one or more runtime feature flags. Body keys not listed are left
        untouched. Returns the post-update snapshot.
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              type: object
              properties:
                audit_report_received_enabled:
                  type: boolean
                audit_blocklist_request_enabled:
                  type: boolean
      responses:
        '200':
          description: Updated snapshot
          content:
            'application/json':
              schema:
                type: object
                additionalProperties:
                  type: boolean
        '400':
          description: Validation error
  '/api/v1/admin/maintenance/purge':
    post:
      tags:
        - Admin
      summary: Wipe operational data (Admin)
      description: |
        Deletes reports, scores, enrichment, manual blocks, allowlist, audit log, job history, reporters, consumers, policies, and non-service tokens. Preserves users, OIDC role mappings, abuse categories, and the `service`-kind token.

        Requires `confirm: "PURGE"` in the body — any other value returns 400.
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              type: object
              required:
                - confirm
              properties:
                confirm:
                  type: string
                  enum:
                    - PURGE
      responses:
        '200':
          description: Purge succeeded
          content:
            'application/json':
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: purged
                  deleted:
                    type: object
                    additionalProperties:
                      type: integer
        '400':
          description: Missing or wrong `confirm` value
  '/api/v1/admin/maintenance/seed-demo':
    post:
      tags:
        - Admin
      summary: Load demo dataset (Admin)
      description: |
        Populates reporters, consumers, IPs, reports, manual blocks, allowlist, and synthetic GeoIP for demos and screenshots. Triggers a full score recompute on completion. Idempotent: returns 409 if demo data is already present.

        Requires `confirm: "SEED"` in the body — any other value returns 400.
      security:
        - BearerAuth: []
      parameters:
        - '$ref': '#/components/parameters/ActingUserId'
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              type: object
              required:
                - confirm
              properties:
                confirm:
                  type: string
                  enum:
                    - SEED
      responses:
        '200':
          description: Demo data inserted
          content:
            'application/json':
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: seeded
                  summary:
                    type: object
                    additionalProperties:
                      type: integer
                  recompute:
                    '$ref': '#/components/schemas/JobOutcome'
        '400':
          description: Missing or wrong `confirm` value
        '409':
          description: Demo data already present
        '412':
          description: No categories configured
  '/api/v1/auth/users/upsert-oidc':
    post:
      tags:
        - Auth
      summary: Upsert an OIDC-authenticated user
      description: |
        **UI BFF only.** Service-token-required, no impersonation header.
        Resolves the user record + role (via `oidc_role_mappings`) for a freshly-validated ID token.
      x-internal: true
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              type: object
              properties:
                subject:
                  type: string
                email:
                  type: string
                display_name:
                  type: string
                groups:
                  type: array
                  items:
                    type: string
      responses:
        '200':
          description: User record
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/User'
  '/api/v1/auth/users/upsert-local':
    post:
      tags:
        - Auth
      summary: Upsert the local-admin user
      description: '**UI BFF only.** Called after the UI validates the local-admin password.'
      x-internal: true
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              type: object
              properties:
                username:
                  type: string
      responses:
        '200':
          description: User record
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/User'
  '/api/v1/auth/users/{id}':
    get:
      tags:
        - Auth
      summary: Refetch a user record
      description: |
        **UI BFF only.** Used to refresh role / display_name during a session.

        Every call (200 and 404) emits a `user.fetched` audit row so iterative enumeration of user ids leaves a per-id trail (SEC_REVIEW F17). The route group is also rate-limited per service-token id (SEC_REVIEW F14).
      x-internal: true
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: User record
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/User'
        '404':
          description: No user with this id
          content:
            'application/json':
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: not_found
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Token in the form `irdb_<kind>_<32 base32 chars>`.
        See `doc/auth-flows.md` for the four token kinds.
  parameters:
    Page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    PageSize:
      name: page_size
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    ActingUserId:
      name: X-Acting-User-Id
      in: header
      description: |
        Required when authenticating with a `service` token.
        The api applies RBAC for the named user. Ignored on other token kinds.
      schema:
        type: integer
  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
          example: unauthorized
        details:
          type: object
          description: Field-level errors for `validation_failed`.
          additionalProperties:
            type: string
    ReportRequest:
      type: object
      required:
        - ip
        - category
      properties:
        ip:
          type: string
          example: '203.0.113.42'
        category:
          type: string
          example: brute_force
        metadata:
          type: object
          description: Free-form per-report data, max 4 KB after json_encode.
          additionalProperties: true
    ReportResponse:
      type: object
      properties:
        report_id:
          type: integer
          example: 12345
        ip:
          type: string
          example: '203.0.113.42'
        received_at:
          type: string
          format: date-time
    BlocklistEntry:
      type: object
      properties:
        ip_or_cidr:
          type: string
          example: '203.0.113.42'
        categories:
          type: array
          items:
            type: string
        score:
          type: number
          format: float
          example: 1.42
        reason:
          type: string
          enum:
            - scored
            - manual
          example: scored
    BlocklistJson:
      type: object
      properties:
        count:
          type: integer
          example: 42
        generated_at:
          type: string
          format: date-time
        policy:
          type: string
          example: moderate
        entries:
          type: array
          items:
            '$ref': '#/components/schemas/BlocklistEntry'
    Token:
      type: object
      properties:
        id:
          type: integer
        kind:
          type: string
          enum:
            - reporter
            - consumer
            - admin
        prefix:
          type: string
          example: irdb_adm
        reporter_id:
          type: integer
          nullable: true
        consumer_id:
          type: integer
          nullable: true
        role:
          type: string
          nullable: true
          enum:
            - viewer
            - operator
            - admin
            - null
        user_id:
          type: integer
          nullable: true
          description: |
            For admin-kind tokens issued via the API, the id of the admin
            user who minted the token. Reporter, consumer, service, and
            admin tokens minted via `bin/console tokens:create` carry null.
            When non-null, the token is rejected (401) at auth time if the
            issuer is later disabled or demoted below the token's role.
            (SEC_REVIEW F16.)
        user_label:
          type: string
          nullable: true
          description: |
            Denormalised display name (or email) of the issuing user;
            null when `user_id` is null or the user has since been deleted.
            Surfaced on list responses only — not part of the create
            response.
        expires_at:
          type: string
          format: date-time
          nullable: true
        revoked_at:
          type: string
          format: date-time
          nullable: true
        last_used_at:
          type: string
          format: date-time
          nullable: true
    TokenCreated:
      allOf:
        - '$ref': '#/components/schemas/Token'
        - type: object
          properties:
            raw_token:
              type: string
              description: Returned ONCE on creation — copy it now, never displayed again.
              example: irdb_adm_AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD
    Reporter:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
          example: web-prod-01
        description:
          type: string
          nullable: true
        trust_weight:
          type: number
          format: float
          minimum: 0
          maximum: 2
        is_active:
          type: boolean
        audit_enabled:
          type: boolean
          description: When false, suppresses `report.received` audit rows for this reporter even if the global toggle is on.
    Consumer:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
          example: edge-fw-01
        description:
          type: string
          nullable: true
        policy_id:
          type: integer
        is_active:
          type: boolean
        audit_enabled:
          type: boolean
          description: When false, suppresses `blocklist.requested` audit rows for this consumer even if the global toggle is on.
        last_pulled_at:
          type: string
          format: date-time
          nullable: true
    Category:
      type: object
      properties:
        id:
          type: integer
        slug:
          type: string
          example: brute_force
        name:
          type: string
        description:
          type: string
          nullable: true
        decay_function:
          type: string
          enum:
            - linear
            - exponential
        decay_param:
          type: number
          format: float
        is_active:
          type: boolean
    Policy:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
          example: moderate
        description:
          type: string
          nullable: true
        include_manual_blocks:
          type: boolean
        thresholds:
          type: object
          description: '`{category_slug: threshold}`'
          additionalProperties:
            type: number
            format: float
    ManualBlock:
      type: object
      properties:
        id:
          type: integer
        kind:
          type: string
          enum:
            - ip
            - subnet
        ip:
          type: string
          nullable: true
        cidr:
          type: string
          nullable: true
        reason:
          type: string
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    AllowlistEntry:
      type: object
      properties:
        id:
          type: integer
        kind:
          type: string
          enum:
            - ip
            - subnet
        ip:
          type: string
          nullable: true
        cidr:
          type: string
          nullable: true
        reason:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    IpDetail:
      type: object
      properties:
        ip:
          type: string
        is_ipv4:
          type: boolean
        scores:
          type: array
          items:
            type: object
            properties:
              category:
                type: string
              score:
                type: number
                format: float
              last_report_at:
                type: string
                format: date-time
                nullable: true
              report_count_30d:
                type: integer
        enrichment:
          type: object
          properties:
            country_code:
              type: string
              nullable: true
            asn:
              type: integer
              nullable: true
            as_org:
              type: string
              nullable: true
            enriched_at:
              type: string
              format: date-time
              nullable: true
        status:
          type: string
          enum:
            - scored
            - manually_blocked
            - allowlisted
            - clean
        manual_block:
          type: object
          nullable: true
        allowlist:
          type: object
          nullable: true
        history:
          type: array
          items:
            type: object
        has_more:
          type: boolean
    AuditEntry:
      type: object
      properties:
        id:
          type: integer
        occurred_at:
          type: string
          format: date-time
        actor_kind:
          type: string
          enum:
            - user
            - admin-token
            - reporter
            - consumer
            - system
        actor_id:
          type: string
          nullable: true
        actor_via:
          type: string
          nullable: true
          description: |
            SEC_REVIEW F11. Upstream auth path used by the actor. `oidc` /
            `local` are subdivisions of `actor_kind=user`; the others mirror
            their `actor_kind`. NULL on rows written before the column was
            added.
          enum:
            - oidc
            - local
            - admin-token
            - service
            - reporter
            - consumer
            - system
        action:
          type: string
          example: manual_block.created
        entity_type:
          type: string
          nullable: true
        entity_id:
          type: string
          nullable: true
        entity_label:
          type: string
          nullable: true
          description: Human-readable identifier for the target (name, slug, IP, CIDR, prefix). Frozen at write time.
        details:
          type: object
          nullable: true
          additionalProperties: true
          description: 'For update events, contains a `changes` map of `{field: {from, to}}` for every modified field.'
        source_ip:
          type: string
          nullable: true
    JobStatus:
      type: object
      properties:
        name:
          type: string
        default_interval_seconds:
          type: integer
        max_runtime_seconds:
          type: integer
        overdue:
          type: boolean
        last_run:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            status:
              type: string
              enum:
                - success
                - failure
                - skipped_locked
                - running
            items_processed:
              type: integer
            triggered_by:
              type: string
              enum:
                - schedule
                - manual
                - api
            started_at:
              type: string
              format: date-time
              nullable: true
            finished_at:
              type: string
              format: date-time
              nullable: true
            error_message:
              type: string
              nullable: true
    JobOutcome:
      type: object
      properties:
        job:
          type: string
        status:
          type: string
          enum:
            - success
            - failure
            - skipped_locked
            - running
        items_processed:
          type: integer
        duration_ms:
          type: integer
        run_id:
          type: integer
          nullable: true
        error:
          type: string
          nullable: true
    User:
      type: object
      properties:
        id:
          type: integer
        email:
          type: string
          nullable: true
        display_name:
          type: string
        role:
          type: string
          enum:
            - viewer
            - operator
            - admin
        source:
          type: string
          enum:
            - oidc
            - local
            - admin-token
        is_local:
          type: boolean
    UserRecord:
      type: object
      description: |
        Admin-API representation of a `users` row. Shared by
        `/api/v1/admin/users` list/get/patch.
      properties:
        id:
          type: integer
        subject:
          type: string
          nullable: true
          description: OIDC `sub` claim (null for the local-admin row).
        email:
          type: string
          nullable: true
        display_name:
          type: string
          nullable: true
        role:
          type: string
          enum:
            - viewer
            - operator
            - admin
        is_local:
          type: boolean
        disabled:
          type: boolean
        disabled_at:
          type: string
          format: date-time
          nullable: true
    Pagination:
      type: object
      properties:
        page:
          type: integer
          minimum: 1
          example: 1
        page_size:
          type: integer
          minimum: 1
          maximum: 200
          example: 50
        total:
          type: integer
          minimum: 0
          example: 1284
