openapi: 3.1.0
info:
  # Do not change the title, if the title changes, the import paths will be broken
  title: Api
  version: 0.1.0
  description: Ders İlan Sitesi API specification
servers:
  - url: /api
    description: Base API path
tags:
  - name: health
    description: Health operations
  - name: auth
    description: Authentication and registration
  - name: instructors
    description: Instructor directory
  - name: geo
    description: Cities, regions and departments
  - name: contact
    description: Contact form
  - name: admin
    description: Admin moderation
  - name: Storage
    description: Object storage upload and serving endpoints.
paths:
  /healthz:
    get:
      operationId: healthCheck
      tags: [health]
      summary: Health check
      description: Returns server health status
      responses:
        "200":
          description: Healthy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthStatus"

  /auth/register:
    post:
      operationId: register
      tags: [auth]
      summary: Register a new student or instructor account
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegisterInput"
      responses:
        "201":
          description: Account created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResult"
        "400":
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Email already registered
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /auth/login:
    post:
      operationId: login
      tags: [auth]
      summary: Log in with email and password
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginInput"
      responses:
        "200":
          description: Logged in
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResult"
        "401":
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /auth/me:
    get:
      operationId: getMe
      tags: [auth]
      summary: Get the currently authenticated user
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Current user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /instructors:
    get:
      operationId: listInstructors
      tags: [instructors]
      summary: List approved instructors, optionally filtered
      parameters:
        - name: city
          in: query
          schema:
            type: string
        - name: department
          in: query
          schema:
            type: string
        - name: vip
          in: query
          schema:
            type: boolean
        - name: search
          in: query
          schema:
            type: string
      responses:
        "200":
          description: List of approved instructors
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/InstructorSummary"

  /instructors/vip:
    get:
      operationId: listVipInstructors
      tags: [instructors]
      summary: List featured VIP instructors for the homepage
      responses:
        "200":
          description: List of VIP instructors
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/InstructorSummary"

  /instructors/{id}:
    get:
      operationId: getInstructor
      tags: [instructors]
      summary: Get an approved instructor's public profile
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Instructor detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InstructorSummary"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /cities:
    get:
      operationId: listCities
      tags: [geo]
      summary: List all 81 provinces with approved instructor counts
      responses:
        "200":
          description: City list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/CityCount"

  /regions:
    get:
      operationId: listRegions
      tags: [geo]
      summary: List Turkey's 7 geographical regions with approved instructor counts
      responses:
        "200":
          description: Region list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/RegionCount"

  /departments:
    get:
      operationId: listDepartments
      tags: [geo]
      summary: List distinct instructor departments/subjects with counts
      responses:
        "200":
          description: Department list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/DepartmentCount"

  /stats:
    get:
      operationId: getStats
      tags: [geo]
      summary: Get homepage summary stats
      responses:
        "200":
          description: Stats
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Stats"

  /contact:
    post:
      operationId: sendContactMessage
      tags: [contact]
      summary: Submit a contact form message
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContactInput"
      responses:
        "201":
          description: Message received
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContactMessage"
        "400":
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /admin/pending-instructors:
    get:
      operationId: listPendingInstructors
      tags: [admin]
      summary: List instructors awaiting approval
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Pending instructors
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/User"
        "403":
          description: Not authorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /admin/instructors/{id}/approve:
    post:
      operationId: approveInstructor
      tags: [admin]
      summary: Approve a pending instructor
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Updated user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "403":
          description: Not authorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /admin/instructors/{id}/reject:
    post:
      operationId: rejectInstructor
      tags: [admin]
      summary: Reject a pending instructor
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Updated user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "403":
          description: Not authorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /admin/instructors/{id}/toggle-vip:
    post:
      operationId: toggleInstructorVip
      tags: [admin]
      summary: Toggle an instructor's VIP featured status
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Updated user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "403":
          description: Not authorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /storage/uploads/request-url:
    post:
      tags: [Storage]
      operationId: requestUploadUrl
      summary: Request a presigned URL for file upload
      description: |
        Returns a presigned GCS URL for direct upload. The client sends JSON
        metadata here, then uploads the file directly to the returned URL.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UploadUrlRequest'
      responses:
        '200':
          description: Presigned upload URL generated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadUrlResponse'
        '400':
          description: Missing or invalid required fields.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: Failed to generate upload URL.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /storage/public-objects/{filePath}:
    get:
      tags: [Storage]
      operationId: getPublicObject
      summary: Serve a public asset from PUBLIC_OBJECT_SEARCH_PATHS
      description: |
        Unconditionally public — no authentication or ACL checks.
        Searches PUBLIC_OBJECT_SEARCH_PATHS for the given file path.
      parameters:
        - name: filePath
          in: path
          required: true
          schema:
            type: string
          description: Relative file path within the public search paths.
      responses:
        '200':
          description: Object content streamed with correct Content-Type.
          content:
            '*/*':
              schema:
                type: string
                format: binary
        '404':
          description: File not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: Failed to serve public object.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /storage/objects/{objectPath}:
    get:
      tags: [Storage]
      operationId: getStorageObject
      summary: Serve an object entity from PRIVATE_OBJECT_DIR
      description: |
        Serves object entities uploaded via presigned URLs. These can optionally
        be protected with authentication or ACL checks based on the use case.
      parameters:
        - name: objectPath
          in: path
          required: true
          schema:
            type: string
          description: Object path within the private object dir (e.g. `uploads/some-uuid`).
      responses:
        '200':
          description: Object content streamed with correct Content-Type.
          content:
            '*/*':
              schema:
                type: string
                format: binary
        '404':
          description: Object not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: Failed to serve object.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  schemas:
    HealthStatus:
      type: object
      properties:
        status:
          type: string
      required:
        - status

    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: string

    User:
      type: object
      required:
        - id
        - role
        - fullName
        - age
        - phone
        - email
        - address
        - city
        - district
        - approvalStatus
        - isVip
        - createdAt
      properties:
        id:
          type: integer
        role:
          type: string
          enum: [student, instructor, admin, b2b]
        fullName:
          type: string
        age:
          type: integer
        phone:
          type: string
        phone2:
          type: ["string", "null"]
        email:
          type: string
        address:
          type: string
        city:
          type: string
        district:
          type: string
        university:
          type: ["string", "null"]
        department:
          type: ["string", "null"]
        grade:
          type: ["string", "null"]
        bio:
          type: ["string", "null"]
        photoUrl:
          type: ["string", "null"]
        cvUrl:
          type: ["string", "null"]
        studentDocumentUrl:
          type: ["string", "null"]
        approvalStatus:
          type: string
          enum: [pending, approved, rejected]
        isVip:
          type: boolean
        hourlyRate:
          type: ["integer", "null"]
        refId:
          type: ["string", "null"]
        referredById:
          type: ["integer", "null"]
        b2bBalance:
          type: integer
        b2bPendingBalance:
          type: integer
        referralCommissionPaid:
          type: boolean
        createdAt:
          type: string
          format: date-time

    RegisterInput:
      type: object
      required:
        - role
        - fullName
        - age
        - phone
        - email
        - password
        - address
        - city
        - district
      properties:
        role:
          type: string
          enum: [student, instructor, b2b]
        fullName:
          type: string
          minLength: 2
        age:
          type: integer
          minimum: 14
          maximum: 100
        phone:
          type: string
          minLength: 6
        phone2:
          type: string
        email:
          type: string
        password:
          type: string
          minLength: 6
        address:
          type: string
          minLength: 2
        city:
          type: string
          minLength: 2
        district:
          type: string
          minLength: 2
        university:
          type: string
        department:
          type: string
        grade:
          type: string
        bio:
          type: string
        photoUrl:
          type: string
        cvUrl:
          type: string
        studentDocumentUrl:
          type: string
        hourlyRate:
          type: integer
        referredByRef:
          type: string

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

    AuthResult:
      type: object
      required: [token, user]
      properties:
        token:
          type: string
        user:
          $ref: "#/components/schemas/User"

    InstructorSummary:
      type: object
      required:
        - id
        - fullName
        - city
        - district
        - department
        - isVip
        - createdAt
      properties:
        id:
          type: integer
        fullName:
          type: string
        city:
          type: string
        district:
          type: string
        university:
          type: ["string", "null"]
        department:
          type: ["string", "null"]
        grade:
          type: ["string", "null"]
        bio:
          type: ["string", "null"]
        photoUrl:
          type: ["string", "null"]
        cvUrl:
          type: ["string", "null"]
        isVip:
          type: boolean
        hourlyRate:
          type: ["integer", "null"]
        createdAt:
          type: string
          format: date-time

    CityCount:
      type: object
      required: [name, region, instructorCount]
      properties:
        name:
          type: string
        region:
          type: string
        instructorCount:
          type: integer

    RegionCount:
      type: object
      required: [name, instructorCount]
      properties:
        name:
          type: string
        instructorCount:
          type: integer

    DepartmentCount:
      type: object
      required: [name, instructorCount]
      properties:
        name:
          type: string
        instructorCount:
          type: integer

    Stats:
      type: object
      required: [totalInstructors, totalCities, totalStudents, membershipFee]
      properties:
        totalInstructors:
          type: integer
        totalCities:
          type: integer
        totalStudents:
          type: integer
        membershipFee:
          type: integer

    ContactInput:
      type: object
      required: [name, email, message]
      properties:
        name:
          type: string
          minLength: 2
        email:
          type: string
        phone:
          type: string
        message:
          type: string
          minLength: 5

    ContactMessage:
      type: object
      required: [id, name, email, message, createdAt]
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
        phone:
          type: ["string", "null"]
        message:
          type: string
        createdAt:
          type: string
          format: date-time

    UploadUrlRequest:
      type: object
      required: [name, size, contentType]
      properties:
        name:
          type: string
          minLength: 1
          description: Original file name.
        size:
          type: integer
          minimum: 1
          description: File size in bytes.
        contentType:
          type: string
          minLength: 1
          description: MIME type of the file (e.g. `image/jpeg`).
    UploadUrlResponse:
      type: object
      required: [uploadURL, objectPath]
      properties:
        uploadURL:
          type: string
          description: Presigned GCS URL for PUT upload.
        objectPath:
          type: string
          description: Normalized object path (e.g. `/objects/uploads/uuid`). Store this in your database.
        metadata:
          $ref: '#/components/schemas/UploadUrlRequest'
