openapi: 3.0.3
info:
  title: WrkPanel API (partial)
  version: "0.1.0"
  description: |
    Gedeeltelijke OpenAPI-specificatie afgeleid van Laravel-routes en controllers.
    Inclusief account-auth, onboarding/signup-flow, workspaces, account `bootstrap` + `switch-callback`,
    portfolio `GET|POST /api/org*`-basis, en organisatieportaal-login/bootstrap (`/api/organisation-app/*`) waar in code vastligt.
    Overige API: zie INSPECTION.md en routebestanden onder backend/routes/.

servers:
  - url: https://api.wrkpanel.com
    description: Standaard API-host (pas aan per omgeving; prefix altijd /api).

tags:
  - name: account-auth
    description: |
      Stateful SPA-authenticatie (web guard `account_web`, middleware `spa`).
      Vereist typisch Sanctum CSRF-cookie flow (GET /sanctum/csrf-cookie op API-origin)
      en headers X-XSRF-TOKEN / X-Requested-With zoals de frontend al doet.
      Sessiecookie-naam via env SESSION_COOKIE (default zie backend/config/session.php).
  - name: onboarding
    description: |
      Registratie en onboarding-stappen (`routes/account.php`, prefix `/api/onboarding`).
      `POST /signup` is publiek binnen `spa`; overige stappen vereisen `auth:account_web`.
  - name: workspaces
    description: |
      Workspace-lijst, slug-check, extra workspace, switch-token en selectie (`WorkspaceController`).
      Alle routes onder `auth:account_web` + `spa` (zie account.php).
  - name: organization
    description: |
      Owner/organisatie-portfolio onder `/api/org` (`routes/organization.php`), guard `account_web` + portfolio-rollen waar van toepassing.
  - name: organisation-app-auth
    description: |
      Aparte organisatieportaal-sessie (`guard organisation_web`, middleware `spa` + `organisation.app.auth`).
      Niet hetzelfde cookie-contract als account_web — zie `routes/organisation_app.php`.

