openapi: 3.1.0
info:
  title: Modtale API
  version: v1
  description: |
    API reference generated from the current backend controllers and service behavior.

    Scope notes:
    - This spec intentionally excludes admin/internal-only endpoints and endpoints that are not reachable as part of the public API surface due to security constraints.
    - Authenticated endpoints accept either a session cookie (`JSESSIONID`) or an API key header (`X-MODTALE-KEY`) unless otherwise noted.
servers:
  - url: https://api.modtale.net
    description: Production
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-MODTALE-KEY
      description: API key from the developer dashboard.
    SessionAuth:
      type: apiKey
      in: cookie
      name: JSESSIONID
      description: Browser/session-based authentication cookie.
  schemas:
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
        message:
          type: string

    GameVersionEntry:
      type: object
      properties:
        version:
          type: string
        preRelease:
          type: boolean
        sourceUrl:
          type: string

    GameVersionCatalog:
      type: object
      properties:
        releaseVersions:
          type: array
          items:
            type: string
        preReleaseVersions:
          type: array
          items:
            type: string
        allVersions:
          type: array
          items:
            type: string
        versions:
          type: array
          items:
            $ref: '#/components/schemas/GameVersionEntry'

    CreateProjectRequest:
      type: object
      required: [title, classification, description]
      properties:
        title:
          type: string
        classification:
          type: string
          enum: [PLUGIN, DATA, ART, SAVE, MODPACK]
        description:
          type: string
        owner:
          type: string
          description: Optional organization username to create under.
        slug:
          type: string

    UpdateProjectRequest:
      type: object
      properties:
        title:
          type: string
        slug:
          type: string
        description:
          type: string
          description: Max 250 chars.
        about:
          type: string
          description: Max 50000 chars.
        tags:
          type: array
          items:
            type: string
        links:
          type: object
          additionalProperties:
            type: string
        repositoryUrl:
          type: string
          description: Must be HTTPS GitHub/GitLab/Codeberg repo URL.
        license:
          type: string
        allowModpacks:
          type: boolean
        allowComments:
          type: boolean
        hmWikiEnabled:
          type: boolean
        hmWikiSlug:
          type: string

    UpdateVersionRequest:
      type: object
      properties:
        modIds:
          type: array
          items:
            type: string
          description: "Dependency entries formatted as projectId:version or projectId:version:optional."
        gameVersions:
          type: array
          items:
            type: string
        changelog:
          type: string
        channel:
          type: string
          enum: [RELEASE, BETA, ALPHA]

    CommentRequest:
      type: object
      required: [content]
      properties:
        content:
          type: string

    UsersBatchRequest:
      type: object
      required: [userIds]
      properties:
        userIds:
          type: array
          items:
            type: string

    UpdateProfileRequest:
      type: object
      properties:
        bio:
          type: string
          description: Max 300 chars.
        username:
          type: string
          description: "3-30 chars using [a-zA-Z0-9_.-]."

    NotificationPreferences:
      type: object
      description: User notification preference object.
      additionalProperties:
        type: string

    CreateOrganizationRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string

    OrganizationRoleRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
        color:
          type: string
        permissions:
          type: array
          items:
            type: string

    AddOrganizationMemberRequest:
      type: object
      required: [userId, roleId]
      properties:
        userId:
          type: string
        roleId:
          type: string

    UpdateOrganizationMemberRoleRequest:
      type: object
      required: [roleId]
      properties:
        roleId:
          type: string

    UpdateOrganizationRequest:
      type: object
      properties:
        displayName:
          type: string
        name:
          type: string
        bio:
          type: string

    RegisterRequest:
      type: object
      required: [username, email, password]
      properties:
        username:
          type: string
        email:
          type: string
        password:
          type: string

    SignInRequest:
      type: object
      required: [username, password]
      properties:
        username:
          type: string
        password:
          type: string

    ForgotPasswordRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string

    ResetPasswordRequest:
      type: object
      required: [token, password]
      properties:
        token:
          type: string
        password:
          type: string

    MfaLoginRequest:
      type: object
      required: [pre_auth_token, code]
      properties:
        pre_auth_token:
          type: string
        code:
          type: string

    VerifyMfaRequest:
      type: object
      required: [code]
      properties:
        code:
          type: string

    UpdateCredentialsRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
        password:
          type: string

    ChangePasswordRequest:
      type: object
      required: [currentPassword, newPassword]
      properties:
        currentPassword:
          type: string
        newPassword:
          type: string

