openapi: 3.0.3
info:
  title: Hello WED API
  version: 1.0.0
  description: >
    This is a test API for candidate assessment.
    Endpoints provide user authentication (login, refresh tokens)
    and image management (uploading, listing, and fetching images).

    - **Any username/password** combination (with minimum format) can log in.
      We do not store or verify real credentials.
    - **Choose a unique username** so you don't conflict with other candidates.
    - **Images may expire in 1 week**.
    - **/images routes** (POST, GET) require a valid Bearer token,
      but **/images/{path}** is world-readable if the URL is known.

servers:
  - url: https://hello.wed.dev/

tags:
  - name: Authentication
    description: Endpoints for managing user authentication and tokens
  - name: Images
    description: Endpoints for uploading and retrieving images

paths:
  /auth/login:
    post:
      tags:
        - Authentication
      summary: Generate an access and refresh token
      description: >
        Any username/password that meets the minimum format requirements is accepted in this test API.
        **Important:** Choose a unique username to avoid collisions with other users.
      requestBody:
        $ref: '#/components/requestBodies/LoginRequest'
      responses:
        '201':
          description: Successfully logged in
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  access_token:
                    type: string
                    description: >
                      JWT access token to be used as a Bearer token; expires in 1 hour.
                  refresh_token:
                    type: string
                    description: >
                      JWT refresh token to obtain new access tokens; expires in 24 hours.
                required:
                  - success
                  - access_token
                  - refresh_token
        '401':
          $ref: '#/components/responses/InvalidCredentialsError'

  /auth/refresh:
    post:
      tags:
        - Authentication
      summary: Refresh tokens using the provided refresh token
      requestBody:
        $ref: '#/components/requestBodies/RefreshRequest'
      responses:
        '201':
          description: Successfully refreshed the tokens
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  access_token:
                    type: string
                    description: >
                      New access token replacing the expired one; expires in 1 hour.
                  refresh_token:
                    type: string
                    description: >
                      New refresh token; expires in 24 hours.
                required:
                  - success
                  - access_token
                  - refresh_token
        '401':
          description: Returned if the refresh token was not provided or invalid
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                required:
                  - success
                  - message
              examples:
                missing:
                  summary: Missing token
                  value:
                    success: false
                    message: "Refresh token not given"
                invalid:
                  summary: Invalid token
                  value:
                    success: false
                    message: "Invalid refresh token"

  /images:
    post:
      tags:
        - Images
      summary: Upload an image (Bearer token required)
      description: >
        Uploads an image to our storage.
        Only `image/*` content is accepted.
        Images may expire after 1 week.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: The image file to upload
                caption:
                  type: string
                  description: >
                    An optional caption for the image;
                    defaults to the filename of the image if not provided
              required:
                - file
            encoding:
              file:
                contentType: image/*
      responses:
        '201':
          description: Successfully uploaded the image
          headers:
            Location:
              description: The URL of the newly uploaded image
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: File uploaded successfully.
                  image:
                    $ref: '#/components/schemas/Image'
                required:
                  - success
                  - message
                  - image
        '400':
          description: No valid file found in the form data
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                required:
                  - success
                  - message
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '415':
          $ref: '#/components/responses/UnsupportedMediaTypeError'

    get:
      tags:
        - Images
      summary: List all uploaded images (Bearer token required)
      description: >
        Returns a list of images in reverse chronological order.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: List of images in reverse chronological order
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  images:
                    type: array
                    items:
                      $ref: '#/components/schemas/Image'
                required:
                  - success
                  - images
        '401':
          $ref: '#/components/responses/UnauthorizedError'

  /images/{path}:
    get:
      tags:
        - Images
      summary: Get an image by path (world-readable)
      description: >
        Returns the image file if found.
        The `path` may include subdirectories.
        This endpoint does **not** require a Bearer token;
        any user with the direct URL can retrieve it.
      parameters:
        - name: path
          in: path
          required: true
          description: The subdirectory/path (containing slashes) to the image file
          schema:
            type: string
      responses:
        '200':
          description: Image found
          content:
            image/jpeg:
              schema:
                type: string
                format: binary
            image/png:
              schema:
                type: string
                format: binary
        '404':
          $ref: '#/components/responses/ImageNotFoundError'

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

  schemas:
    Image:
      type: object
      properties:
        url:
          type: string
          description: The URL of the newly uploaded image
        size:
          type: integer
          description: Size of the uploaded file in bytes
          minimum: 1
        content_type:
          type: string
          description: MIME type of the uploaded file
        caption:
          type: string
          description: >
            The caption for the image; defaults to the filename if not
            provided, which may be an empty string
      required:
        - url
        - size
        - content_type
        - caption
    LoginSchema:
      type: object
      properties:
        username:
          type: string
          description: "Must be >=8 chars"
          minLength: 8
        password:
          type: string
          description: "Must be >=8 chars, alphanumeric/underscore"
          minLength: 8
          pattern: "^[a-zA-Z0-9_]{8,}$"
      required:
        - username
        - password
    RefreshSchema:
      type: object
      properties:
        refresh_token:
          type: string
      required:
        - refresh_token

  requestBodies:
    LoginRequest:
      description: Request body for login
      required: true
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/LoginSchema'
        application/x-www-form-urlencoded:
          schema:
            $ref: '#/components/schemas/LoginSchema'
    RefreshRequest:
      description: Request body for refreshing tokens
      required: true
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/LoginSchema'
        application/x-www-form-urlencoded:
          schema:
            $ref: '#/components/schemas/LoginSchema'

  responses:
    UnauthorizedError:
      description: Unauthorized (missing or invalid access token)
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                example: false
              message:
                type: string
                example: "Unauthorized"
            required:
              - success
              - message

    UnsupportedMediaTypeError:
      description: Only image/* content type is accepted
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                example: false
              message:
                type: string
                example: "Only image files are allowed"
            required:
              - success
              - message

    InvalidCredentialsError:
      description: Invalid username or password
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                example: false
              message:
                type: string
                example: "Invalid username or password"
            required:
              - success
              - message

    ImageNotFoundError:
      description: Image not found
      content: {}