paths:
  /api/auth/login:
    post:
      tags: [account-auth]
      summary: Login met e-mail en wachtwoord
      operationId: accountAuthLogin
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
      responses:
        "200":
          description: |
            Twee geldige JSON-vormen op status 200:
            (A) Geen 2FA: `{"message":"Login succesvol"}` na finalizeAccountLogin.
            (B) 2FA vereist: `{"two_factor_required":true,"challenge_id":"<uuid>"}` — daarna POST /api/auth/login/two-factor.
            Sessie/cookies worden door Laravel gezet volgens implementatie.
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [message]
                    properties:
                      message:
                        type: string
                        example: Login succesvol
                  - type: object
                    required: [two_factor_required, challenge_id]
                    properties:
                      two_factor_required:
                        type: boolean
                        enum: [true]
                      challenge_id:
                        type: string
                        format: uuid
        "422":
          description: Ongeldige credentials of validatiefout
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JsonMessage"
        "403":
          description: Account disabled of e-mail niet geverifieerd
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  code:
                    type: string
                    description: o.a. email_not_verified wanneer van toepassing

  /api/auth/login/two-factor:
    post:
      tags: [account-auth]
      summary: Voltooien login met 2FA na challenge_id
      operationId: accountAuthLoginTwoFactor
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [challenge_id]
              properties:
                challenge_id:
                  type: string
                  format: uuid
                code:
                  type: string
                  description: 6 cijfers
                  pattern: "^[0-9]{6}$"
                recovery_code:
                  type: string
                  maxLength: 64
      responses:
        "200":
          description: Zelfde succes-response als login zonder 2FA (message Login succesvol)
        "422":
          description: challenge ongeldig/verlopen, geen code+recovery, of onjuiste code
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JsonMessage"

  /api/auth/logout:
    post:
      tags: [account-auth]
      summary: Uitloggen (sessie invalideren)
      operationId: accountAuthLogout
      responses:
        "200":
          description: response()->json(['success' => true])

  /api/auth/me:
    get:
      tags: [account-auth]
      summary: Huidige account + memberships + actieve context
      operationId: accountAuthMe
      responses:
        "200":
          description: Zie AccountAuthController::me — o.a. account, tenant_memberships, portal_memberships, active_context, capabilities, organization_memberships
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          description: Niet ingelogd — response JSON null (Laravel)

  /api/auth/post-login:
    get:
      tags: [account-auth]
      summary: Redirect-hints na login (tenant/portal URL)
      operationId: accountAuthPostLogin
      responses:
        "200":
          description: |
            JSON met o.a. redirect, url, tenant, auto_context, eventueel tenants[].
            Zie AccountAuthController::postLogin.
        "401":
          description: response()->json(null, 401)

  /api/auth/contexts:
    get:
      tags: [account-auth]
      summary: Beschikbare contexten + actieve context
      operationId: accountAuthContexts
      responses:
        "200":
          description: contexts[] + active_context (ContextController::contexts)
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /api/auth/context:
    post:
      tags: [account-auth]
      summary: Actieve context zetten
      operationId: accountAuthContextSet
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type]
              properties:
                type:
                  type: string
                  enum: [workspace, portal, platform, tenant]
                tenant_id:
                  description: optioneel afhankelijk van type
                membership_id:
                  type: string
                  nullable: true
                portal_membership_id:
                  type: string
                  nullable: true
      responses:
        "200":
          description: active_context, permissions; optioneel redirect_url bij workspace
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "403":
          description: CONTEXT_INVALID — message + code uit catch in ContextController::set
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  code:
                    type: string
                    example: CONTEXT_INVALID
    delete:
      tags: [account-auth]
      summary: Actieve context wissen
      operationId: accountAuthContextClear
      responses:
        "200":
          description: active_context null, permissions []

  /api/auth/bootstrap:
    get:
      tags: [account-auth]
      summary: Sessie-bootstrap na login (memberships, next-actie)
      operationId: accountAuthBootstrap
      description: |
        Vereist `auth:account_web` + `spa`. Response vorm: zie `BootstrapController::__invoke`
        (account, active_context, tenant_memberships, portal_memberships, capabilities, organization_memberships, next).
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          description: response()->json(null, 401)

  /api/auth/switch-callback:
    post:
      tags: [account-auth]
      summary: Consumptie eenmalig workspace-wisseltoken
      operationId: accountAuthSwitchCallback
      description: |
        Alleen middleware `spa` (geen `auth:account_web` in route file). Valideert `token`, kan account inloggen.
        Zie `WorkspaceSwitchCallbackController::consume`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token:
                  type: string
                  minLength: 32
                  maxLength: 128
      responses:
        "200":
          description: '{"success":true,"tenant":{"id":...,"slug":"..."}}'
          content:
            application/json:
              schema:
                type: object
                required: [success, tenant]
                properties:
                  success:
                    type: boolean
                  tenant:
                    type: object
                    required: [id, slug]
                    properties:
                      id:
                        type: integer
                      slug:
                        type: string
        "422":
          description: |
            JSON met `message` + `code`: `SWITCH_TOKEN_INVALID` (ongeldig/verlopen/ontbrekende account of tenant),
            `SWITCH_TOKEN_CONSUMED` (race: token al verbruikt), of validatiefout op `token`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JsonMessageWithCode"
        "403":
          description: |
            Geen membership op tenant — `code` `SWITCH_TOKEN_FORBIDDEN` (zie WorkspaceSwitchCallbackController).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JsonMessageWithCode"

  /api/onboarding/signup:
    post:
      tags: [onboarding]
      summary: Stap 1 — account + organisatie + eerste workspace (e-mailverificatie)
      operationId: onboardingSignup
      description: |
        Geen `auth:account_web` vereist. Na succes geen automatische login; response 201 met status verification_required.
        Extra business rules: abort 422 (trial domain), abort 429 (IP-limiet), abort 422 (gereserveerde slug), 500 bij ontbrekende owner-role.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email, password, password_confirmation, organization_name, workspace_name, tenant_slug]
              properties:
                name:
                  type: string
                  maxLength: 255
                email:
                  type: string
                  format: email
                  maxLength: 255
                password:
                  type: string
                  minLength: 8
                password_confirmation:
                  type: string
                organization_name:
                  type: string
                  maxLength: 255
                workspace_name:
                  type: string
                  maxLength: 255
                tenant_slug:
                  type: string
                  maxLength: 40
                  pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
      responses:
        "201":
          description: Body zoals `OnboardingController::signup` return response()->json(..., 201)
          content:
            application/json:
              schema:
                type: object
                required: [status, message, email, verification_mail_sent, organization, tenant]
                properties:
                  status:
                    type: string
                    example: verification_required
                  message:
                    type: string
                  email:
                    type: string
                    format: email
                  verification_mail_sent:
                    type: boolean
                  organization:
                    type: object
                    properties:
                      id:
                        type: integer
                  tenant:
                    type: object
                    properties:
                      id:
                        type: integer
                      slug:
                        type: string
        "422":
          description: Validatiefout (Laravel), unieke e-mail/slug, gereserveerde slug, of abort trial-domain message
        "429":
          description: Te veel signups vanaf IP (abort in controller)
        "500":
          description: Owner role not found (abort)

  /api/onboarding/status:
    get:
      tags: [onboarding]
      summary: Onboardingstatus eerste actieve tenant
      operationId: onboardingStatus
      description: Vereist `auth:account_web` + `spa`.
      responses:
        "200":
          description: tenant_id + state uit tenant.meta.onboarding.state, of `{ "state": "no_tenant" }`
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          description: '{"error":"Not authenticated"}' — redundant als middleware al 401 forceert

  /api/onboarding/plan:
    post:
      tags: [onboarding]
      summary: Stap 2 — plankeuze (trial of checkout)
      operationId: onboardingPlan
      description: Vereist `auth:account_web`. Gebruikt eerste actieve tenant membership.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [plan_key, mode]
              properties:
                plan_key:
                  type: string
                mode:
                  type: string
                  enum: [trial, pay]
      responses:
        "200":
          description: |
            trial: `{ "mode":"trial", "trial_ends_at": "<date>" }` of
            checkout: `{ "mode":"checkout", "checkout_url": "...", "payment_id": ... }` (velden uit BillingEngine).
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "404":
          description: BillingPlan `firstOrFail` of membership `firstOrFail` — Laravel model not found
        "422":
          description: Validatiefout op `plan_key` / `mode` (Laravel)

  /api/onboarding/complete:
    post:
      tags: [onboarding]
      summary: Stap 3 — onboarding afronden
      operationId: onboardingComplete
      responses:
        "200":
          description: '{"success":true}'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        "404":
          description: Geen membership / tenant (firstOrFail in controller)

  /api/workspaces:
    get:
      tags: [workspaces]
      summary: Lijst workspaces voor ingelogd account
      operationId: workspacesIndex
      responses:
        "200":
          description: active_tenant_id + tenants[] (formatTenant per tenant)
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
    post:
      tags: [workspaces]
      summary: Extra workspace aanmaken (self-service)
      operationId: workspacesStore
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [company_name, slug, terms_accepted, billing_consent_accepted]
              properties:
                company_name:
                  type: string
                  maxLength: 191
                slug:
                  type: string
                  maxLength: 40
                  pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
                plan_key:
                  type: string
                  nullable: true
                  enum: [starter, pro]
                terms_accepted:
                  description: Vereist; Laravel-regel `accepted` (zie Laravel-documentatie voor toegestane waarden).
                  type: string
                billing_consent_accepted:
                  description: Vereist; Laravel-regel `accepted`.
                  type: string
      responses:
        "200":
          description: success, tenant (formatTenant), redirect_url + Set-Cookie wrkpanel_active_tenant
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "403":
          description: Geen rechten om workspace te creëren of geen toegang bron-tenant
        "422":
          description: Validatie, gereserveerde slug, geen actieve workspace-context

  /api/workspaces/slug-available:
    get:
      tags: [workspaces]
      summary: Controleren of workspace-slug beschikbaar is
      operationId: workspacesSlugAvailable
      parameters:
        - name: slug
          in: query
          required: false
          schema:
            type: string
          description: |
            Ontbreekt of leeg → altijd **200** met `available: false`, `reason: invalid` (geen 422 in controller).
      responses:
        "200":
          description: |
            Altijd 200 — `{ available, normalized, reason? }`; `reason` o.a. `invalid`, `format`, `reserved` (WorkspaceController::slugAvailable).

  /api/workspaces/switch-token:
    post:
      tags: [workspaces]
      summary: Eenmalige switch-URL voor andere workspace
      operationId: workspacesSwitchToken
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tenant_id]
              properties:
                tenant_id:
                  type: integer
      responses:
        "200":
          description: '{ "redirect_url": "<https://{host}/auth/callback?switch_token=...>", "expires_in": <seconds> }' (WorkspaceSwitchLaunchService)
          content:
            application/json:
              schema:
                type: object
                required: [redirect_url, expires_in]
                properties:
                  redirect_url:
                    type: string
                  expires_in:
                    type: integer
        "403":
          description: Geen actieve membership op gekozen tenant — JSON `message` + `code` `SWITCH_TOKEN_FORBIDDEN`
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JsonMessageWithCode"
        "404":
          description: Workspace not found
        "422":
          description: Validatiefout op `tenant_id`
        "429":
          description: Rate limit — JSON `message` + `code` `SWITCH_TOKEN_RATE_LIMIT`
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JsonMessageWithCode"

  /api/workspaces/select:
    post:
      tags: [workspaces]
      summary: Actieve tenant/workspace kiezen (cookie + session)
      operationId: workspacesSelect
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tenant_id]
              properties:
                tenant_id:
                  type: integer
      responses:
        "200":
          description: success, tenant_id, redirect_url, redirect (zelfde URL) + Set-Cookie `wrkpanel_active_tenant`
          content:
            application/json:
              schema:
                type: object
                required: [success, tenant_id, redirect_url, redirect]
                properties:
                  success:
                    type: boolean
                  tenant_id:
                    type: integer
                  redirect_url:
                    type: string
                  redirect:
                    type: string
        "403":
          description: JSON `message` "No access to this workspace"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JsonMessage"
        "404":
          description: JSON `message` "Workspace not found"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JsonMessage"
        "422":
          description: Validatiefout op `tenant_id`

  /api/org:
    post:
      tags: [organization]
      summary: Eerste organisatie voor account (caller wordt owner)
      operationId: orgBootstrapStore
      description: Vereist `auth:account_web` + `spa`. Geen `{organization}` in pad.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  maxLength: 255
                display_name:
                  type: string
                  nullable: true
                  maxLength: 255
                description:
                  type: string
                  nullable: true
                  maxLength: 10000
                slug:
                  type: string
                  nullable: true
                  maxLength: 191
      responses:
        "201":
          description: Envelope `data.organization` + `data.membership` (OrganizationBootstrapController::store)
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "422":
          description: Validatiefout (Laravel)

  /api/org/memberships:
    get:
      tags: [organization]
      summary: Organisaties waartoe het account toegang heeft
      operationId: orgMembershipsIndex
      responses:
        "200":
          description: '{ "data": [ { "role", "organization": { id, name, slug, display_name } } ] }'
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          description: response JSON null

  /api/org/{organization}:
    get:
      tags: [organization]
      summary: Organisatie-overzicht (dashboard payload)
      operationId: orgDashboardShow
      parameters:
        - name: organization
          in: path
          required: true
          schema:
            type: string
          description: Organization route key (Laravel implicit binding — typisch numeriek id)
      description: |
        Vereist `organization.portfolio:viewer` middleware. Response `data` komt uit `OrganizationManagementService::overviewPayload`;
        exacte velden niet in deze spec uitgewerkt — zie service/controller.
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "403":
          description: Portfolio-RBAC weigert toegang (middleware `organization.portfolio:viewer`)
        "404":
          description: Onbekende `organization` route-parameter (Laravel model binding)

  /api/organisation-app/login:
    post:
      tags: [organisation-app-auth]
      summary: Login organisatieportaal (aparte sessie)
      operationId: organisationAppLogin
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
      responses:
        "200":
          description: |
            Twee vormen op 200: met 2FA `{ ok, two_factor_required: true, requires_2fa_setup: false }`
            of zonder 2FA `{ ok, two_factor_required: false, requires_2fa_setup: true }` (2FA nog instellen in app).
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          description: Ongeldige credentials — message vertaald
        "403":
          description: Account disabled, e-mail niet geverifieerd, of geen organizationMembers

  /api/organisation-app/logout:
    post:
      tags: [organisation-app-auth]
      summary: Uitloggen organisatieportaal
      operationId: organisationAppLogout
      description: Alleen middleware `spa` in route file — geen `organisation.app.auth` op deze route.
      responses:
        "200":
          description: '{"ok":true}'

  /api/organisation-app/me:
    get:
      tags: [organisation-app-auth]
      summary: Huidige organisatieportaal-sessie + organisaties
      operationId: organisationAppMe
      description: Middleware `organisation.app.auth` (ingelogd op organisation_web guard).
      responses:
        "200":
          description: authenticated, account, organizations, two_factor_* velden (OrganisationAppAuthController::me)
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          description: '{"authenticated":false}'
        "403":
          description: Geen organization access — logout + message

  /api/organisation-app/bootstrap:
    get:
      tags: [organisation-app-auth]
      summary: Minimale bootstrap payload (account + organisaties)
      operationId: organisationAppBootstrap
      description: |
        Route-chain in `routes/organisation_app.php`: `spa` → `organisation.app.auth` → `organisation.app.twofactor`
        (`EnsureOrganisationAppTwoFactor`) → inline closure.
        Succes: `{ ok: true, account: { id, name, email }, organizations: ... }` waarbij `organizations` uit
        `Account::organizationMembershipsPayload()` komt — nested keys **niet** in deze spec uitgewerkt.
        Bij geen ingelogde user in closure: `{ "message": "Unauthenticated" }` met status **401**.
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                required: [ok, account, organizations]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  account:
                    type: object
                    required: [id, name, email]
                    properties:
                      id:
                        type: integer
                      name:
                        type: string
                      email:
                        type: string
                        format: email
                  organizations:
                    description: Zie `organizationMembershipsPayload()` op Account — vorm niet verder gespecificeerd.
                    type: array
                    items:
                      type: object
                      additionalProperties: true
        "401":
          description: |
            `EnsureOrganisationAppTwoFactor` zonder organisation_web-user: `{ "message": "Unauthenticated" }`;
            of closure zonder user (zelfde body).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JsonMessage"
        "403":
          description: |
            Van `EnsureOrganisationAppTwoFactor`: geen org-toegang; 2FA nog niet ingesteld (`requires_2fa_setup: true`);
            of 2FA-challenge nog niet afgerond (`requires_2fa_challenge: true`). Exacte JSON-keys staan in die middleware.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

components:
  schemas:
    JsonMessage:
      type: object
      properties:
        message:
          type: string
    JsonMessageWithCode:
      type: object
      properties:
        message:
          type: string
        code:
          type: string
