# Proposed contract, version 1: endpoints, errors, paging, idempotency, security and the capability of every
# operation. Resource schemas were initialized from the data model (docs/06-data) and are kept aligned with it; input
# schemas (*Input, *Request) and views are specific to this contract. Status: proposal for review, not confirmed design.
# Conventions: docs/05-interfaces/choosing-an-interface.md. Vendor extensions: x-moonitoring-audience (who calls),
# x-moonitoring-capability (the capability checked), x-moonitoring-entitlement (the entitlement feature checked),
# x-moonitoring-requirements (the requirements of 1.requisiti-raffinati.txt the operation serves).
openapi: 3.1.0
info:
  title: News Articles API
  version: 1.0.0
  description: 'The articles of MooNitoring News: drafts and their translations, review, publication, previews and
    images. Called by the Console; readers never call it, they read the static site built from what is published.'
servers:
- url: https://console.moonitoring.com/api/news/articles/v1
  description: Prod, through the Console's host; the backend has no host of its own
- url: https://stage-console.moonitoring.com/api/news/articles/v1
  description: Stage, through the Console's host
security:
- bearer: []
tags:
- name: Articles
  description: Articles and their metadata
- name: Translations
  description: An article in one language
- name: Workflow
  description: Review, publication and previews
- name: Assets
  description: Images