security:
  - ApiKeyAuth: []
  - SessionAuth: []

paths:
  /api/v1/tags:
    get:
      summary: List canonical project tags
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string

  /api/v1/meta/classifications:
    get:
      summary: List supported project classifications
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string

  /api/v1/meta/game-versions:
    get:
      summary: List supported game versions
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string

  /api/v1/meta/game-versions/catalog:
    get:
      summary: Get full game version catalog
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GameVersionCatalog'

  /api/v1/status:
    get:
      summary: Get platform health and latency history
      security: []
      parameters:
        - name: range
          in: query
          schema:
            type: string
            enum: [24h, 30d]
            default: 24h
      responses:
        '200':
          description: OK

  /api/v1/analytics/platform/stats:
    get:
      summary: Get public platform aggregate stats
      security: []
      responses:
        '200':
          description: OK

  /api/v1/wiki/{slug}:
    get:
      summary: Proxy wiki project metadata
      security: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
        '404':
          description: Wiki slug not found

  /api/v1/wiki/{slug}/{pagePath}:
    get:
      summary: Proxy wiki page payload
      description: "pagePath may represent nested wiki paths."
      security: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: pagePath
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK

  /api/v1/og/project/{identifier}:
    get:
      summary: Generate dynamic Open Graph image for a project
      security: []
      parameters:
        - name: identifier
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Image bytes
          content:
            image/png:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary

  /api/v1/projects:
    get:
      summary: Search projects
      security: []
      parameters:
        - name: search
          in: query
          schema:
            type: string
        - name: tags
          in: query
          schema:
            type: string
          description: Comma-separated tags.
        - name: page
          in: query
          schema:
            type: integer
            default: 0
        - name: size
          in: query
          schema:
            type: integer
            default: 10
            maximum: 100
        - name: sort
          in: query
          schema:
            type: string
            enum: [relevance, downloads, updated, new, newest, favorites]
        - name: gameVersion
          in: query
          schema:
            type: string
        - name: classification
          in: query
          schema:
            type: string
            enum: [PLUGIN, DATA, ART, SAVE, MODPACK]
        - name: minDownloads
          in: query
          schema:
            type: integer
        - name: minFavorites
          in: query
          schema:
            type: integer
        - name: category
          in: query
          schema:
            type: string
            enum: [Favorites, Your Projects]
          description: Ignored for API-key auth callers.
        - name: dateRange
          in: query
          schema:
            type: string
            example: 30d
        - name: author
          in: query
          schema:
            type: string
        - name: creator
          in: query
          schema:
            type: string
      responses:
        '200':
          description: OK
    post:
      summary: Create project draft
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/CreateProjectRequest'
      responses:
        '200':
          description: Created
        '400':
          description: Validation error
        '401':
          description: Authentication required
        '403':
          description: Permission or email verification failure

  /api/v1/projects/{id}:
    get:
      summary: Get project details
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
        '403':
          description: Private project access denied
        '404':
          description: Not found
    put:
      summary: Update project metadata
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateProjectRequest'
      responses:
        '200':
          description: Updated
        '400':
          description: Validation error
        '401':
          description: Authentication required
        '403':
          description: Permission denied
    delete:
      summary: Delete project (soft-delete strategy)
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Deleted
        '401':
          description: Authentication required
        '403':
          description: Permission denied

  /api/v1/projects/{id}/meta:
    get:
      summary: Get lightweight project metadata
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
        '404':
          description: Not found

  /api/v1/projects/{id}/analytics:
    get:
      summary: Get project analytics snapshot
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: range
          in: query
          schema:
            type: string
            default: 30d
      responses:
        '200':
          description: OK
        '403':
          description: Draft project analytics access denied

  /api/v1/projects/{id}/submit:
    post:
      summary: Submit project for review
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Submitted
        '400':
          description: Validation error
        '401':
          description: Authentication required
        '403':
          description: Permission denied

  /api/v1/projects/{id}/revert:
    post:
      summary: Revert project to DRAFT
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Reverted
        '401':
          description: Authentication required
        '403':
          description: Permission denied

  /api/v1/projects/{id}/archive:
    post:
      summary: Archive project
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Archived
        '401':
          description: Authentication required
        '403':
          description: Permission denied

  /api/v1/projects/{id}/unlist:
    post:
      summary: Unlist project
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Unlisted
        '401':
          description: Authentication required
        '403':
          description: Permission denied

  /api/v1/projects/{id}/publish:
    post:
      summary: Publish project
      description: Admin-only for first publish; owners can republish archived/unlisted projects.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Published
        '401':
          description: Authentication required
        '403':
          description: Permission denied

  /api/v1/projects/{id}/versions/{version}/dependencies:
    get:
      summary: Get dependency list for a project version
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: version
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
        '401':
          description: Authentication required
        '404':
          description: Project or version not found

  /api/v1/projects/{id}/versions:
    post:
      summary: Create project version
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [versionNumber]
              properties:
                versionNumber:
                  type: string
                  description: "Strict SemVer (e.g., 1.2.3)."
                gameVersions:
                  type: array
                  items:
                    type: string
                file:
                  type: string
                  format: binary
                  description: Required for non-modpack versions.
                modIds:
                  type: array
                  items:
                    type: string
                  description: "Dependency entries as projectId:version or projectId:version:optional."
                changelog:
                  type: string
                channel:
                  type: string
                  enum: [RELEASE, BETA, ALPHA]
      responses:
        '200':
          description: Created
        '400':
          description: Validation error
        '401':
          description: Authentication required
        '403':
          description: Permission denied

  /api/v1/projects/{id}/versions/dependency-suggestions:
    post:
      summary: Inspect plugin manifest and suggest Modtale dependency matches
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          description: Suggestion payload
        '400':
          description: Validation error
        '401':
          description: Authentication required
        '403':
          description: Permission denied

  /api/v1/projects/{id}/versions/{versionId}:
    put:
      summary: Update version metadata/dependencies
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: versionId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateVersionRequest'
      responses:
        '200':
          description: Updated
        '400':
          description: Validation error
        '401':
          description: Authentication required
        '403':
          description: Permission denied
    delete:
      summary: Delete version
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: versionId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Deleted
        '400':
          description: Validation error
        '401':
          description: Authentication required
        '403':
          description: Permission denied

  /api/v1/projects/{id}/versions/{version}/download-url:
    get:
      summary: Create one-time download token URL
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: version
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Tokenized URL
        '404':
          description: Project/version not found

  /api/v1/download/{token}:
    get:
      summary: Download project binary/modpack via one-time token
      security: []
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Binary stream
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '403':
          description: Token invalid/expired

  /api/v1/projects/{id}/versions/{version}/download-bundle-url:
    get:
      summary: Create one-time bundle token URL
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: version
          in: path
          required: true
          schema:
            type: string
        - name: deps
          in: query
          schema:
            type: array
            items:
              type: string
      responses:
        '200':
          description: Tokenized bundle URL
        '404':
          description: Project/version not found

  /api/v1/download-bundle/{token}:
    get:
      summary: Download dependency bundle via one-time token
      security: []
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Zip stream
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '403':
          description: Token invalid/expired

  /api/v1/version/{hash}:
    get:
      summary: Resolve version metadata by SHA-256 hash
      parameters:
        - name: hash
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Found
        '401':
          description: Authentication required
        '404':
          description: Not found

  /api/v1/projects/{id}/icon:
    put:
      summary: Upload project icon (1:1)
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          description: Updated
        '400':
          description: Validation error

  /api/v1/projects/{id}/banner:
    put:
      summary: Upload project banner (3:1)
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          description: Updated
        '400':
          description: Validation error

  /api/v1/projects/{id}/gallery:
    post:
      summary: Add gallery image (16:9)
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          description: Added
        '400':
          description: Validation error
    delete:
      summary: Remove gallery image
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imageUrl]
              properties:
                imageUrl:
                  type: string
      responses:
        '200':
          description: Removed
        '400':
          description: Validation error

  /api/v1/projects/{id}/favorite:
    post:
      summary: Toggle project favorite status
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Toggled

  /api/v1/projects/{id}/comments:
    post:
      summary: Add project comment
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommentRequest'
      responses:
        '200':
          description: Created
        '403':
          description: Comments disabled or permission denied

  /api/v1/projects/{id}/comments/{commentId}:
    put:
      summary: Edit own comment
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: commentId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommentRequest'
      responses:
        '200':
          description: Updated
        '403':
          description: Not comment owner or permission denied

  /api/v1/projects/{id}/comments/{commentId}/vote:
    post:
      summary: Vote on comment
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: commentId
          in: path
          required: true
          schema:
            type: string
        - name: upvote
          in: query
          required: true
          schema:
            type: boolean
      responses:
        '200':
          description: Voted

  /api/v1/users/search:
    get:
      summary: Search users
      parameters:
        - name: query
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK

  /api/v1/users/batch:
    post:
      summary: Fetch user summaries by ID list
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UsersBatchRequest'
      responses:
        '200':
          description: OK

  /api/v1/users/lookup/{username}:
    get:
      summary: Resolve username to user ID
      security: []
      parameters:
        - name: username
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
        '404':
          description: Not found

  /api/v1/user/me:
    get:
      summary: Get current authenticated user profile
      responses:
        '200':
          description: OK
        '401':
          description: Authentication required
    delete:
      summary: Soft-delete current account
      responses:
        '200':
          description: Deleted
        '401':
          description: Authentication required

  /api/v1/user/profile/{userId}:
    get:
      summary: Get public profile by user ID
      security: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
        '404':
          description: Not found

  /api/v1/user/profile:
    put:
      summary: Update own profile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateProfileRequest'
      responses:
        '200':
          description: Updated
        '400':
          description: Validation error

  /api/v1/user/profile/avatar:
    post:
      summary: Upload own avatar
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          description: Avatar URL string
        '400':
          description: Validation error

  /api/v1/user/profile/banner:
    post:
      summary: Upload own banner
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          description: Banner URL string
        '400':
          description: Validation error

  /api/v1/user/settings/notifications:
    put:
      summary: Update own notification preferences
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NotificationPreferences'
      responses:
        '200':
          description: Updated

  /api/v1/user/follow/{targetId}:
    post:
      summary: Follow user
      parameters:
        - name: targetId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Followed
        '404':
          description: Target not found

  /api/v1/user/unfollow/{targetId}:
    post:
      summary: Unfollow user
      parameters:
        - name: targetId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Unfollowed
        '404':
          description: Target not found

  /api/v1/users/{userId}/following:
    get:
      summary: Get users followed by user
      security: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK

  /api/v1/users/{userId}/followers:
    get:
      summary: Get followers of user
      security: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK

  /api/v1/creators/search:
    get:
      summary: Search creators/orgs
      parameters:
        - name: query
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK

  /api/v1/creators/{userId}/projects:
    get:
      summary: List creator projects
      security: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            default: 0
        - name: size
          in: query
          schema:
            type: integer
            default: 10
      responses:
        '200':
          description: OK

  /api/v1/projects/user/contributed:
    get:
      summary: List projects where current user is a contributor
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 0
        - name: size
          in: query
          schema:
            type: integer
            default: 100
      responses:
        '200':
          description: OK
        '401':
          description: Authentication required

  /api/v1/reports:
    post:
      summary: Submit a moderation report
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [targetId, targetType, reason]
              properties:
                targetId:
                  type: string
                targetType:
                  type: string
                reason:
                  type: string
                description:
                  type: string
      responses:
        '200':
          description: Created
        '400':
          description: Validation error
        '401':
          description: Authentication required

  /api/v1/orgs:
    post:
      summary: Create organization
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrganizationRequest'
      responses:
        '200':
          description: Created
        '400':
          description: Validation error

  /api/v1/user/orgs:
    get:
      summary: List organizations current user belongs to
      responses:
        '200':
          description: OK

  /api/v1/users/{userId}/organizations:
    get:
      summary: List organizations for user ID
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK

  /api/v1/orgs/{orgId}/members:
    get:
      summary: List organization members
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
        '403':
          description: Permission denied

  /api/v1/orgs/{orgId}/invites:
    get:
      summary: List pending organization invites
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
        '403':
          description: Permission denied

  /api/v1/orgs/{orgId}/roles:
    post:
      summary: Create organization role
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrganizationRoleRequest'
      responses:
        '200':
          description: Updated
        '400':
          description: Validation error
        '403':
          description: Permission denied

  /api/v1/orgs/{orgId}/roles/{roleId}:
    put:
      summary: Update organization role
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
        - name: roleId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrganizationRoleRequest'
      responses:
        '200':
          description: Updated
        '400':
          description: Validation error
        '403':
          description: Permission denied
    delete:
      summary: Delete organization role
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
        - name: roleId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Deleted
        '400':
          description: Validation error
        '403':
          description: Permission denied

  /api/v1/orgs/{orgId}/members:
    post:
      summary: Invite organization member
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddOrganizationMemberRequest'
      responses:
        '200':
          description: Invited
        '400':
          description: Validation error
        '403':
          description: Permission denied

  /api/v1/orgs/{orgId}/members/{userId}:
    delete:
      summary: Remove member (or self-leave)
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Removed
        '400':
          description: Validation error
        '403':
          description: Permission denied
    put:
      summary: Change member role
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
        - name: userId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateOrganizationMemberRoleRequest'
      responses:
        '200':
          description: Updated
        '400':
          description: Validation error
        '403':
          description: Permission denied

  /api/v1/orgs/{orgId}:
    put:
      summary: Update organization metadata
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateOrganizationRequest'
      responses:
        '200':
          description: Updated
        '400':
          description: Validation error
        '403':
          description: Permission denied
    delete:
      summary: Delete organization
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Deleted
        '403':
          description: Permission denied

  /api/v1/orgs/{orgId}/avatar:
    post:
      summary: Upload organization avatar (1:1)
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          description: URL
        '400':
          description: Validation error
        '403':
          description: Permission denied

  /api/v1/orgs/{orgId}/banner:
    post:
      summary: Upload organization banner (3:1)
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          description: URL
        '400':
          description: Validation error
        '403':
          description: Permission denied

  /api/v1/orgs/{orgId}/invite/accept:
    post:
      summary: Accept organization invite
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Accepted
        '400':
          description: Invalid invite

  /api/v1/orgs/{orgId}/invite/decline:
    post:
      summary: Decline organization invite
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Declined
        '400':
          description: Invalid invite

  /api/v1/orgs/{orgId}/invites/{userId}:
    delete:
      summary: Cancel pending organization invite
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Canceled
        '400':
          description: Validation error

  /api/v1/user/connections/{provider}/toggle-visibility:
    post:
      summary: Toggle visibility of linked personal provider account
      parameters:
        - name: provider
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Updated
        '400':
          description: Invalid provider

  /api/v1/user/connections/{provider}:
    delete:
      summary: Unlink personal provider account
      parameters:
        - name: provider
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Unlinked
        '400':
          description: Validation error

  /api/v1/user/repos/github:
    get:
      summary: List personal GitHub repos
      responses:
        '200':
          description: OK
        '401':
          description: Provider token missing/expired

  /api/v1/user/repos/gitlab:
    get:
      summary: List personal GitLab repos
      responses:
        '200':
          description: OK
        '401':
          description: Provider token missing/expired

  /api/v1/user/repos:
    get:
      summary: Alias for personal GitHub repo list
      responses:
        '200':
          description: OK

  /api/v1/orgs/{orgId}/link/prepare:
    post:
      summary: Prepare session context for linking org provider account
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Prepared
        '403':
          description: Permission denied

  /api/v1/orgs/{orgId}/connections/{provider}/toggle-visibility:
    post:
      summary: Toggle visibility of linked org provider account
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
        - name: provider
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Updated
        '400':
          description: Validation error
        '403':
          description: Permission denied

  /api/v1/orgs/{orgId}/connections/{provider}:
    delete:
      summary: Unlink org provider account
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
        - name: provider
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Unlinked
        '400':
          description: Validation error
        '403':
          description: Permission denied

  /api/v1/orgs/{orgId}/repos/github:
    get:
      summary: List organization GitHub repos
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
        '401':
          description: Token missing/expired
        '403':
          description: Permission denied

  /api/v1/notifications:
    get:
      summary: List current user's notifications
      responses:
        '200':
          description: OK

  /api/v1/notifications/{id}/read:
    post:
      summary: Mark notification as read
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Updated

  /api/v1/notifications/{id}/unread:
    post:
      summary: Mark notification as unread
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Updated

  /api/v1/notifications/read-all:
    post:
      summary: Mark all notifications as read
      responses:
        '200':
          description: Updated

  /api/v1/notifications/{id}:
    delete:
      summary: Delete notification
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Deleted

  /api/v1/notifications/clear-all:
    delete:
      summary: Delete all notifications
      responses:
        '200':
          description: Deleted

  /api/v1/auth/register:
    post:
      summary: Register account
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
      responses:
        '200':
          description: Registered
        '400':
          description: Validation error

  /api/v1/auth/verify:
    post:
      summary: Verify email using token
      security: []
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Verified
        '400':
          description: Invalid/expired token

  /api/v1/auth/resend-verification:
    post:
      summary: Resend verification email for current user
      responses:
        '200':
          description: Sent
        '401':
          description: Authentication required
        '400':
          description: Already verified

  /api/v1/auth/forgot-password:
    post:
      summary: Initiate password reset
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ForgotPasswordRequest'
      responses:
        '200':
          description: Accepted

  /api/v1/auth/reset-password:
    post:
      summary: Complete password reset
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResetPasswordRequest'
      responses:
        '200':
          description: Updated
        '400':
          description: Invalid/expired token or weak password

  /api/v1/auth/credentials:
    put:
      summary: Add/update local email+password credentials for current account
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateCredentialsRequest'
      responses:
        '200':
          description: Updated
        '400':
          description: Validation error

  /api/v1/auth/change-password:
    post:
      summary: Change current account password
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChangePasswordRequest'
      responses:
        '200':
          description: Updated
        '400':
          description: Validation error

  /api/v1/auth/mfa/setup:
    get:
      summary: Generate MFA setup secret + QR payload
      responses:
        '200':
          description: Setup data
        '400':
          description: MFA already enabled

  /api/v1/auth/mfa/verify:
    post:
      summary: Verify MFA setup code and enable MFA
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyMfaRequest'
      responses:
        '200':
          description: Enabled
        '400':
          description: Invalid code

  /api/v1/auth/signin:
    post:
      summary: Sign in with username/email + password
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SignInRequest'
      responses:
        '200':
          description: Session created
        '202':
          description: MFA required
        '401':
          description: Invalid credentials

  /api/v1/auth/mfa/validate-login:
    post:
      summary: Complete MFA login with pre-auth token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MfaLoginRequest'
      responses:
        '200':
          description: Session created
        '400':
          description: Invalid MFA code
        '401':
          description: Invalid/expired pre-auth token
