openapi: 3.0.0
info:
  title: v1 combined specification
  version: 1.0.0
servers:
  - url: http://localhost:8080/api/v1
    description: local
tags:
  - name: auth
    x-displayName: auth
  - name: users
    x-displayName: users
  - name: dummy
    x-displayName: dummy
paths:
  /auth/signup:
    post:
      tags:
        - auth
      summary: Create a new user
      operationId: signup
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SignUpRequest'
      responses:
        '201':
          description: User created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          description: Invalid request
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          $ref: '#/components/responses/UserConflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/AuthenticationUnavailable'
        default:
          $ref: '#/components/responses/UnexpectedResponse'
      security: []
  /auth/login:
    post:
      tags:
        - auth
      summary: Login an existing user
      operationId: login
      description: >-
        Unknown email, incorrect password, and inactive accounts receive the
        same generic 400 response.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Successfully logged in
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tokens'
        '400':
          description: Invalid request
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/AuthenticationUnavailable'
        default:
          $ref: '#/components/responses/UnexpectedResponse'
      security: []
  /auth/refresh:
    post:
      tags:
        - auth
      summary: Rotate the session refresh token
      description: >-
        Refresh tokens are single-use. Reusing one ends that session, including
        successor tokens.
      operationId: refresh
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefreshRequest'
      responses:
        '200':
          description: Token refreshed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tokens'
        '400':
          description: Invalid request
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid, expired, reused or inactive session
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/AuthenticationUnavailable'
        default:
          $ref: '#/components/responses/UnexpectedResponse'
      security: []
  /auth/logout:
    post:
      tags:
        - auth
      summary: End the session
      description: >-
        The refresh token identifies the session. Repeated logout succeeds,
        including after access-token expiry.
      operationId: logout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LogoutRequest'
      responses:
        '200':
          description: Tokens invalidated successfully
        '400':
          description: Invalid request
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid refresh token
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AuthenticationUnavailable'
        default:
          $ref: '#/components/responses/UnexpectedResponse'
      security: []
  /users/me:
    get:
      tags:
        - users
      summary: Retrieve current user information
      operationId: users.me.read
      x-required-permissions:
        - users:read_self
      security:
        - jwt: []
        - apiKey: []
      responses:
        '200':
          description: User information retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '401':
          description: Unauthorized
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        default:
          $ref: '#/components/responses/UnexpectedResponse'
    put:
      tags:
        - users
      summary: Update current user credentials
      description: >-
        Requires the current password when a non-null email or password is
        supplied, including an unchanged email. Omitted or null email/password
        fields leave those credentials unchanged. A request with neither
        credential supplied is a successful no-op. Changing email or password
        ends all existing JWT sessions.
      operationId: users.me.update
      x-required-permissions:
        - users:update_self
      security:
        - jwt: []
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateSelfRequest'
      responses:
        '200':
          description: User updated
        '400':
          description: Invalid request
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/UserConflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/AuthenticationUnavailable'
        default:
          $ref: '#/components/responses/UnexpectedResponse'
  /users:
    get:
      tags:
        - users
      summary: List users
      operationId: users.list
      x-required-permissions:
        - users:list
      security:
        - jwt: []
        - apiKey: []
      parameters:
        - name: limit
          in: query
          description: >-
            Number of users to return. Omit or use 0 for the default of 10.
            Empty values are invalid.
          schema:
            type: integer
            default: 10
            minimum: 0
            maximum: 100
        - name: offset
          in: query
          description: >-
            Number of users to skip. Omit for the default of 0. Empty values are
            invalid.
          schema:
            type: integer
            default: 0
            minimum: 0
            maximum: 2147483647
      responses:
        '200':
          description: List of users
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'
        '400':
          description: Invalid pagination
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        default:
          $ref: '#/components/responses/UnexpectedResponse'
    post:
      tags:
        - users
      summary: Create a new user
      operationId: users.create
      x-required-permissions:
        - users:create
      security:
        - jwt: []
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
      responses:
        '201':
          description: User created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          description: Invalid request
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/UserConflict'
        default:
          $ref: '#/components/responses/UnexpectedResponse'
  /users/{uuid}:
    get:
      tags:
        - users
      summary: Get user by UUID
      operationId: users.get
      x-required-permissions:
        - users:read_others
      security:
        - jwt: []
        - apiKey: []
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: User information
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          description: Invalid UUID
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          $ref: '#/components/responses/UnexpectedResponse'
    put:
      tags:
        - users
      summary: Update user
      operationId: users.update
      x-required-permissions:
        - users:update
      security:
        - jwt: []
        - apiKey: []
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateUserRequest'
      responses:
        '200':
          description: User updated
        '400':
          description: Invalid request
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          $ref: '#/components/responses/UserConflict'
        default:
          $ref: '#/components/responses/UnexpectedResponse'
    delete:
      tags:
        - users
      summary: Delete user
      operationId: users.delete
      x-required-permissions:
        - users:delete
      security:
        - jwt: []
        - apiKey: []
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: User deleted
        '400':
          description: Invalid UUID
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          $ref: '#/components/responses/UnexpectedResponse'
  /dummy:
    get:
      tags:
        - dummy
      summary: dummy endpoint
      operationId: dummy
      responses:
        '200':
          description: successful operation
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: This is a dummy response
      security: []