paths:
  /articles:
    get:
      summary: List articles
      operationId: listArticles
      description: Every article with the state of each translation, newest change first.
      tags:
      - Articles
      parameters:
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - draft
          - in_review
          - scheduled
          - published
          - unpublished
        description: Only articles in this status.
      - name: tag
        in: query
        required: false
        schema:
          type: string
      - $ref: ../../../../common/v1/schemas.yaml#/components/parameters/Cursor
      - $ref: ../../../../common/v1/schemas.yaml#/components/parameters/Limit
      responses:
        '200':
          description: One page of results.
          content:
            application/json:
              schema:
                type: object
                description: One page of items.
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/Article'
                  next_cursor:
                    type: string
                    description: Where the next page starts; absent on the last page.
                required:
                - items
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
      x-moonitoring-audience: staff
      x-moonitoring-capability: news.articles.write
      x-moonitoring-requirements:
      - '147'
    post:
      summary: Create an article
      operationId: createArticle
      description: A draft with its first translation; the caller becomes its first author.
      tags:
      - Articles
      parameters:
      - $ref: ../../../../common/v1/schemas.yaml#/components/parameters/IdempotencyKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ArticleInput'
      responses:
        '201':
          description: The result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Article'
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '422':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/UnprocessableEntity
      x-moonitoring-audience: staff
      x-moonitoring-capability: news.articles.write
      x-moonitoring-requirements:
      - '147'
      - '148'
      - '1006.45'
  /articles/{article_id}:
    get:
      summary: Read an article
      operationId: getArticle
      description: The article with every translation, published or not.
      tags:
      - Articles
      parameters:
      - name: article_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: The result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Article'
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '404':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/NotFound
      x-moonitoring-audience: staff
      x-moonitoring-capability: news.articles.write
      x-moonitoring-requirements:
      - '147'
      - '148'
    patch:
      summary: Update an article
      operationId: updateArticle
      description: Authors, cover and tags. A change to a published article is visible after the next site build, within minutes.
      tags:
      - Articles
      parameters:
      - name: article_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ArticleChanges'
      responses:
        '200':
          description: The result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Article'
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '404':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/NotFound
        '409':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Conflict
        '422':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/UnprocessableEntity
      x-moonitoring-audience: staff
      x-moonitoring-capability: news.articles.write
      x-moonitoring-requirements:
      - '147'
  /articles/{article_id}/translations/{locale}:
    put:
      summary: Write a translation
      operationId: putArticleTranslation
      description: Creates or replaces the article in one language. Changing the slug of a published translation needs news.articles.publish and keeps the old slug as a permanent redirect.
      tags:
      - Translations
      parameters:
      - name: article_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: locale
        in: path
        required: true
        schema:
          type: string
          enum:
          - it
          - en
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ArticleTranslationInput'
      responses:
        '200':
          description: The result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ArticleTranslation'
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '404':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/NotFound
        '409':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Conflict
        '422':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/UnprocessableEntity
      x-moonitoring-audience: staff
      x-moonitoring-capability: news.articles.write
      x-moonitoring-requirements:
      - '148'
      - '148.12'
      - '1006.45'
    delete:
      summary: Remove a translation
      operationId: deleteArticleTranslation
      description: Removes one language. Refused for the last ready translation of a published article; unpublish it instead.
      tags:
      - Translations
      parameters:
      - name: article_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: locale
        in: path
        required: true
        schema:
          type: string
          enum:
          - it
          - en
      responses:
        '204':
          description: Removed.
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '404':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/NotFound
        '409':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Conflict
      x-moonitoring-audience: staff
      x-moonitoring-capability: news.articles.write
      x-moonitoring-requirements:
      - '148'
  /articles/{article_id}/submit:
    post:
      summary: Submit an article for review
      operationId: submitArticle
      description: From draft to in review; staff with news.articles.publish are told.
      tags:
      - Workflow
      parameters:
      - name: article_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: The result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Article'
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '404':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/NotFound
        '409':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Conflict
        '422':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/UnprocessableEntity
      x-moonitoring-audience: staff
      x-moonitoring-capability: news.articles.write
      x-moonitoring-requirements:
      - '147.2'
  /articles/{article_id}/publish:
    post:
      summary: Publish an article
      operationId: publishArticle
      description: Publishes the ready translations now, or schedules them when publish_at is in the future. Refused when a ready translation lacks a slug, a summary or the cover alt text.
      tags:
      - Workflow
      parameters:
      - name: article_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublishRequest'
      responses:
        '200':
          description: The result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Article'
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '404':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/NotFound
        '409':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Conflict
        '422':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/UnprocessableEntity
      x-moonitoring-audience: staff
      x-moonitoring-capability: news.articles.publish
      x-moonitoring-requirements:
      - '147'
      - '147.6'
      - '1006.46'
  /articles/{article_id}/unpublish:
    post:
      summary: Unpublish an article
      operationId: unpublishArticle
      description: Its pages answer 410 Gone and leave the sitemap at the next site build.
      tags:
      - Workflow
      parameters:
      - name: article_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: The result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Article'
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '404':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/NotFound
        '409':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Conflict
      x-moonitoring-audience: staff
      x-moonitoring-capability: news.articles.publish
      x-moonitoring-requirements:
      - '147.2'
      - '1006.46'
  /articles/{article_id}/previews:
    post:
      summary: Open a preview
      operationId: createArticlePreview
      description: A preview address on the Console host, valid for 30 minutes, that renders one translation exactly as readers will see it, published or not.
      tags:
      - Workflow
      parameters:
      - name: article_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreviewRequest'
      responses:
        '201':
          description: The result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Preview'
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '404':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/NotFound
      x-moonitoring-audience: staff
      x-moonitoring-capability: news.articles.write
      x-moonitoring-requirements:
      - '147'
  /assets:
    get:
      summary: List images
      operationId: listNewsAssets
      description: Images uploaded for News, newest first.
      tags:
      - Assets
      parameters:
      - $ref: ../../../../common/v1/schemas.yaml#/components/parameters/Cursor
      - $ref: ../../../../common/v1/schemas.yaml#/components/parameters/Limit
      responses:
        '200':
          description: One page of results.
          content:
            application/json:
              schema:
                type: object
                description: One page of items.
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/NewsAsset'
                  next_cursor:
                    type: string
                    description: Where the next page starts; absent on the last page.
                required:
                - items
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
      x-moonitoring-audience: staff
      x-moonitoring-capability: news.articles.write
      x-moonitoring-requirements:
      - '149'
    post:
      summary: Upload an image
      operationId: createNewsAsset
      description: Registers an image and returns a signed URL valid for 15 minutes; the image goes straight to the media bucket and is checked before it can be used.
      tags:
      - Assets
      parameters:
      - $ref: ../../../../common/v1/schemas.yaml#/components/parameters/IdempotencyKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewsAssetInput'
      responses:
        '201':
          description: The result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NewsAssetUpload'
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '422':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/UnprocessableEntity
      x-moonitoring-audience: staff
      x-moonitoring-capability: news.articles.write
      x-moonitoring-requirements:
      - '149'
      - '1006.45'
