openapi: 3.1.0
info:
  title: Stromcom Auth — User Management API
  description: |
    Programmatic user management for `auth.stromcom.cz`. Designed for
    service-account integrations (M2M) — every request is authenticated with a
    Bearer JWT issued through the `client_credentials` grant, and every endpoint
    requires the calling client to hold a specific `auth.users.*` role in
    `oauth_clients.service_roles`.

    ## Authentication

    1. Obtain an access token from `POST /oauth/token` using `grant_type=client_credentials`.
    2. Attach it to every API request as `Authorization: Bearer <token>`.

    Tokens expire after one hour. Role membership is read from the **current**
    client row on every request, so role revocations take effect immediately
    even on otherwise-valid tokens.

    ## Required roles

    | Endpoint                       | Role                  |
    |--------------------------------|-----------------------|
    | `GET    /api/v1/users`         | `auth.users.read`     |
    | `GET    /api/v1/users/{id}`    | `auth.users.read`     |
    | `POST   /api/v1/users`         | `auth.users.create`   |
    | `PATCH  /api/v1/users/{id}`    | `auth.users.update`   |
    | `DELETE /api/v1/users/{id}`    | `auth.users.delete`   |

    ## Error shape

    Every non-2xx response follows the unified envelope:

    ```json
    { "error": { "code": "validation_failed", "message": "...", "details": { } } }
    ```

  version: 1.0.0
  contact:
    name: Stromcom Auth
    url: https://auth.stromcom.cz/docs/integration

servers:
  - url: http://1fkowcaa32.execute-api.eu-central-1.amazonaws.com
    description: Current server (auto-detected)

tags:
  - name: Users
    description: Manage users in the Identity context

security:
  - bearerAuth: []

paths:
  /api/v1/users:
    get:
      tags: [Users]
      summary: List users
      description: |
        Returns all users matching the optional filters. Required role:
        `auth.users.read`.
      operationId: listUsers
      parameters:
        - in: query
          name: search
          schema: { type: string }
          description: Substring match against email or name (case-insensitive).
        - in: query
          name: status
          schema:
            type: string
            enum: [all, active, inactive, locked]
            default: all
        - in: query
          name: admin
          schema: { type: boolean }
          description: When set, filter by `is_admin` flag.
        - in: query
          name: role
          schema: { type: string }
          description: Role name (dotted, e.g. `translator.editor`).
        - in: query
          name: group
          schema: { type: string }
          description: Group name (lowercase alnum + `_-`).
      responses:
        '200':
          description: User list
          content:
            application/json:
              schema:
                type: object
                required: [items, count]
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/User' }
                  count:
                    type: integer
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

    post:
      tags: [Users]
      summary: Create a user
      description: |
        Creates a new user. Required role: `auth.users.create`.

        Pass `password` to set one explicitly; otherwise pass
        `generate_password=true` to have the server mint a random one and
        return it **once** in the response (`generated_password` field). If
        neither is provided the user is created without a password (e.g. for
        future SSO linking).
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, name]
              properties:
                email: { type: string, format: email, example: "alice@stromcom.cz" }
                name:  { type: string, maxLength: 200, example: "Alice Example" }
                password:
                  type: string
                  minLength: 10
                  description: Plaintext password — at least 10 chars. Omit to skip or use `generate_password`.
                generate_password:
                  type: boolean
                  description: When true and `password` is empty, server generates and returns one.
                is_admin:  { type: boolean, default: false }
                is_active: { type: boolean, default: true }
                groups:
                  type: array
                  items: { type: string }
                  description: Group names to attach immediately after creation.
                  example: ["beta", "vip-users"]
      responses:
        '201':
          description: Created
          headers:
            Location:
              schema: { type: string }
              description: Canonical URL of the new user.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/User'
                  - type: object
                    properties:
                      generated_password:
                        type: string
                        description: Present only when the server generated the password. Returned **once** — store it now.
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409':
          description: Email already taken
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/users/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: integer, minimum: 1 }

    get:
      tags: [Users]
      summary: Get a user by id
      description: "Required role: `auth.users.read`."
      operationId: getUser
      responses:
        '200':
          description: User
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

    patch:
      tags: [Users]
      summary: Update user profile / membership
      description: |
        Partial update — only fields present in the body are touched. To
        replace the group membership, pass the full desired set as `groups`.
        To set a new password, pass `password` (plaintext, ≥10 chars).

        Required role: `auth.users.update`.
      operationId: updateUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:           { type: string, maxLength: 200 }
                is_active:      { type: boolean }
                is_admin:       { type: boolean }
                email_verified: { type: boolean }
                locale:         { type: string, example: "cs" }
                zoneinfo:       { type: string, example: "Europe/Prague" }
                picture:        { type: string, format: uri }
                password:       { type: string, minLength: 10 }
                groups:
                  type: array
                  items: { type: string }
                  description: When present, fully replaces group membership.
      responses:
        '200':
          description: Updated user
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

    delete:
      tags: [Users]
      summary: Delete a user
      description: |
        Default is soft-delete (sets `is_active=false`). Pass `?hard=true` to
        remove the row entirely — only allowed in develop environments.

        Required role: `auth.users.delete`.
      operationId: deleteUser
      parameters:
        - in: query
          name: hard
          schema: { type: boolean, default: false }
      responses:
        '200':
          description: Deletion outcome
          content:
            application/json:
              schema:
                type: object
                required: [deleted, id, hard]
                properties:
                  deleted: { type: boolean }
                  id:      { type: integer }
                  hard:    { type: boolean }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: Missing role, or `hard=true` outside develop.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '404': { $ref: '#/components/responses/NotFound' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Service-account access token obtained via `POST /oauth/token`
        (`grant_type=client_credentials`). Token's client must hold the role
        required by the endpoint in `oauth_clients.service_roles`.

  schemas:
    User:
      type: object
      required: [id, email, name, is_active, is_admin, email_verified, roles, groups, created_at]
      properties:
        id:             { type: integer, example: 42 }
        email:          { type: string, format: email }
        name:           { type: string }
        is_active:      { type: boolean }
        is_admin:       { type: boolean }
        email_verified: { type: boolean }
        locale:         { type: string, nullable: true }
        zoneinfo:       { type: string, nullable: true }
        picture:        { type: string, format: uri, nullable: true }
        roles:
          type: array
          items: { type: string }
          example: ["translator.editor"]
        groups:
          type: array
          items: { type: string }
          example: ["beta"]
        last_login_at:  { type: string, format: date-time, nullable: true }
        profile_updated_at: { type: string, format: date-time, nullable: true }
        created_at:     { type: string, format: date-time }

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:    { type: string, example: "validation_failed" }
            message: { type: string }
            details:
              description: Optional per-field validation messages or supplementary data.
              oneOf:
                - type: object
                  additionalProperties: true
                - type: array
                  items: {}

  responses:
    Unauthorized:
      description: Missing, invalid, or expired Bearer token.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Authenticated, but the client lacks the required role.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Resource does not exist.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ValidationFailed:
      description: Request body or query parameters failed validation.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
