openapi: 3.0.3
info:
  title: Parental Control API
  description: |
    REST API for the **api-parental** service: parents, children, pairing, WireGuard-oriented client config,
    blocked-service catalog, per-child blocks, and optional **blocked_services_schedule** (AdGuard Home–compatible pause windows).

    When `DEMO_API_KEY` is set on the server, protected routes require header `X-Api-Key` with that value.
    Parent-scoped routes also require `X-Parent-Id` (UUID) matching the parent you act as.
  version: 1.0.0
  license:
    name: Proprietary

servers:
  - url: http://127.0.0.1:8080
    description: Local api-parental default
  - url: https://api.example.com
    description: Replace with your deployed API base URL (no trailing slash)

tags:
  - name: Health
  - name: Pairing
  - name: Parents
  - name: Children
  - name: Blocked services

components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-Api-Key
      description: Required when the server is configured with DEMO_API_KEY.
    ParentId:
      type: apiKey
      in: header
      name: X-Parent-Id
      description: UUID of the parent account (must match the parent_id in query/body where applicable).
    BearerChild:
      type: http
      scheme: bearer
      description: Child device access token from pairing registration response.

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
        message:
          type: string

    Parent:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

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

    Child:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        public_key:
          type: string
        assigned_ip:
          type: string
        parent_id:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ChildCreate:
      type: object
      required: [name, public_key]
      properties:
        name:
          type: string
        public_key:
          type: string
        parent_id:
          type: string
          format: uuid
          description: Optional; defaults to legacy built-in parent when omitted.

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

    ChildRegisterRequest:
      type: object
      required: [name, public_key]
      properties:
        name:
          type: string
        public_key:
          type: string

    ChildRegisterResponse:
      type: object
      properties:
        child_id:
          type: string
          format: uuid
        pairing_code:
          type: string
        access_token:
          type: string
        expires_at:
          type: string
          format: date-time
        assigned_ip:
          type: string
        name:
          type: string
        public_key:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ClaimRequest:
      type: object
      required: [parent_id, pairing_code]
      properties:
        parent_id:
          type: string
          format: uuid
        child_name:
          type: string
          description: Optional; when set, used with pairing_code for stricter matching.
        pairing_code:
          type: string

    RegeneratePairingRequest:
      type: object
      required: [child_id]
      properties:
        child_id:
          type: string
          format: uuid

    RegeneratePairingResponse:
      type: object
      properties:
        pairing_code:
          type: string
        expires_at:
          type: string
          format: date-time

    ChildConfig:
      type: object
      properties:
        child_id:
          type: string
        server_public_key:
          type: string
        endpoint:
          type: string
        client_address:
          type: string
        allowed_ips:
          type: array
          items:
            type: string
        dns:
          type: array
          items:
            type: string
        persistent_keepalive:
          type: integer

    CatalogResponse:
      type: object
      properties:
        services:
          type: array
          items:
            $ref: "#/components/schemas/CatalogService"
        groups:
          type: array
          items:
            $ref: "#/components/schemas/CatalogGroup"

    CatalogService:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        group_id:
          type: string

    CatalogGroup:
      type: object
      properties:
        id:
          type: string
        name:
          type: string

    DayRange:
      type: object
      required: [start, end]
      properties:
        start:
          type: number
          description: Milliseconds from midnight (0–86400000), start inclusive.
        end:
          type: number
          description: Milliseconds from midnight, end exclusive. Must satisfy start < end <= 86400000.

    Schedule:
      type: object
      required: [time_zone]
      properties:
        time_zone:
          type: string
          description: IANA zone id or "Local".
        sun:
          $ref: "#/components/schemas/DayRange"
        mon:
          $ref: "#/components/schemas/DayRange"
        tue:
          $ref: "#/components/schemas/DayRange"
        wed:
          $ref: "#/components/schemas/DayRange"
        thu:
          $ref: "#/components/schemas/DayRange"
        fri:
          $ref: "#/components/schemas/DayRange"
        sat:
          $ref: "#/components/schemas/DayRange"
      description: |
        Periods when blocked services are **not** enforced (pause windows), aligned with AdGuard Home client schedules.

    ChildBlockedResponse:
      type: object
      properties:
        blocked_service_ids:
          type: array
          items:
            type: string
        blocked_services_schedule:
          $ref: "#/components/schemas/Schedule"

    PatchChildBlockedRequest:
      type: object
      required: [blocked_service_ids]
      properties:
        blocked_service_ids:
          type: array
          items:
            type: string
        blocked_services_schedule:
          description: |
            Omit to leave schedule unchanged. Send JSON `null` to clear. Otherwise a full schedule object.
          nullable: true
          allOf:
            - $ref: "#/components/schemas/Schedule"

paths:
  /api/v1/health:
    get:
      tags: [Health]
      summary: Liveness check
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /api/v1/pairing/child-register:
    post:
      tags: [Pairing]
      summary: Register a pending child and receive pairing code + access token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChildRegisterRequest"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChildRegisterResponse"

  /api/v1/pairing/regenerate-code:
    post:
      tags: [Pairing]
      summary: Regenerate pairing code (child Bearer token)
      security:
        - BearerChild: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegeneratePairingRequest"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RegeneratePairingResponse"

  /api/v1/pairing/claim:
    post:
      tags: [Pairing]
      summary: Parent claims a child with pairing code
      security:
        - ApiKey: []
        - ParentId: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ClaimRequest"
      responses:
        "200":
          description: Linked child
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Child"

  /api/v1/parents:
    post:
      tags: [Parents]
      summary: Create parent
      security:
        - ApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ParentCreate"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Parent"
    get:
      tags: [Parents]
      summary: List parents
      security:
        - ApiKey: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Parent"

  /api/v1/parents/{id}:
    delete:
      tags: [Parents]
      summary: Delete parent (self only)
      security:
        - ApiKey: []
        - ParentId: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Deleted
        "404":
          description: Not found

  /api/v1/children:
    get:
      tags: [Children]
      summary: List children for a parent
      security:
        - ApiKey: []
        - ParentId: []
      parameters:
        - name: parent_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Child"
    post:
      tags: [Children]
      summary: Create child (admin-style allocation)
      security:
        - ApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChildCreate"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Child"

  /api/v1/children/{id}:
    get:
      tags: [Children]
      summary: Get child
      security:
        - ApiKey: []
        - ParentId: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Child"
    put:
      tags: [Children]
      summary: Update child
      security:
        - ApiKey: []
        - ParentId: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChildUpdate"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Child"
    delete:
      tags: [Children]
      summary: Delete child
      security:
        - ApiKey: []
        - ParentId: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Deleted

  /api/v1/children/{id}/config:
    get:
      tags: [Children]
      summary: WireGuard-oriented client parameters
      description: |
        Use **either** Bearer child access token **or** (when the server enforces it) **X-Api-Key** together with **X-Parent-Id** for the child's parent.
      security:
        - BearerChild: []
        - ApiKey: []
          ParentId: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChildConfig"

  /api/v1/blocked-services/catalog:
    get:
      tags: [Blocked services]
      summary: List blockable services (from AdGuard when configured)
      security:
        - ApiKey: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CatalogResponse"

  /api/v1/children/{id}/blocked-services:
    get:
      tags: [Blocked services]
      summary: Get blocked service IDs and optional schedule for a child
      security:
        - ApiKey: []
        - ParentId: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChildBlockedResponse"
    patch:
      tags: [Blocked services]
      summary: Replace blocked services and optionally set or clear schedule
      security:
        - ApiKey: []
        - ParentId: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PatchChildBlockedRequest"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChildBlockedResponse"
