openapi: 3.1.0
x-document-control:
  stand: "09.08.2026 15:33"
  branch: main
  status: historisch
  replaced_by: ./openapi-uc00.yaml
  change_history:
    - version: "1.0.1-doc"
      date_time: "09.08.2026 15:33"
      change: "Als historischen API-Block gekennzeichnet; verbindlicher UC00-Vertrag in openapi-uc00.yaml"
      author: Codex

info:
  title: "DiveLogix360 API – Block 3: User-Management (Tenant-Admin)"
  version: 1.0.0
  contact:
    name: DiveLogix360 Support
    url: https://divelogix360.ch
  description: |
    > **Historisch:** Dieser Block bleibt als Arbeitsstand erhalten. Fuer UC00 ist
    > `openapi-uc00.yaml` verbindlich.

    ## Block 3: User-Management

    Dieser Block beschreibt alle API-Endpoints zur Verwaltung von Benutzern
    innerhalb eines Tenants durch den Tenant-Admin.

    **Zugriff:** Tenant-Admin. Alle Endpoints sind mit `BearerAuth` (JWT) gesichert
    und prüfen die Rolle `tenant_admin`. Tenant-Isolation wird serverseitig via
    RLS (Row-Level Security) erzwungen — ein Tenant-Admin sieht ausschliesslich
    Benutzer seines eigenen Tenants.

    **Selbst-Modifikation:**
    Ein Tenant-Admin kann seine eigene Rolle nicht ändern, sich selbst nicht sperren
    und sich selbst nicht löschen (`self_modification_forbidden`). Dies verhindert
    versehentliche Lockout-Szenarien. Rollenänderungen des eigenen Accounts
    können nur durch den SuperAdmin vorgenommen werden.

    **Benutzer-Lebenszyklus:**
    ```
    invited → active → suspended → deactivated
                ↑__________↓
    ```
    - `invited`     : Einladungs-Token verschickt, Account noch nicht aktiviert
    - `active`      : Benutzer aktiv, kann sich einloggen
    - `suspended`   : Temporär gesperrt (laufende Sessions werden invalidiert)
    - `deactivated` : Dauerhaft deaktiviert (Soft-Delete, Daten bleiben erhalten)

    **Plan-Limits:**
    Beim Einladen neuer Benutzer wird geprüft ob das Benutzer-Limit des Plans
    noch nicht erreicht ist (`plan_limit_exceeded`).

servers:
  - url: https://api.divelogix360.ch/v1
    description: Produktion (Hetzner Cloud, Go-Live)
  - url: https://api-dev.divelogix360.ch/v1
    description: Entwicklung / Staging

tags:
  - name: Users
    description: Benutzer-Verwaltung (Tenant-Admin)
  - name: Invitations
    description: Einladungs-Verwaltung (Tenant-Admin)

# ============================================================
# PATHS
# ============================================================

