openapi: 3.0.3
info:
  title: Post Analyzer Webserver API
  description: |
    Enterprise-grade post management and analysis platform, backed by a
    Kitex RPC microservices architecture (gateway, postsvc, authsvc) behind
    a Hertz HTTP edge.

    ## Architecture
    The HTTP API below is served entirely by the **gateway** service. The
    gateway holds no data of its own — `posts` requests are proxied over
    Kitex RPC to `postsvc`, and every request (except `/auth/login` and the
    unauthenticated health/metrics endpoints) is authorized against
    `authsvc`'s ABAC policy decision point.

    ## Authentication
    `POST /api/v1/auth/login` exchanges username/password for a JWT. Send
    it as `Authorization: Bearer <token>` on every other `/api/v1/*`
    request. Demo accounts: `admin/admin123` (role `admin`), `editor/editor123`
    (role `editor`), `viewer/viewer123` (role `viewer`).

    ## ABAC policy summary
    - `viewer`: read-only.
    - `editor`: read + write; delete requires an `X-MFA-Verified: true`
      header (attribute-based condition, not just role membership).
    - `admin`: unrestricted.
  version: 3.0.0
  contact:
    name: Post Analyzer Team
    url: https://github.com/hoangsonww/Post-Analyzer-Webserver
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

servers:
  - url: http://localhost:8080
    description: Gateway, direct (local dev)
  - url: http://localhost
    description: Through nginx (docker-compose)

tags:
  - name: Auth
    description: Login / JWT issuance
  - name: Posts
    description: Post CRUD and bulk operations
  - name: Analysis
    description: Character-frequency analysis and async reanalysis
  - name: Exports
    description: Generated post exports, persisted to MinIO
  - name: ML
    description: Nvidia Triton sentiment classification
  - name: Health
    description: Health, readiness, and Prometheus metrics
  - name: Admin
    description: ABAC admin-only operational status (covered by the admin-full-access policy)

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  parameters:
    PostId:
      name: id
      in: path
      required: true
      schema: { type: integer }
      description: Post ID
    MfaVerifiedHeader:
      name: X-MFA-Verified
      in: header
      required: false
      schema: { type: string, enum: ["true", "false"] }
      description: Required "true" for editor-role DELETE requests (ABAC attribute condition)

  schemas:
    Post:
      type: object
      properties:
        id: { type: integer, example: 1 }
        userId: { type: integer, example: 1 }
        title: { type: string, example: "Hello World" }
        body: { type: string, example: "Post content here" }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    CreatePostRequest:
      type: object
      required: [title, body]
      properties:
        userId: { type: integer, example: 1 }
        title: { type: string, maxLength: 500 }
        body: { type: string, maxLength: 10000 }

    UpdatePostRequest:
      type: object
      properties:
        title: { type: string, maxLength: 500 }
        body: { type: string, maxLength: 10000 }

    BulkCreateRequest:
      type: object
      required: [posts]
      properties:
        posts:
          type: array
          minItems: 1
          maxItems: 1000
          items: { $ref: "#/components/schemas/CreatePostRequest" }

    BulkCreateResponse:
      type: object
      properties:
        created: { type: integer }
        failed: { type: integer }
        errors: { type: array, items: { type: string } }
        postIds: { type: array, items: { type: integer } }

    PaginationMeta:
      type: object
      properties:
        page: { type: integer }
        pageSize: { type: integer }
        totalItems: { type: integer }
        totalPages: { type: integer }
        hasNext: { type: boolean }
        hasPrev: { type: boolean }

    ResponseMeta:
      type: object
      properties:
        requestId: { type: string }
        timestamp: { type: string, format: date-time }
        duration: { type: integer, description: "nanoseconds, when present" }

    AnalyticsResult:
      type: object
      properties:
        totalPosts: { type: integer }
        totalCharacters: { type: integer }
        uniqueChars: { type: integer }
        topCharacters:
          type: array
          items:
            type: object
            properties:
              character: { type: string }
              count: { type: integer }
              frequency: { type: number }
        statistics:
          type: object
          properties:
            averagePostLength: { type: number }
            medianPostLength: { type: integer }
            postsPerUser: { type: object, additionalProperties: { type: integer } }
            timeDistribution: { type: object, additionalProperties: { type: integer } }

    ExportObjectInfo:
      type: object
      properties:
        Key: { type: string, example: "exports/posts_export_20260101_120000.json" }
        Size: { type: integer }
        LastModified: { type: string, format: date-time }
        ContentType: { type: string }

    SentimentResponse:
      type: object
      properties:
        label: { type: string, enum: [positive, negative, neutral] }
        probabilities:
          type: object
          additionalProperties: { type: number }
          example: { positive: 0.79, negative: 0.12, neutral: 0.09 }

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string }
            message: { type: string }
            fields: { type: object, additionalProperties: { type: string } }
        meta: { $ref: "#/components/schemas/ResponseMeta" }

