openapi: 3.1.0
info:
  title: SaaSFort API
  version: "1.0"
  description: |
    SaaSFort REST API — automated web security auditing for B2B SaaS.

    **Base URL:** `https://api.saasfort.com`

    ## Authentication

    ### REST API v1 (programmatic integration)
    API key via `X-API-Key` header or `Authorization: Bearer <key>`.

    ### Internal endpoints (scan, report, auth)
    JWT Bearer token from `/api/login` or `/api/register`.

    ## Rate Limits
    All endpoints are rate-limited by IP. Limits are noted per endpoint.

  contact:
    name: SaaSFort Support
    email: hello@saasfort.com
    url: https://saasfort.com/contact
  license:
    name: Proprietary

servers:
  - url: https://api.saasfort.com
    description: Production

tags:
  - name: v1
    description: REST API v1 — programmatic integration (API key auth)
  - name: scan
    description: Security scan endpoints
  - name: auth
    description: Authentication
  - name: report
    description: Deal reports
  - name: checkout
    description: Stripe checkout

paths:
  /api/v1/scan:
    post:
      summary: Run a security scan
      description: |
        Trigger a full security scan against a domain. Returns 38+ security findings
        across 16 categories including OWASP, TLS, CSP, DNS, HTTP headers.

        Results include NIS2 Article 21 and ISO 27001:2022 Annex A compliance mappings.

        A Slack notification is sent if `SLACK_WEBHOOK_URL` is configured.

        **Rate limit:** 30 requests/minute per IP.
      tags: [v1]
      security:
        - apiKey: []
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain:
                  type: string
                  description: Domain to scan (with or without https://)
                  example: example.com
      responses:
        "200":
          description: Scan completed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1ScanResponse'
        "400":
          $ref: '#/components/responses/BadRequest'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "500":
          $ref: '#/components/responses/InternalError'

  /api/scan:
    post:
      summary: Run a free scan (unauthenticated)
      description: |
        Public endpoint — prospects can scan without an account. 1 free scan per IP.
        Authenticated users get full history storage.

        **Rate limit:** 10 requests/minute per IP.
      tags: [scan]
      security:
        - {}
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain:
                  type: string
                  example: example.com
                email:
                  type: string
                  description: Optional — to receive results by email
                  example: cto@example.com
      responses:
        "200":
          description: Scan result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanResult'
        "400":
          $ref: '#/components/responses/BadRequest'

  /api/report:
    get:
      summary: Generate Deal Report (HTML)
      description: |
        Returns a print-ready branded HTML security report for a previously scanned domain.
        Includes OWASP findings, NIS2 compliance mapping, ISO 27001 controls, and remediation guidance.

        **Rate limit:** 15 requests/minute per IP.
      tags: [report]
      parameters:
        - name: domain
          in: query
          required: true
          schema:
            type: string
          example: example.com
      responses:
        "200":
          description: HTML report
          content:
            text/html:
              schema:
                type: string
        "400":
          $ref: '#/components/responses/BadRequest'

  /api/report/pdf:
    get:
      summary: Generate Deal Report (PDF)
      description: |
        Returns a PDF binary of the Deal Report. Requires Chromium on the server.

        **Rate limit:** 5 requests/minute per IP.
      tags: [report]
      parameters:
        - name: domain
          in: query
          required: true
          schema:
            type: string
          example: example.com
      responses:
        "200":
          description: PDF report
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "400":
          $ref: '#/components/responses/BadRequest'
        "500":
          $ref: '#/components/responses/InternalError'

  /api/register:
    post:
      summary: Register a new account
      description: "**Rate limit:** 5 requests/minute per IP."
      tags: [auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuthRequest'
      responses:
        "201":
          description: Account created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        "400":
          $ref: '#/components/responses/BadRequest'
        "409":
          description: Email already registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/login:
    post:
      summary: Login
      description: "**Rate limit:** 10 requests/minute per IP."
      tags: [auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuthRequest'
      responses:
        "200":
          description: Login successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        "401":
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/me:
    get:
      summary: Get current user
      tags: [auth]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Current user profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        "401":
          $ref: '#/components/responses/Unauthorized'

  /api/scan/history:
    get:
      summary: Get scan history
      description: Returns previous scans for the authenticated user.
      tags: [scan]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: List of past scans
          content:
            application/json:
              schema:
                type: object
                properties:
                  scans:
                    type: array
                    items:
                      $ref: '#/components/schemas/ScanResult'
        "401":
          $ref: '#/components/responses/Unauthorized'

  /api/password-reset:
    post:
      summary: Request password reset
      description: |
        Sends a reset link to the email if an account exists.
        Always returns 200 to prevent email enumeration.

        **Rate limit:** 3 requests/minute per IP.
      tags: [auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
      responses:
        "200":
          description: Reset email sent (if account exists)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  message:
                    type: string

  /api/password-reset/confirm:
    post:
      summary: Confirm password reset
      description: "**Rate limit:** 5 requests/minute per IP."
      tags: [auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, new_password]
              properties:
                token:
                  type: string
                  description: Token from reset email
                new_password:
                  type: string
                  minLength: 8
      responses:
        "200":
          description: Password updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  message:
                    type: string
        "400":
          description: Invalid or expired token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/checkout:
    post:
      summary: Create Stripe checkout session
      description: |
        Returns a Stripe checkout URL for the selected plan tier.
        Includes a 14-day free trial by default (configurable via STRIPE_TRIAL_DAYS env).

        Valid tier IDs: `TIER_STARTER`, `TIER_GROWTH`, `TIER_SCALE`,
        `TIER_STARTER_ANNUAL`, `TIER_GROWTH_ANNUAL`, `TIER_SCALE_ANNUAL`.

        **Rate limit:** 10 requests/minute per IP.
      tags: [checkout]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tierId:
                  type: string
                  example: TIER_GROWTH
                  description: Plan tier identifier
                tier:
                  type: string
                  example: growth
                  description: Shorthand alias (starter/growth/scale)
      responses:
        "200":
          description: Stripe checkout URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
        "400":
          $ref: '#/components/responses/BadRequest'
        "503":
          description: Payment not configured

  /api/portal:
    post:
      summary: Create Stripe Customer Portal session
      description: Returns a URL to the Stripe billing portal where the user can upgrade, downgrade, or cancel their subscription.
      tags: [billing]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Portal session URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
        "401":
          $ref: '#/components/responses/Unauthorized'
        "400":
          $ref: '#/components/responses/BadRequest'

  /api/change-plan:
    post:
      summary: Switch subscription to a different plan
      description: Directly switches the user's active subscription to a new plan with prorated billing.
      tags: [billing]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tierId]
              properties:
                tierId:
                  type: string
                  description: Target tier (e.g. TIER_GROWTH, TIER_SCALE_ANNUAL)
                  example: TIER_GROWTH
      responses:
        "200":
          description: Plan changed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  plan:
                    type: string
                  subscription:
                    type: object
                    properties:
                      id:
                        type: string
                      status:
                        type: string
        "401":
          $ref: '#/components/responses/Unauthorized'
        "400":
          $ref: '#/components/responses/BadRequest'

  /api/domains:
    get:
      summary: List monitored domains
      description: Returns unique domains the user has scanned, with latest score per domain.
      tags: [dashboard]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Domain list with latest scores
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  domains:
                    type: array
                    items:
                      type: object
                      properties:
                        domain:
                          type: string
                        score:
                          type: integer
                        grade:
                          type: string
                        last_scan:
                          type: string
                          format: date-time
                        scan_count:
                          type: integer
        "401":
          $ref: '#/components/responses/Unauthorized'

  /api/incidents:
    get:
      summary: Security incidents
      description: Returns critical and high severity findings from latest scans.
      tags: [dashboard]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Incident list
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  incidents:
                    type: array
                    items:
                      type: object
                      properties:
                        domain:
                          type: string
                        category:
                          type: string
                        check:
                          type: string
                        severity:
                          type: string
                        description:
                          type: string
                        scanned_at:
                          type: string
                          format: date-time
                  total:
                    type: integer
        "401":
          $ref: '#/components/responses/Unauthorized'

  /api/nis2:
    get:
      summary: NIS2 compliance overview
      description: Returns NIS2 Art.21(2) control coverage from the user's latest scans.
      tags: [dashboard]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: NIS2 compliance summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  controls:
                    type: array
                    items:
                      type: object
                      properties:
                        control:
                          type: string
                        description:
                          type: string
                        checks_performed:
                          type: integer
                        checks_passed:
                          type: integer
                        checks_failed:
                          type: integer
                  compliance_pct:
                    type: integer
                  domains_assessed:
                    type: integer
                  deadline:
                    type: string
                  framework:
                    type: string
        "401":
          $ref: '#/components/responses/Unauthorized'

  /api/scan/detail/{id}:
    get:
      summary: Get scan detail
      description: Returns the full scan result for a specific scan ID from history.
      tags: [scan]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Scan ID from scan history (e.g. "20260320-143000")
      responses:
        "200":
          description: Full scan result
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  result:
                    $ref: '#/components/schemas/ScanResult'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          description: Scan not found

  /api/health:
    get:
      summary: Health check
      tags: [v1]
      responses:
        "200":
          description: API is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key for REST API v1 programmatic access
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    AuthRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
          example: cto@example.com
        password:
          type: string
          minLength: 8
          example: securepassword

    AuthResponse:
      type: object
      properties:
        token:
          type: string
          description: JWT token (24h validity)
        user:
          $ref: '#/components/schemas/User'

    User:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
        name:
          type: string
        plan:
          type: string
          description: Active plan (starter/growth/scale or empty for free)
        paid_until:
          type: string
          format: date-time

    Finding:
      type: object
      properties:
        category:
          type: string
          description: Check category (e.g. TLS, CSP, DNS, Headers)
          example: TLS
        check:
          type: string
          description: Specific check name
          example: Certificate validity
        severity:
          type: string
          enum: [pass, info, low, medium, high, critical]
        description:
          type: string
          description: Detailed analysis of what was found
          example: "TLS certificate issued by Let's Encrypt, valid until 2027-01-15, SHA-256, supports TLS 1.2 and 1.3"
        value:
          type: string
          description: Observed value (header value, cert subject, etc.)
        evidence:
          type: string
          description: Technical evidence — raw response data, header values, cert chain details
        duration_ms:
          type: integer
          description: Time taken for this check in milliseconds
        compliance:
          type: array
          items:
            type: string
          description: NIS2 Article 21 references (e.g. NIS2 Art.21(2)(h))
          example: ["NIS2 Art.21(2)(h)", "NIS2 Art.21(2)(e)"]
        iso27001:
          type: array
          items:
            type: string
          description: ISO 27001:2022 Annex A control references
          example: ["A.8.24"]

    ScanResult:
      type: object
      properties:
        domain:
          type: string
          example: example.com
        scanned_at:
          type: string
          format: date-time
        score:
          type: integer
          minimum: 0
          maximum: 100
          description: Overall security score (0-100)
          example: 80
        grade:
          type: string
          description: Letter grade (A+ to F)
          example: B
        findings:
          type: array
          items:
            $ref: '#/components/schemas/Finding'
        duration_ms:
          type: integer
          description: Total scan duration in milliseconds

    V1ScanResponse:
      type: object
      properties:
        ok:
          type: boolean
          example: true
        version:
          type: string
          example: v1
        result:
          $ref: '#/components/schemas/ScanResult'

    Error:
      type: object
      properties:
        error:
          type: string
          example: invalid domain

  responses:
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Unauthorized — missing or invalid credentials
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