components:
  schemas:
    Article:
      description: One news article, in one or more languages.
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum:
          - draft
          - in_review
          - scheduled
          - published
          - unpublished
        authors:
          type: array
          items:
            type: string
            format: uuid
          description: IDs of MoonitoringUsers; the first is shown in previews.
        cover:
          type: string
          format: uuid
          description: ID of a NewsAsset.
        tags:
          type: array
          items:
            type: string
        publish_at:
          type: string
          format: date-time
        published_at:
          type: string
          format: date-time
        content_updated_at:
          type: string
          format: date-time
        reviewed_by:
          type: string
          format: uuid
        translations:
          type: array
          items:
            $ref: '#/components/schemas/ArticleTranslation'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
      - id
      - status
      - authors
      - translations
      - created_at
      - updated_at
    ArticleTranslation:
      description: The article in one language, with its own address.
      type: object
      properties:
        locale:
          type: string
          enum:
          - it
          - en
        slug:
          type: string
          pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
        url:
          type: string
          format: uri
          description: Where readers find it once published, e.g. https://news.moonitoring.com/it/<slug>.
        title:
          type: string
          maxLength: 70
        seo_title:
          type: string
          maxLength: 60
        summary:
          type: string
          minLength: 50
          maxLength: 160
        body:
          type: string
          description: Markdown, the subset of the data model; no raw HTML.
        status:
          type: string
          enum:
          - draft
          - ready
        social_image:
          type: string
          format: uuid
          description: ID of a NewsAsset of 1200x630; absent, the site generates one from the cover and the title.
        cover_alt:
          type: string
          maxLength: 250
        previous_slugs:
          type: array
          items:
            type: string
        updated_at:
          type: string
          format: date-time
      required:
      - locale
      - slug
      - title
      - status
    ArticleInput:
      type: object
      properties:
        tags:
          type: array
          items:
            type: string
        cover:
          type: string
          format: uuid
        translation:
          $ref: '#/components/schemas/ArticleTranslationInputWithLocale'
      required:
      - translation
    ArticleChanges:
      type: object
      properties:
        authors:
          type: array
          items:
            type: string
            format: uuid
          minItems: 1
        cover:
          type:
          - string
          - 'null'
          format: uuid
        tags:
          type: array
          items:
            type: string
    ArticleTranslationInput:
      type: object
      properties:
        slug:
          type: string
          pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
        title:
          type: string
          maxLength: 70
        seo_title:
          type: string
          maxLength: 60
        summary:
          type: string
          maxLength: 160
        body:
          type: string
        status:
          type: string
          enum:
          - draft
          - ready
          default: draft
        social_image:
          type: string
          format: uuid
        cover_alt:
          type: string
          maxLength: 250
      required:
      - slug
      - title
    ArticleTranslationInputWithLocale:
      allOf:
      - $ref: '#/components/schemas/ArticleTranslationInput'
      - type: object
        properties:
          locale:
            type: string
            enum:
            - it
            - en
        required:
        - locale
    PublishRequest:
      type: object
      properties:
        publish_at:
          type: string
          format: date-time
          description: Absent or in the past publishes now; in the future schedules it.
    PreviewRequest:
      type: object
      properties:
        locale:
          type: string
          enum:
          - it
          - en
      required:
      - locale
    Preview:
      type: object
      properties:
        url:
          type: string
          format: uri
          description: An address under https://console.moonitoring.com/preview/news/, behind Identity-Aware Proxy.
        expires_at:
          type: string
          format: date-time
      required:
      - url
      - expires_at
    NewsAsset:
      description: An image uploaded for News.
      type: object
      properties:
        id:
          type: string
          format: uuid
        content_type:
          type: string
          enum:
          - image/jpeg
          - image/png
          - image/webp
          - image/avif
        width:
          type: integer
        height:
          type: integer
        bytes:
          type: integer
        description:
          type: string
        status:
          type: string
          enum:
          - pending
          - ready
          - rejected
          description: ready once the uploaded file was checked against what was declared.
        uploaded_by:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
      required:
      - id
      - content_type
      - status
      - uploaded_by
      - created_at
    NewsAssetInput:
      type: object
      properties:
        content_type:
          type: string
          enum:
          - image/jpeg
          - image/png
          - image/webp
          - image/avif
        bytes:
          type: integer
          maximum: 10485760
        sha256:
          type: string
          pattern: ^[a-f0-9]{64}$
        description:
          type: string
      required:
      - content_type
      - bytes
      - sha256
    NewsAssetUpload:
      type: object
      properties:
        asset:
          $ref: '#/components/schemas/NewsAsset'
        upload_url:
          type: string
          format: uri
          description: A signed PUT URL of the media bucket.
        expires_at:
          type: string
          format: date-time
      required:
      - asset
      - upload_url
      - expires_at
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: 'The short-lived staff token signed by `access`, sent by the Console through its own host.'