security:
  - bearerAuth: []

paths:
  /health:
    get:
      tags: [Health]
      summary: Liveness check (unauthenticated)
      security: []
      responses:
        "200": { description: Healthy }

  /readiness:
    get:
      tags: [Health]
      summary: Readiness check — verifies postsvc RPC is reachable (unauthenticated)
      security: []
      responses:
        "200": { description: Ready }
        "503": { description: Not ready }

  /metrics:
    get:
      tags: [Health]
      summary: Prometheus metrics (unauthenticated)
      security: []
      responses:
        "200":
          description: Prometheus text exposition format
          content:
            text/plain: { schema: { type: string } }

  /api/v1/auth/login:
    post:
      tags: [Auth]
      summary: Exchange credentials for a JWT
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, password]
              properties:
                username: { type: string, example: admin }
                password: { type: string, example: admin123 }
      responses:
        "200":
          description: Login succeeded
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      token: { type: string }
                      username: { type: string }
                      role: { type: string }
                  meta: { $ref: "#/components/schemas/ResponseMeta" }
        "401":
          description: Invalid credentials
          content:
            application/json: { schema: { $ref: "#/components/schemas/Error" } }

  /api/v1/posts:
    get:
      tags: [Posts]
      summary: List posts
      description: "ABAC action: read"
      parameters:
        - { name: userId, in: query, schema: { type: integer } }
        - { name: search, in: query, schema: { type: string } }
        - { name: sortBy, in: query, schema: { type: string, enum: [id, title, createdAt, updatedAt] } }
        - { name: sortOrder, in: query, schema: { type: string, enum: [asc, desc] } }
        - { name: page, in: query, schema: { type: integer, default: 1 } }
        - { name: pageSize, in: query, schema: { type: integer, default: 20, maximum: 100 } }
      responses:
        "200":
          description: Paginated post list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Post" } }
                  pagination: { $ref: "#/components/schemas/PaginationMeta" }
                  meta: { $ref: "#/components/schemas/ResponseMeta" }
        "401": { description: Missing/invalid token }
        "403": { description: ABAC denied }
    post:
      tags: [Posts]
      summary: Create a post
      description: "ABAC action: write"
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: "#/components/schemas/CreatePostRequest" } }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Post" }
                  meta: { $ref: "#/components/schemas/ResponseMeta" }
        "422": { description: Validation error }

  /api/v1/posts/{id}:
    get:
      tags: [Posts]
      summary: Get a post by ID
      description: "ABAC action: read"
      parameters: [{ $ref: "#/components/parameters/PostId" }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Post" }
        "404": { description: Not found }
    put:
      tags: [Posts]
      summary: Update a post
      description: "ABAC action: write"
      parameters: [{ $ref: "#/components/parameters/PostId" }]
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: "#/components/schemas/UpdatePostRequest" } }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Post" }
        "404": { description: Not found }
    delete:
      tags: [Posts]
      summary: Delete a post
      description: "ABAC action: delete. Editors additionally need X-MFA-Verified: true."
      parameters:
        - { $ref: "#/components/parameters/PostId" }
        - { $ref: "#/components/parameters/MfaVerifiedHeader" }
      responses:
        "200": { description: Deleted }
        "403": { description: ABAC denied (e.g. editor without MFA header) }
        "404": { description: Not found }

  /api/v1/posts/bulk:
    post:
      tags: [Posts]
      summary: Bulk-create posts
      description: "ABAC action: write"
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: "#/components/schemas/BulkCreateRequest" } }
      responses:
        "201": { description: All created }
        "207":
          description: Partial success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/BulkCreateResponse" }

  /api/v1/posts/export:
    get:
      tags: [Exports]
      summary: Export posts as JSON or CSV
      description: |
        Streams the export in the response body AND (when MinIO is
        enabled) persists a copy — see GET /api/v1/exports to list them.
        ABAC action: read.
      parameters:
        - { name: format, in: query, schema: { type: string, enum: [json, csv], default: json } }
        - { name: userId, in: query, schema: { type: integer } }
        - { name: search, in: query, schema: { type: string } }
      responses:
        "200":
          description: Export file
          content:
            application/json: { schema: { type: array, items: { $ref: "#/components/schemas/Post" } } }
            text/csv: { schema: { type: string } }

  /api/v1/posts/analytics:
    get:
      tags: [Analysis]
      summary: Character-frequency analysis across all posts (synchronous)
      description: "ABAC action: read"
      responses:
        "200":
          description: Analysis result
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AnalyticsResult" }
                  meta: { $ref: "#/components/schemas/ResponseMeta" }

  /api/v1/posts/reanalyze:
    post:
      tags: [Analysis]
      summary: Enqueue an async reanalysis job (RabbitMQ)
      description: |
        Returns immediately with a job ID; cmd/reanalysis-worker performs
        the actual analysis asynchronously. Requires RABBITMQ_ENABLED=true
        on the gateway. ABAC action: write.
      responses:
        "202":
          description: Job queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      jobId: { type: string }
                      status: { type: string, example: queued }
        "503": { description: RabbitMQ not enabled }

  /api/v1/exports:
    get:
      tags: [Exports]
      summary: List previously generated exports (MinIO-backed)
      description: "ABAC action: read"
      responses:
        "200":
          description: List of stored exports
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/ExportObjectInfo" } }
        "503": { description: MinIO not enabled }

  /api/v1/exports/{key}:
    get:
      tags: [Exports]
      summary: Download a previously generated export by key
      description: "ABAC action: read"
      parameters:
        - { name: key, in: path, required: true, schema: { type: string }, example: "posts_export_20260101_120000.json" }
      responses:
        "200": { description: File contents }
        "404": { description: Export not found }
        "503": { description: MinIO not enabled }

  /api/v1/ml/sentiment:
    post:
      tags: [ML]
      summary: Classify the sentiment of arbitrary text (Nvidia Triton)
      description: |
        Same model postsvc uses to auto-enrich newly created posts (when
        both Kafka and Triton are enabled). ABAC action: write.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string, example: "I love this, fantastic work" }
      responses:
        "200":
          description: Classification result
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/SentimentResponse" }

  /api/v1/admin/status:
    get:
      tags: [Admin]
      summary: Operational status — environment, uptime, RPC addresses, which optional integrations are enabled
      description: |
        ABAC-gated separately from the `post` resource (resource: `admin`,
        action: `read`) — covered by the `admin-full-access` wildcard
        policy, so only the `admin` role can call this. Used by the
        dashboard's admin panel.
      responses:
        "200":
          description: Status snapshot
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      environment: { type: string, example: production }
                      uptime: { type: string, example: "1h23m45.678s" }
                      integrations:
                        type: object
                        additionalProperties: { type: boolean }
                        example: { redis: true, kafka: true, rabbitmq: true, rocketmq: true, minio: true, triton: true, rpc_mux: true }
                      rpc:
                        type: object
                        properties:
                          postsvc: { type: string, example: "postsvc:9001" }
                          authsvc: { type: string, example: "authsvc:9002" }
                      requestedBy: { type: string, example: admin }
        "403": { description: Forbidden — caller is not an admin }
        "503": { description: Triton not enabled }
