openapi: "3.1.0" info: title: QuotesDB API description: A simple quotes database with passphrase-based quote ownership. version: "0.1.0" license: name: MIT OR Apache-2.0 servers: - url: http://localhost:8787 description: Local development - url: https://api.quotesdb.example.com description: Production (Cloudflare Workers) # --------------------------------------------------------------------------- # Security # --------------------------------------------------------------------------- # Auth is per-quote: each quote has a 4-word passphrase (auth_code) that was # returned at creation time. Callers supply it via the X-Auth-Code header for # mutating operations (POST /:id, DELETE /:id). components: securitySchemes: AuthCode: type: apiKey in: header name: X-Auth-Code description: > 4-word passphrase returned when the quote was created (e.g. ocean-table-purple-storm). Required for update and delete. AdminCode: type: apiKey in: header name: X-Admin-Code description: > Super admin passphrase seeded at server startup and printed to the server log. Required for all /api/admin/* endpoints. # ------------------------------------------------------------------------- # Schemas # ------------------------------------------------------------------------- schemas: # Returned for all GET responses — auth_code intentionally omitted. Quote: type: object required: - id - text - author - tags - created_at - updated_at properties: id: type: string description: NanoID (~21 characters). example: "V1StGXR8_Z5jdHi6B-myT" text: type: string description: The quote text. example: "The only way to do great work is to love what you do." author: type: string description: The person attributed with the quote. example: "Steve Jobs" source: type: ["string", "null"] description: Optional source (book, speech, etc.). example: "Stanford Commencement Address, 2005" date: type: ["string", "null"] format: date description: Optional ISO 8601 date (YYYY-MM-DD) associated with the quote. example: "2005-06-12" tags: type: array items: type: string description: Zero or more tags attached to the quote. example: ["work", "inspiration"] created_at: type: string format: date-time description: When the quote was first stored (UTC). updated_at: type: string format: date-time description: When the quote was last modified (UTC). # Returned only from the create (PUT) endpoint — includes auth_code. QuoteCreated: allOf: - $ref: "#/components/schemas/Quote" - type: object required: - auth_code properties: auth_code: type: string description: > 4-word passphrase that authorises future edits and deletes. Store this — it cannot be recovered later. example: "ocean-table-purple-storm" # Request body for PUT /api/quotes (create). QuoteCreateRequest: type: object required: - text - author properties: text: type: string description: The quote text. author: type: string description: The person attributed with the quote. source: type: string description: Optional source (book, speech, etc.). date: type: string format: date description: Optional ISO 8601 date (YYYY-MM-DD). tags: type: array items: type: string description: Zero or more tags. default: [] auth_code: type: string description: > Optional custom auth code. If omitted, a 4-word passphrase is auto-generated by the server and returned in the response. cf_turnstile_token: type: string description: Cloudflare Turnstile CAPTCHA token. Required when the server has TURNSTILE_SECRET_KEY configured. nullable: true # Request body for POST /api/quotes/:id (update — all fields optional). QuoteUpdateRequest: type: object properties: text: type: string description: Replacement quote text. author: type: string description: Replacement author name. source: type: ["string", "null"] description: Replacement source. Pass null to clear. date: type: ["string", "null"] format: date description: Replacement date. Pass null to clear. tags: type: array items: type: string description: Replacement tag list. Replaces all existing tags. # Paginated list of quotes. QuoteList: type: object required: - quotes - page - total_pages - total_count properties: quotes: type: array items: $ref: "#/components/schemas/Quote" page: type: integer minimum: 1 description: Current page number. total_pages: type: integer description: Total number of pages given the current filters. total_count: type: integer description: Total number of quotes matching the current filters. # Response body for GET /api/status. StatusResponse: type: object required: - submissions_locked properties: submissions_locked: type: boolean description: Whether new quote submissions are currently disabled. example: false # Returned by POST /api/admin/lock and POST /api/admin/unlock. LockResponse: type: object required: - submissions_locked properties: submissions_locked: type: boolean description: The current lock state after the operation. example: true # Returned by POST /api/admin/reset-auth-code. ResetAuthCodeResponse: type: object required: - auth_code properties: auth_code: type: string description: The new admin auth code now in effect. example: "ocean-table-purple-storm" # Standard error envelope used by all error responses. Error: type: object required: - error properties: error: type: string description: Human-readable error message. example: "quote not found" # --------------------------------------------------------------------------- # Paths # --------------------------------------------------------------------------- # IMPORTANT — router registration order for the Rust implementation: # GET /api/quotes/random must be registered BEFORE GET /api/quotes/{id} # to prevent "random" being matched as an id parameter. paths: /api/: get: operationId: getOpenApiSpec summary: OpenAPI specification description: Returns this OpenAPI specification as JSON. tags: [meta] responses: "200": description: The OpenAPI spec in JSON format. content: application/json: schema: type: object description: Raw OpenAPI 3.1 document. /api/status: get: operationId: getStatus summary: API status description: > Returns the current operational status of the API. Callers can check submissions_locked to determine whether the PUT /api/quotes endpoint is accepting new submissions. tags: [meta] responses: "200": description: Current API status. content: application/json: schema: $ref: "#/components/schemas/StatusResponse" "500": description: Internal server error. content: application/json: schema: $ref: "#/components/schemas/Error" /api/admin/lock: post: operationId: lockSubmissions summary: Lock new quote submissions description: > Sets submissions_locked to true, preventing any new quotes from being created via PUT /api/quotes. Requires the X-Admin-Code header. The operation is idempotent — locking when already locked returns 200. tags: [admin] security: - AdminCode: [] responses: "200": description: Submissions are now locked. content: application/json: schema: $ref: "#/components/schemas/LockResponse" "403": description: X-Admin-Code header missing or incorrect. content: application/json: schema: $ref: "#/components/schemas/Error" "500": description: Internal server error. content: application/json: schema: $ref: "#/components/schemas/Error" /api/admin/unlock: post: operationId: unlockSubmissions summary: Unlock new quote submissions description: > Sets submissions_locked to false, re-enabling quote creation via PUT /api/quotes. Requires the X-Admin-Code header. The operation is idempotent — unlocking when already unlocked returns 200. tags: [admin] security: - AdminCode: [] responses: "200": description: Submissions are now unlocked. content: application/json: schema: $ref: "#/components/schemas/LockResponse" "403": description: X-Admin-Code header missing or incorrect. content: application/json: schema: $ref: "#/components/schemas/Error" "500": description: Internal server error. content: application/json: schema: $ref: "#/components/schemas/Error" /api/admin/reset-auth-code: post: operationId: resetAdminAuthCode summary: Replace the admin auth code description: > Replaces the stored admin super auth code with a new value. The caller must supply the current code via the X-Admin-Code header. If new_code is omitted from the request body, the server generates a fresh 4-word passphrase. The new code is returned in the response body and is immediately active — the old code is no longer accepted. tags: [admin] security: - AdminCode: [] requestBody: required: true content: application/json: schema: type: object properties: new_code: type: string description: > Optional replacement code. If omitted, the server auto-generates a 4-word passphrase (e.g. "ocean-table-purple-storm"). example: "ocean-table-purple-storm" responses: "200": description: The admin auth code was replaced successfully. content: application/json: schema: $ref: "#/components/schemas/ResetAuthCodeResponse" "403": description: X-Admin-Code header missing or incorrect. content: application/json: schema: $ref: "#/components/schemas/Error" "500": description: Internal server error. content: application/json: schema: $ref: "#/components/schemas/Error" /api/quotes: get: operationId: listQuotes summary: List quotes description: Returns a paginated list of quotes, optionally filtered by author or tag. tags: [quotes] parameters: - name: page in: query description: Page number (1-based). required: false schema: type: integer minimum: 1 default: 1 - name: author in: query description: Filter by exact author name (case-insensitive). required: false schema: type: string - name: tag in: query description: Filter to quotes that have this tag. required: false schema: type: string - name: date_after_year in: query description: Only include quotes dated on or after this year. required: false schema: type: integer minimum: 0 maximum: 9999 - name: date_after_month in: query description: Narrows after-bound to this month (1–12). Requires date_after_year. required: false schema: type: integer minimum: 1 maximum: 12 - name: date_after_day in: query description: Narrows after-bound to this day (1–31). Requires date_after_year and date_after_month. required: false schema: type: integer minimum: 1 maximum: 31 - name: date_before_year in: query description: Only include quotes dated on or before this year. required: false schema: type: integer minimum: 0 maximum: 9999 - name: date_before_month in: query description: Narrows before-bound to this month (1–12). Requires date_before_year. required: false schema: type: integer minimum: 1 maximum: 12 - name: date_before_day in: query description: Narrows before-bound to this day (1–31). Requires date_before_year and date_before_month. required: false schema: type: integer minimum: 1 maximum: 31 responses: "200": description: Paginated list of quotes. content: application/json: schema: $ref: "#/components/schemas/QuoteList" put: operationId: createQuote summary: Create a quote description: > Creates a new quote. If auth_code is omitted from the request body, the server auto-generates a 4-word passphrase and returns it in the response. Store the auth_code — it cannot be recovered later. tags: [quotes] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/QuoteCreateRequest" responses: "201": description: Quote created successfully. content: application/json: schema: $ref: "#/components/schemas/QuoteCreated" "422": description: Validation error (e.g. missing required fields). content: application/json: schema: $ref: "#/components/schemas/Error" "423": description: Submissions are currently locked. content: application/json: schema: $ref: "#/components/schemas/Error" # NOTE: registered before /api/quotes/{id} in the Rust router. /api/quotes/random: get: operationId: getRandomQuote summary: Random quote description: Returns a single randomly selected quote. tags: [quotes] responses: "200": description: A random quote. content: application/json: schema: $ref: "#/components/schemas/Quote" "404": description: No quotes exist in the database. content: application/json: schema: $ref: "#/components/schemas/Error" /api/quotes/{id}: parameters: - name: id in: path required: true description: NanoID of the quote (~21 characters). schema: type: string example: "V1StGXR8_Z5jdHi6B-myT" get: operationId: getQuote summary: Get a quote by ID description: Returns a single quote identified by its NanoID. tags: [quotes] responses: "200": description: The requested quote. content: application/json: schema: $ref: "#/components/schemas/Quote" "404": description: Quote not found. content: application/json: schema: $ref: "#/components/schemas/Error" post: operationId: updateQuote summary: Update a quote description: > Partially updates an existing quote. Only the fields included in the request body are modified. Requires the quote's auth_code via the X-Auth-Code header. tags: [quotes] security: - AuthCode: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/QuoteUpdateRequest" responses: "200": description: The updated quote. content: application/json: schema: $ref: "#/components/schemas/Quote" "403": description: Auth code missing or incorrect. content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Quote not found. content: application/json: schema: $ref: "#/components/schemas/Error" delete: operationId: deleteQuote summary: Delete a quote description: > Permanently deletes a quote. Requires the quote's auth_code via the X-Auth-Code header. tags: [quotes] security: - AuthCode: [] responses: "204": description: Quote deleted successfully. No response body. "403": description: Auth code missing or incorrect. content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Quote not found. content: application/json: schema: $ref: "#/components/schemas/Error" # --------------------------------------------------------------------------- # Tags (for grouping in generated docs) # --------------------------------------------------------------------------- tags: - name: meta description: API metadata endpoints. - name: quotes description: CRUD operations on quotes. - name: admin description: Admin-only endpoints for managing the submissions lock.