paths:

  # ----------------------------------------------------------
  # POST /users/invite  — Benutzer einladen
  # ----------------------------------------------------------
  /users/invite:
    post:
      tags: [Users, Invitations]
      summary: Benutzer einladen
      description: |
        Erstellt einen Einladungs-Token und versendet eine Einladungs-E-Mail
        an die angegebene E-Mail-Adresse (Onboarding-Modell C1).

        **Ablauf:**
        1. Tenant-Admin gibt E-Mail, Vorname, Nachname und Rolle ein
        2. System prüft Plan-Limit (max. Benutzer erreicht?)
        3. System prüft ob E-Mail bereits im Tenant existiert
        4. Einladungs-Token wird generiert (UUID, 72h gültig, einmalig verwendbar)
        5. E-Mail wird an eingeladene Person verschickt (Link mit Token)
        6. Einladung erscheint in `GET /users/invitations` mit Status `pending`

        **Mehrfach-Einladung:**
        Wenn für eine E-Mail bereits eine offene Einladung existiert, wird diese
        invalidiert und eine neue verschickt (Token-Reset, neue 72h).

        **Rollen die eingeladen werden können:**
        - `mitarbeiter` — Standardrolle für neue Benutzer
        - `tenant_admin` — Weiterer Admin (mind. 1 Admin muss immer bestehen)

        **Nicht einladbar:**
        - `superadmin` — Wird nur direkt angelegt, nie via Invitation
        - `kunde` — Kunden-Einladung ist separater Flow (UC03, geplant)
      operationId: inviteUser
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InviteUserRequest'
            examples:
              invite_mitarbeiter:
                summary: Mitarbeiter einladen
                value:
                  email: "neuer.mitarbeiter@tauchschule-muster.ch"
                  first_name: "Anna"
                  last_name: "Beispiel"
                  role: "mitarbeiter"
              invite_admin:
                summary: Weiteren Admin einladen
                value:
                  email: "zweiter.admin@tauchschule-muster.ch"
                  first_name: "Beat"
                  last_name: "Muster"
                  role: "tenant_admin"
      responses:
        '201':
          description: Einladung erfolgreich erstellt und E-Mail verschickt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvitationResponse'
              example:
                id: "inv-a3f9b2c1-1234-5678-abcd-ef0123456789"
                email: "neuer.mitarbeiter@tauchschule-muster.ch"
                first_name: "Anna"
                last_name: "Beispiel"
                role: "mitarbeiter"
                status: "pending"
                expires_at: "2024-01-18T10:30:00Z"
                created_at: "2024-01-15T10:30:00Z"
                created_by: "admin@tauchschule-muster.ch"
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: E-Mail bereits als aktiver Benutzer im Tenant registriert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "user_already_exists"
                message: "Ein aktiver Benutzer mit dieser E-Mail-Adresse existiert bereits im Tenant."
                status: 409
        '422':
          description: Plan-Limit erreicht
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "plan_limit_exceeded"
                message: "Benutzer-Limit des aktuellen Plans erreicht (max. 2). Bitte upgraden Sie Ihren Plan."
                status: 422
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ----------------------------------------------------------
  # GET /users  — Benutzer auflisten
  # ----------------------------------------------------------
  /users:
    get:
      tags: [Users]
      summary: Alle Benutzer des Tenants auflisten
      description: |
        Gibt eine paginierte Liste aller Benutzer des eigenen Tenants zurück.

        **Tenant-Isolation:** Serverseitig via RLS erzwungen.
        Ein Tenant-Admin sieht ausschliesslich Benutzer seines Tenants.

        **Filter:** Status, Rolle, Suchbegriff (Name, E-Mail)
      operationId: listUsers
      security:
        - BearerAuth: []
      parameters:
        - name: status
          in: query
          description: Nach Status filtern
          schema:
            type: string
            enum: [invited, active, suspended, deactivated]
        - name: role
          in: query
          description: Nach Rolle filtern
          schema:
            type: string
            enum: [tenant_admin, mitarbeiter]
        - name: search
          in: query
          description: Suche in Name und E-Mail
          schema:
            type: string
            example: "Anna"
        - name: sort_by
          in: query
          schema:
            type: string
            enum: [created_at, last_name, status]
            default: last_name
        - name: sort_dir
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: asc
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: after
          in: query
          description: Cursor für die nächste Seite
          schema:
            type: string
      responses:
        '200':
          description: Liste der Benutzer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserListResponse'
              example:
                data:
                  - id: "c1d2e3f4-1234-5678-abcd-ef0123456789"
                    email: "anna.beispiel@tauchschule-muster.ch"
                    first_name: "Anna"
                    last_name: "Beispiel"
                    role: "mitarbeiter"
                    status: "active"
                    two_fa_enabled: false
                    created_at: "2024-01-15T10:30:00Z"
                    last_login_at: "2024-01-20T08:15:00Z"
                pagination:
                  total: 3
                  limit: 20
                  has_more: false
                  next_cursor: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ----------------------------------------------------------
  # GET /users/{userId}  — Einzelnen Benutzer abrufen
  # ----------------------------------------------------------
  /users/{userId}:
    parameters:
      - $ref: '#/components/parameters/UserId'

    get:
      tags: [Users]
      summary: Einzelnen Benutzer abrufen
      description: |
        Gibt vollständige Daten eines Benutzers zurück.
        Tenant-Isolation via RLS: Zugriff nur auf Benutzer des eigenen Tenants.
      operationId: getUser
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Benutzer-Daten
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

    # ----------------------------------------------------------
    # PATCH /users/{userId}  — Benutzer-Daten aktualisieren
    # ----------------------------------------------------------
    patch:
      tags: [Users]
      summary: Benutzer-Stammdaten aktualisieren
      description: |
        Aktualisiert Stammdaten eines Benutzers (Name, E-Mail).

        **Partial Update:** Nur übermittelte Felder werden geändert.

        **Nicht änderbar via diesem Endpoint:**
        - `role` → via `PATCH /users/{userId}/role`
        - `status` → via `PATCH /users/{userId}/status`
        - `password` → separater Flow (Passwort-Reset, geplant)

        **Selbst-Modifikation:** Erlaubt für Stammdaten (Name, E-Mail).
        Nicht erlaubt für Rolle und Status (siehe Beschreibung oben).

        **E-Mail-Änderung:** Wenn E-Mail geändert wird, muss die neue
        E-Mail-Adresse bestätigt werden (Verifikations-E-Mail). Bis zur
        Bestätigung bleibt die alte E-Mail aktiv.
      operationId: updateUser
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateUserRequest'
            example:
              first_name: "Anna-Maria"
              last_name: "Beispiel-Neu"
      responses:
        '200':
          description: Benutzer erfolgreich aktualisiert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: E-Mail bereits von anderem Benutzer verwendet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "email_already_in_use"
                message: "Diese E-Mail-Adresse wird bereits von einem anderen Benutzer verwendet."
                status: 409
        '500':
          $ref: '#/components/responses/InternalServerError'

    # ----------------------------------------------------------
    # DELETE /users/{userId}  — Benutzer löschen (Soft-Delete)
    # ----------------------------------------------------------
    delete:
      tags: [Users]
      summary: Benutzer löschen (Soft-Delete)
      description: |
        Markiert einen Benutzer als gelöscht (Soft-Delete).

        **Soft-Delete:** Der Datensatz bleibt in der Datenbank erhalten
        (`deleted_at`, `deleted_by` werden gesetzt). Personendaten werden
        nach Ablauf der DSGVO-Aufbewahrungsfrist pseudonymisiert.

        **Auswirkungen:**
        - Benutzer kann sich nicht mehr einloggen
        - Laufende Sessions werden sofort invalidiert
        - Verknüpfte Workflow-Daten (TÜV-Protokolle etc.) bleiben erhalten

        **Selbst-Schutz:** Tenant-Admin kann sich selbst nicht löschen
        (`self_modification_forbidden`).

        **Letzter Admin:** Wenn der zu löschende Benutzer der letzte
        Tenant-Admin ist, wird der Löschvorgang abgelehnt (`last_admin_protected`).
      operationId: deleteUser
      security:
        - BearerAuth: []
      responses:
        '204':
          description: Benutzer erfolgreich gelöscht (kein Body)
        '400':
          description: Löschvorgang nicht erlaubt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                self_modification:
                  summary: Selbst-Löschung nicht erlaubt
                  value:
                    error: "self_modification_forbidden"
                    message: "Sie können Ihren eigenen Account nicht löschen."
                    status: 400
                last_admin:
                  summary: Letzter Admin geschützt
                  value:
                    error: "last_admin_protected"
                    message: "Der letzte Tenant-Admin kann nicht gelöscht werden. Bitte zuerst einen weiteren Admin ernennen."
                    status: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ----------------------------------------------------------
  # PATCH /users/{userId}/role  — Rolle ändern
  # ----------------------------------------------------------
  /users/{userId}/role:
    patch:
      tags: [Users]
      summary: Benutzer-Rolle ändern
      description: |
        Ändert die Rolle eines Benutzers.

        **Erlaubte Übergänge:**
        | Von           | Nach          | Beschreibung                        |
        |---------------|---------------|-------------------------------------|
        | mitarbeiter   | tenant_admin  | Beförderung zum Admin               |
        | tenant_admin  | mitarbeiter   | Degradierung zum Mitarbeiter        |

        **Selbst-Schutz:** Tenant-Admin kann seine eigene Rolle nicht ändern
        (`self_modification_forbidden`).

        **Letzter Admin:** Degradierung des letzten Tenant-Admins wird abgelehnt
        (`last_admin_protected`). Mindestens 1 Tenant-Admin muss immer vorhanden sein.

        **Auswirkungen einer Degradierung:**
        - Laufende Sessions bleiben aktiv (kein Zwangs-Logout)
        - Beim nächsten Token-Refresh wird die neue Rolle im JWT übernommen
        - Zugriff auf Admin-Funktionen ist sofort eingeschränkt (RLS)
      operationId: updateUserRole
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/UserId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateUserRoleRequest'
            examples:
              promote:
                summary: Zum Admin befördern
                value:
                  role: "tenant_admin"
                  reason: "Übernahme der Teamleitung."
              demote:
                summary: Zum Mitarbeiter degradieren
                value:
                  role: "mitarbeiter"
                  reason: "Wechsel der Zuständigkeit."
      responses:
        '200':
          description: Rolle erfolgreich geändert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserRoleResponse'
              example:
                id: "c1d2e3f4-1234-5678-abcd-ef0123456789"
                role: "tenant_admin"
                previous_role: "mitarbeiter"
                changed_at: "2024-01-20T09:00:00Z"
                changed_by: "admin@tauchschule-muster.ch"
                reason: "Übernahme der Teamleitung."
        '400':
          description: Rollenänderung nicht erlaubt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                self_modification:
                  value:
                    error: "self_modification_forbidden"
                    message: "Sie können Ihre eigene Rolle nicht ändern."
                    status: 400
                last_admin:
                  value:
                    error: "last_admin_protected"
                    message: "Der letzte Tenant-Admin kann nicht degradiert werden."
                    status: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ----------------------------------------------------------
  # PATCH /users/{userId}/status  — Status ändern
  # ----------------------------------------------------------
  /users/{userId}/status:
    patch:
      tags: [Users]
      summary: Benutzer-Status ändern
      description: |
        Ändert den Status eines Benutzers (sperren, entsperren, deaktivieren).

        **Erlaubte Übergänge:**
        | Von         | Nach        | Beschreibung                            |
        |-------------|-------------|-----------------------------------------|
        | active      | suspended   | Temporär sperren                        |
        | suspended   | active      | Sperre aufheben                         |
        | active      | deactivated | Dauerhaft deaktivieren                  |
        | suspended   | deactivated | Dauerhaft deaktivieren                  |

        **Nicht erlaubt:**
        - `deactivated` → irgendein anderer Status (final, nur SuperAdmin kann reaktivieren)
        - Rücksprung zu `invited`

        **Selbst-Schutz:** Tenant-Admin kann sich selbst nicht sperren
        (`self_modification_forbidden`).

        **Auswirkung von `suspended`:**
        - Laufende Sessions werden sofort invalidiert
        - Kein Login möglich bis Entsperrung

        **Auswirkung von `deactivated`:**
        - Wie `suspended`, zusätzlich dauerhaft
        - Für Reaktivierung ist SuperAdmin-Eingriff erforderlich
      operationId: updateUserStatus
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/UserId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateUserStatusRequest'
            examples:
              suspend:
                summary: Benutzer sperren
                value:
                  status: "suspended"
                  reason: "Mitarbeiter ist erkrankt, Sicherheitsmassnahme."
              unsuspend:
                summary: Sperre aufheben
                value:
                  status: "active"
                  reason: "Rückkehr nach Krankheit bestätigt."
              deactivate:
                summary: Dauerhaft deaktivieren
                value:
                  status: "deactivated"
                  reason: "Arbeitsverhältnis beendet per 31.01.2024."
      responses:
        '200':
          description: Status erfolgreich geändert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserStatusResponse'
              example:
                id: "c1d2e3f4-1234-5678-abcd-ef0123456789"
                status: "suspended"
                previous_status: "active"
                changed_at: "2024-01-20T09:00:00Z"
                changed_by: "admin@tauchschule-muster.ch"
                reason: "Mitarbeiter ist erkrankt, Sicherheitsmassnahme."
        '400':
          description: Statusänderung nicht erlaubt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                self_modification:
                  value:
                    error: "self_modification_forbidden"
                    message: "Sie können Ihren eigenen Status nicht ändern."
                    status: 400
                invalid_transition:
                  value:
                    error: "invalid_status_transition"
                    message: "Übergang von 'deactivated' zu 'active' ist nicht erlaubt."
                    status: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ----------------------------------------------------------
  # GET /users/invitations  — Offene Einladungen auflisten
  # ----------------------------------------------------------
  /users/invitations:
    get:
      tags: [Invitations]
      summary: Einladungen auflisten
      description: |
        Gibt eine Liste aller Einladungen des Tenants zurück.

        **Filter:** Status (`pending`, `used`, `expired`, `revoked`)
        Standardmässig werden nur `pending` Einladungen angezeigt.
      operationId: listInvitations
      security:
        - BearerAuth: []
      parameters:
        - name: status
          in: query
          description: "Nach Status filtern (Standard: pending)"
          schema:
            type: string
            enum: [pending, used, expired, revoked]
            default: pending
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: after
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Liste der Einladungen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvitationListResponse'
              example:
                data:
                  - id: "inv-a3f9b2c1-1234-5678-abcd-ef0123456789"
                    email: "neuer.mitarbeiter@tauchschule-muster.ch"
                    first_name: "Anna"
                    last_name: "Beispiel"
                    role: "mitarbeiter"
                    status: "pending"
                    expires_at: "2024-01-18T10:30:00Z"
                    created_at: "2024-01-15T10:30:00Z"
                    created_by: "admin@tauchschule-muster.ch"
                pagination:
                  total: 1
                  limit: 20
                  has_more: false
                  next_cursor: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ----------------------------------------------------------
  # DELETE /users/invitations/{invitationId}  — Einladung widerrufen
  # ----------------------------------------------------------
  /users/invitations/{invitationId}:
    delete:
      tags: [Invitations]
      summary: Einladung widerrufen
      description: |
        Widerruft eine offene Einladung (Status `pending` → `revoked`).

        **Nur möglich für:** Einladungen mit Status `pending`.
        Bereits eingelöste (`used`) oder abgelaufene (`expired`) Einladungen
        können nicht widerrufen werden.

        **Auswirkung:** Der Einladungs-Link in der E-Mail wird sofort ungültig.
        Eine neue Einladung kann danach für dieselbe E-Mail erstellt werden.
      operationId: revokeInvitation
      security:
        - BearerAuth: []
      parameters:
        - name: invitationId
          in: path
          required: true
          description: ID der Einladung
          schema:
            type: string
            format: uuid
            example: "inv-a3f9b2c1-1234-5678-abcd-ef0123456789"
      responses:
        '204':
          description: Einladung erfolgreich widerrufen (kein Body)
        '400':
          description: Einladung kann nicht widerrufen werden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                already_used:
                  summary: Bereits eingelöst
                  value:
                    error: "invitation_already_used"
                    message: "Diese Einladung wurde bereits eingelöst und kann nicht widerrufen werden."
                    status: 400
                already_expired:
                  summary: Bereits abgelaufen
                  value:
                    error: "invitation_already_expired"
                    message: "Diese Einladung ist bereits abgelaufen."
                    status: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