components:
  securitySchemes:
    jwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
    apiKey:
      type: apiKey
      in: header
      name: X-Api-Key
  responses:
    Forbidden:
      description: Authenticated credentials lack the operation's required permission.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
    UserConflict:
      description: >-
        The normalized email is already used by another user. Rejected updates
        leave credentials and sessions unchanged.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: Too many authentication attempts
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
    AuthenticationUnavailable:
      description: Authentication storage or rate limiter is unavailable
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
    UnexpectedResponse:
      description: Unexpected response
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      required:
        - type
        - title
        - status
      properties:
        type:
          type: string
          description: A URI reference that identifies the problem type.
        title:
          type: string
          description: A short, human-readable summary of the problem type.
        status:
          type: integer
          format: int32
          description: The HTTP status code generated by the origin server.
        detail:
          type: string
          description: >-
            A human-readable explanation specific to this occurrence of the
            problem.
        instance:
          type: string
          description: >-
            A URI reference that identifies the specific occurrence of the
            problem.
        errors:
          type: array
          items:
            type: object
          description: Optional list of additional error details.
    User:
      type: object
      required:
        - uuid
        - email
        - created_at
        - roles
        - permissions
      properties:
        uuid:
          type: string
          format: uuid
        email:
          type: string
          format: email
          minLength: 1
        created_at:
          type: string
          description: Creation time in YYYY-MM-DD HH:MM:SS format.
          pattern: ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$
        roles:
          type: array
          items:
            type: string
        permissions:
          type: array
          items:
            type: string
    Tokens:
      type: object
      required:
        - access_token
        - refresh_token
        - expires_in
      properties:
        access_token:
          type: string
          minLength: 1
        refresh_token:
          type: string
          minLength: 1
        expires_in:
          type: integer
          minimum: 0
          description: Access-token lifetime in whole seconds, rounded down.
    SignUpRequest:
      type: object
      required:
        - email
        - password
      properties:
        email:
          type: string
          description: >-
            Trimmed, lowercased, then validated as an email address by the
            service.
          example: user@example.com
        password:
          type: string
          minLength: 8
          maxLength: 72
          description: >-
            At least 8 Unicode characters and at most 72 UTF-8 bytes. Used
            exactly as supplied, including whitespace. New passwords must also
            meet the configured strength requirement.
          example: 12#$abCD%$
    LoginRequest:
      type: object
      required:
        - email
        - password
      properties:
        email:
          type: string
          description: >-
            Trimmed, lowercased, then validated as an email address by the
            service.
          example: user@example.com
        password:
          type: string
          minLength: 8
          maxLength: 72
          description: >-
            At least 8 Unicode characters and at most 72 UTF-8 bytes. Used
            exactly as supplied, including whitespace.
          example: 12#$abCD%$
    RefreshRequest:
      type: object
      required:
        - token
      properties:
        token:
          type: string
          minLength: 1
          description: Nonempty refresh token. Nonempty invalid tokens return 401.
    LogoutRequest:
      type: object
      required:
        - refresh_token
      properties:
        access_token:
          description: Optional; logout uses the refresh token to identify the session.
          type: string
          default: ''
        refresh_token:
          type: string
          minLength: 1
          description: Nonempty refresh token. Nonempty invalid tokens return 401.
    CreateUserRequest:
      type: object
      required:
        - email
        - password
      properties:
        email:
          type: string
          description: >-
            Trimmed, lowercased, then validated as an email address by the
            service.
          example: user@example.com
        password:
          type: string
          minLength: 8
          maxLength: 72
          description: >-
            At least 8 Unicode characters and at most 72 UTF-8 bytes. Used
            exactly as supplied, including whitespace. New passwords must also
            meet the configured strength requirement.
          example: 12#$abCD%$
    UpdateUserRequest:
      type: object
      properties:
        email:
          type: string
          description: >-
            Trimmed, lowercased, then validated as an email address by the
            service.
          example: user@example.com
        password:
          type: string
          minLength: 8
          maxLength: 72
          description: >-
            At least 8 Unicode characters and at most 72 UTF-8 bytes. Used
            exactly as supplied, including whitespace. New passwords must also
            meet the configured strength requirement.
          example: 12#$abCD%$
    UpdateSelfRequest:
      type: object
      description: >-
        The correct current_password is required when email or password is
        non-null, even for an unchanged email. Omitted or null email/password
        fields are ignored. With neither credential supplied, the request
        succeeds without verifying current_password or changing credentials or
        sessions.
      not:
        allOf:
          - anyOf:
              - required:
                  - email
                properties:
                  email:
                    type: string
              - required:
                  - password
                properties:
                  password:
                    type: string
          - not:
              required:
                - current_password
              properties:
                current_password:
                  type: string
                  minLength: 8
                  maxLength: 72
      properties:
        current_password:
          type: string
          nullable: true
          description: >-
            Existing password, used exactly as supplied. When changing
            credentials, at least 8 Unicode characters and at most 72 UTF-8
            bytes. Ignored when neither email nor password is supplied with a
            non-null value.
          writeOnly: true
        email:
          type: string
          nullable: true
          description: >-
            Trimmed, lowercased, then validated as an email address by the
            service.
          example: user@example.com
        password:
          type: string
          nullable: true
          minLength: 8
          maxLength: 72
          description: >-
            At least 8 Unicode characters and at most 72 UTF-8 bytes. Used
            exactly as supplied, including whitespace. New passwords must also
            meet the configured strength requirement.
          example: 12#$abCD%$
x-tagGroups:
  - name: auth
    tags:
      - auth
      - users
  - name: dummy
    tags:
      - dummy