# ============================================================
# COMPONENTS
# ============================================================

components:

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT Access Token (RS256, 15 Minuten Laufzeit).
        Wird im `Authorization: Bearer <token>` Header gesendet.
        Muss die Rolle `tenant_admin` enthalten.

  parameters:
    UserId:
      name: userId
      in: path
      required: true
      description: UUID des Benutzers
      schema:
        type: string
        format: uuid
        example: "c1d2e3f4-1234-5678-abcd-ef0123456789"

  schemas:

    # ----------------------------------------------------------
    # Request Schemas
    # ----------------------------------------------------------

    InviteUserRequest:
      type: object
      required: [email, first_name, last_name, role]
      properties:
        email:
          type: string
          format: email
          description: E-Mail-Adresse der einzuladenden Person
        first_name:
          type: string
          minLength: 1
          maxLength: 50
          description: Vorname
        last_name:
          type: string
          minLength: 1
          maxLength: 50
          description: Nachname
        role:
          type: string
          enum: [tenant_admin, mitarbeiter]
          description: Rolle die der Benutzer nach Aktivierung erhält

    UpdateUserRequest:
      type: object
      description: Partial Update – nur übermittelte Felder werden geändert
      properties:
        first_name:
          type: string
          minLength: 1
          maxLength: 50
        last_name:
          type: string
          minLength: 1
          maxLength: 50
        email:
          type: string
          format: email
          description: |
            Neue E-Mail-Adresse. Löst Verifikations-E-Mail aus.
            Bis zur Bestätigung bleibt die alte E-Mail aktiv.

    UpdateUserRoleRequest:
      type: object
      required: [role]
      properties:
        role:
          type: string
          enum: [tenant_admin, mitarbeiter]
          description: Neue Rolle
        reason:
          type: string
          nullable: true
          maxLength: 500
          description: Begründung (wird in audit_log gespeichert)

    UpdateUserStatusRequest:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [active, suspended, deactivated]
          description: Neuer Status
        reason:
          type: string
          nullable: true
          maxLength: 500
          description: Begründung (wird in audit_log gespeichert)

    # ----------------------------------------------------------
    # Response Schemas
    # ----------------------------------------------------------

    UserResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        first_name:
          type: string
        last_name:
          type: string
        role:
          type: string
          enum: [tenant_admin, mitarbeiter]
        status:
          type: string
          enum: [invited, active, suspended, deactivated]
        two_fa_enabled:
          type: boolean
          description: Gibt an ob 2FA eingerichtet ist
        tenant_id:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        last_login_at:
          type: string
          format: date-time
          nullable: true

    UserListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/UserListItem'
        pagination:
          $ref: '#/components/schemas/Pagination'

    UserListItem:
      type: object
      description: Kurzform für Listen
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        first_name:
          type: string
        last_name:
          type: string
        role:
          type: string
          enum: [tenant_admin, mitarbeiter]
        status:
          type: string
          enum: [invited, active, suspended, deactivated]
        two_fa_enabled:
          type: boolean
        created_at:
          type: string
          format: date-time
        last_login_at:
          type: string
          format: date-time
          nullable: true

    UserRoleResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        role:
          type: string
          enum: [tenant_admin, mitarbeiter]
        previous_role:
          type: string
          enum: [tenant_admin, mitarbeiter]
        changed_at:
          type: string
          format: date-time
        changed_by:
          type: string
          format: email
        reason:
          type: string
          nullable: true

    UserStatusResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [active, suspended, deactivated]
        previous_status:
          type: string
          enum: [invited, active, suspended, deactivated]
        changed_at:
          type: string
          format: date-time
        changed_by:
          type: string
          format: email
        reason:
          type: string
          nullable: true

    InvitationResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        first_name:
          type: string
        last_name:
          type: string
        role:
          type: string
          enum: [tenant_admin, mitarbeiter]
        status:
          type: string
          enum: [pending, used, expired, revoked]
        expires_at:
          type: string
          format: date-time
          description: Ablaufzeitpunkt (72h ab Erstellung)
        created_at:
          type: string
          format: date-time
        created_by:
          type: string
          format: email
          description: E-Mail des Tenant-Admins der die Einladung erstellt hat

    InvitationListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/InvitationResponse'
        pagination:
          $ref: '#/components/schemas/Pagination'

    # ----------------------------------------------------------
    # Shared Schemas
    # ----------------------------------------------------------

    Pagination:
      type: object
      properties:
        total:
          type: integer
        limit:
          type: integer
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true

    ErrorResponse:
      type: object
      required: [error, message, status]
      properties:
        error:
          type: string
        message:
          type: string
        status:
          type: integer

    ValidationErrorResponse:
      allOf:
        - $ref: '#/components/schemas/ErrorResponse'
        - type: object
          properties:
            details:
              type: array
              items:
                type: object
                properties:
                  field:
                    type: string
                  code:
                    type: string
                  message:
                    type: string

  # ----------------------------------------------------------
  # Reusable Responses
  # ----------------------------------------------------------
  responses:

    ValidationError:
      description: Validierungsfehler
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationErrorResponse'

    Unauthorized:
      description: Nicht authentifiziert
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "unauthorized"
            message: "Authentifizierung erforderlich."
            status: 401

    Forbidden:
      description: Keine Berechtigung (falsche Rolle oder anderer Tenant)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "forbidden"
            message: "Zugriff verweigert."
            status: 403

    NotFound:
      description: Ressource nicht gefunden
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "not_found"
            message: "Benutzer nicht gefunden."
            status: 404

    InternalServerError:
      description: Interner Serverfehler
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "internal_server_error"
            message: "Ein unerwarteter Fehler ist aufgetreten."
            status: 500
