Skip to content

Latest commit

 

History

History
1149 lines (938 loc) · 21.5 KB

File metadata and controls

1149 lines (938 loc) · 21.5 KB

API Specification Document

ZapBB Forum Platform

Version: 1.0
Status: Draft
Date: January 20, 2026
Reference: PRD 34c32848.md


Overview

This document provides comprehensive API specifications for ZapBB, including endpoint definitions, request/response formats, authentication requirements, and error handling.

API Design Principles

Core Principles

  1. Command-style for actions: POST /threads/{id}:lock, POST /posts/{id}:approve
  2. Resource CRUD where appropriate: GET /categories, POST /threads, PATCH /posts/{id}
  3. Consistent error responses: JSON error with code, message, details
  4. Pagination by default: ?limit=20&offset=0 or cursor-based for large result sets
  5. OpenAPI v3.1: Full schema coverage, auto-generated Swagger UI at /api/v1/docs

API Versioning

  • Base URL: https://api.example.com/api/v1/
  • Version in URL path: /api/v1/, /api/v2/, etc.
  • Breaking changes require major version bump
  • Deprecated endpoints include X-Deprecated header with sunset date

OpenAPI Contract Flow

Source of Truth

  • Backend uses utoipa to derive OpenAPI v3.1 schema from Rust types and route annotations.
  • Schema endpoint is served at /api/v1/openapi.json.
  • Swagger UI is available at /api/v1/docs.

Client Generation (Frontend)

  • Tooling: OpenAPI-Qraft
  • Output location: frontend/lib/api/generated/
  • Usage: Frontend imports generated hooks instead of manual fetches.

CI Expectations

  • API schema changes must update generated client code in the same PR.
  • CI should validate that generated client code matches the OpenAPI schema.

Authentication & Authorization

Authentication Methods

NextAuth.js Session Authentication (Web App)

  • The Next.js web app uses NextAuth.js (Auth.js) sessions.
  • Session cookies authenticate SSR and API calls originating from Next.js.

JWT Token Authentication (API Clients / Service Tokens)

Headers:

Authorization: Bearer <jwt_token>

Token Structure:

{
  "sub": "user_uuid",
  "role": "member|moderator|admin",
  "exp": 1234567890,
  "iat": 1234567890
}

Permission Model

Permission Description Roles
thread.create Create new threads Member, Moderator, Admin
thread.lock Lock/unlock threads Moderator, Admin
post.create Create posts/replies Member, Moderator, Admin
post.moderate Edit/delete any post Moderator, Admin
user.ban Ban users Moderator, Admin
admin.settings Modify site settings Admin

Common Response Formats

Success Response

{
  "data": { /* resource or collection */ },
  "meta": {
    "request_id": "uuid",
    "timestamp": "2026-01-20T04:00:00Z"
  }
}

Paginated Response

{
  "data": [ /* items */ ],
  "pagination": {
    "limit": 20,
    "offset": 0,
    "total": 150,
    "has_more": true
  },
  "meta": {
    "request_id": "uuid",
    "timestamp": "2026-01-20T04:00:00Z"
  }
}

Error Response

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "User must be authenticated to perform this action",
    "details": {
      "endpoint": "/api/v1/posts",
      "action": "POST",
      "timestamp": "2026-01-20T04:00:00Z"
    }
  }
}

Authentication Endpoints

These endpoints are used for issuing JWTs to API clients and service integrations. The web app uses NextAuth.js session routes under the Next.js app (e.g., /api/auth/*).

POST /api/v1/auth/register

Register a new user account.

Request:

{
  "username": "john_doe",
  "email": "john@example.com",
  "password": "SecureP@ssw0rd"
}

Response (201 Created):

{
  "data": {
    "user": {
      "id": "uuid",
      "username": "john_doe",
      "email": "john@example.com",
      "role": {
        "id": "uuid",
        "name": "Member"
      },
      "created_at": "2026-01-20T04:00:00Z"
    },
    "token": "jwt_token_here",
    "refresh_token": "refresh_token_here"
  }
}

Validation Rules:

  • username: 3-50 characters, alphanumeric + underscore
  • email: Valid email format
  • password: Min 8 characters, must include uppercase, lowercase, number

POST /api/v1/auth/login

Authenticate user and receive tokens.

Request:

{
  "email": "john@example.com",
  "password": "SecureP@ssw0rd"
}

Response (200 OK):

{
  "data": {
    "user": {
      "id": "uuid",
      "username": "john_doe",
      "email": "john@example.com",
      "role": {
        "id": "uuid",
        "name": "Member"
      }
    },
    "token": "jwt_token_here",
    "refresh_token": "refresh_token_here"
  }
}

POST /api/v1/auth/logout

Invalidate current session.

Authorization: Required

Response (204 No Content)

POST /api/v1/auth/refresh-token

Refresh JWT token using refresh token.

Request:

{
  "refresh_token": "refresh_token_here"
}

Response (200 OK):

{
  "data": {
    "token": "new_jwt_token_here",
    "refresh_token": "new_refresh_token_here"
  }
}

Category Endpoints

GET /api/v1/categories

List all categories (forums).

Query Parameters:

  • limit (int, default: 20): Number of results per page
  • offset (int, default: 0): Pagination offset
  • parent_id (uuid, optional): Filter by parent category

Response (200 OK):

{
  "data": [
    {
      "id": "uuid",
      "name": "General Discussion",
      "description": "Talk about anything here",
      "slug": "general-discussion",
      "parent_id": null,
      "display_order": 0,
      "is_locked": false,
      "is_leaf": false,
      "child_count": 3,
      "thread_count": 125,
      "post_count": 1450,
      "created_at": "2026-01-01T00:00:00Z"
    }
  ],
  "pagination": {
    "limit": 20,
    "offset": 0,
    "total": 15
  }
}

GET /api/v1/categories/{id}

Get category details with permissions for current user.

Authorization: Optional (affects permission data)

Response (200 OK):

{
  "data": {
    "id": "uuid",
    "name": "General Discussion",
    "description": "Talk about anything here",
    "slug": "general-discussion",
    "parent_id": null,
    "display_order": 0,
    "is_locked": false,
    "thread_count": 125,
    "post_count": 1450,
    "permissions": {
      "can_view": true,
      "can_create_thread": true,
      "can_reply": true,
      "can_moderate": false
    },
    "created_at": "2026-01-01T00:00:00Z"
  }
}

POST /api/v1/categories

Create a new category.

Authorization: Required (Admin only)

Request:

{
  "name": "New Category",
  "description": "Description here",
  "parent_id": null,
  "display_order": 10
}

Response (201 Created):

{
  "data": {
    "id": "uuid",
    "name": "New Category",
    "description": "Description here",
    "slug": "new-category",
    "parent_id": null,
    "display_order": 10,
    "is_locked": false,
    "thread_count": 0,
    "post_count": 0,
    "created_at": "2026-01-20T04:00:00Z"
  }
}

PATCH /api/v1/categories/{id}

Update category details.

Authorization: Required (Admin only)

Request:

{
  "name": "Updated Name",
  "description": "Updated description",
  "display_order": 5
}

Response (200 OK):

{
  "data": {
    "id": "uuid",
    "name": "Updated Name",
    "description": "Updated description",
    "slug": "updated-name",
    "display_order": 5,
    "updated_at": "2026-01-20T04:00:00Z"
  }
}

Thread Endpoints

GET /api/v1/categories/{category_id}/threads

List threads in a category.

Query Parameters:

  • limit (int, default: 20)
  • offset (int, default: 0)
  • sort_by (string): recent, popular, pinned (default: recent)

Response (200 OK):

{
  "data": [
    {
      "id": "uuid",
      "title": "Welcome to ZapBB!",
      "slug": "welcome-to-zapbb",
      "category_id": "uuid",
      "author": {
        "id": "uuid",
        "username": "admin",
        "profile": {
          "avatar_url": "https://..."
        }
      },
      "is_locked": false,
      "is_pinned": true,
      "is_sticky": false,
      "reply_count": 25,
      "view_count": 350,
      "last_post_at": "2026-01-20T03:00:00Z",
      "created_at": "2026-01-01T00:00:00Z"
    }
  ],
  "pagination": {
    "limit": 20,
    "offset": 0,
    "total": 125
  }
}

GET /api/v1/threads/{id}

Get thread details including first post.

Response (200 OK):

{
  "data": {
    "id": "uuid",
    "title": "Welcome to ZapBB!",
    "slug": "welcome-to-zapbb",
    "category": {
      "id": "uuid",
      "name": "General Discussion",
      "slug": "general-discussion"
    },
    "author": {
      "id": "uuid",
      "username": "admin"
    },
    "first_post": {
      "id": "uuid",
      "content": "Welcome everyone!",
      "content_html": "<p>Welcome everyone!</p>",
      "created_at": "2026-01-01T00:00:00Z"
    },
    "is_locked": false,
    "is_pinned": true,
    "reply_count": 25,
    "view_count": 350,
    "created_at": "2026-01-01T00:00:00Z"
  }
}

GET /api/v1/threads/{id}/posts

Get paginated posts in a thread.

Query Parameters:

  • limit (int, default: 20)
  • offset (int, default: 0)

Response (200 OK):

{
  "data": [
    {
      "id": "uuid",
      "thread_id": "uuid",
      "author": {
        "id": "uuid",
        "username": "user123",
        "profile": {
          "avatar_url": "https://...",
          "custom_title": "Forum Veteran"
        }
      },
      "content": "Great forum!",
      "content_html": "<p>Great forum!</p>",
      "reply_to_id": null,
      "is_deleted": false,
      "edited_at": null,
      "reaction_count": 5,
      "reactions": [
        { "emoji": "👍", "count": 3 },
        { "emoji": "❤️", "count": 2 }
      ],
      "created_at": "2026-01-02T00:00:00Z"
    }
  ],
  "pagination": {
    "limit": 20,
    "offset": 0,
    "total": 25
  }
}

POST /api/v1/threads

Create a new thread.

Authorization: Required

Validation:

  • category_id must reference a leaf category (a category with no children). Attempting to create a thread under a parent category will return a 400 Bad Request error.

Request:

{
  "title": "My New Thread",
  "content": "This is the first post content...",
  "category_id": "uuid (must be a leaf category)"
}

Response (201 Created):

{
  "data": {
    "thread": {
      "id": "uuid",
      "title": "My New Thread",
      "slug": "my-new-thread",
      "category_id": "uuid",
      "author_id": "uuid",
      "created_at": "2026-01-20T04:00:00Z"
    },
    "post": {
      "id": "uuid",
      "thread_id": "uuid",
      "content": "This is the first post content...",
      "created_at": "2026-01-20T04:00:00Z"
    }
  }
}

PATCH /api/v1/threads/{id}

Update thread (title only).

Authorization: Required (Author or Moderator)

Request:

{
  "title": "Updated Thread Title"
}

Response (200 OK):

{
  "data": {
    "id": "uuid",
    "title": "Updated Thread Title",
    "slug": "updated-thread-title",
    "updated_at": "2026-01-20T04:00:00Z"
  }
}

DELETE /api/v1/threads/{id}

Soft-delete a thread.

Authorization: Required (Author or Moderator)

Response (204 No Content)

POST /api/v1/threads/{id}:lock

Lock a thread (moderator action).

Authorization: Required (Moderator or Admin)

Request:

{
  "reason": "Off-topic discussion"
}

Response (200 OK):

{
  "data": {
    "thread": {
      "id": "uuid",
      "is_locked": true,
      "updated_at": "2026-01-20T04:00:00Z"
    },
    "mod_log": {
      "id": "uuid",
      "action": "thread_lock",
      "reason": "Off-topic discussion",
      "created_at": "2026-01-20T04:00:00Z"
    }
  }
}

POST /api/v1/threads/{id}:unlock

Unlock a thread (moderator action).

Authorization: Required (Moderator or Admin)

Response (200 OK)

POST /api/v1/threads/{id}:pin

Pin a thread to top of category.

Authorization: Required (Moderator or Admin)

Response (200 OK)

POST /api/v1/threads/{id}:unpin

Unpin a thread.

Authorization: Required (Moderator or Admin)

Response (200 OK)

POST /api/v1/threads/{id}:move

Move thread to different category.

Authorization: Required (Moderator or Admin)

Request:

{
  "new_category_id": "uuid",
  "reason": "Better suited for this category"
}

Response (200 OK):

{
  "data": {
    "thread": {
      "id": "uuid",
      "category_id": "uuid",
      "updated_at": "2026-01-20T04:00:00Z"
    },
    "mod_log": {
      "action": "thread_move",
      "reason": "Better suited for this category"
    }
  }
}

Post Endpoints

GET /api/v1/posts/{id}

Get post details with edit history and reactions.

Response (200 OK):

{
  "data": {
    "id": "uuid",
    "thread_id": "uuid",
    "author": {
      "id": "uuid",
      "username": "user123"
    },
    "content": "Post content here",
    "content_html": "<p>Post content here</p>",
    "reply_to_id": null,
    "is_deleted": false,
    "edited_at": "2026-01-15T10:00:00Z",
    "edit_reason": "Fixed typo",
    "reaction_count": 10,
    "reactions": [
      { "emoji": "👍", "count": 7, "users": ["user1", "user2"] },
      { "emoji": "❤️", "count": 3, "users": ["user3"] }
    ],
    "edit_history": [
      {
        "edited_at": "2026-01-15T10:00:00Z",
        "editor": "user123",
        "reason": "Fixed typo"
      }
    ],
    "created_at": "2026-01-10T00:00:00Z"
  }
}

POST /api/v1/posts

Create a new post (reply).

Authorization: Required

Request:

{
  "thread_id": "uuid",
  "content": "My reply to this thread...",
  "reply_to_id": "uuid" // Optional, for nested replies
}

Response (201 Created):

{
  "data": {
    "id": "uuid",
    "thread_id": "uuid",
    "author_id": "uuid",
    "content": "My reply to this thread...",
    "content_html": "<p>My reply to this thread...</p>",
    "reply_to_id": "uuid",
    "created_at": "2026-01-20T04:00:00Z"
  }
}

PATCH /api/v1/posts/{id}

Edit a post.

Authorization: Required (Author or Moderator)

Request:

{
  "content": "Updated post content...",
  "edit_reason": "Fixed grammar"
}

Response (200 OK):

{
  "data": {
    "id": "uuid",
    "content": "Updated post content...",
    "content_html": "<p>Updated post content...</p>",
    "edited_at": "2026-01-20T04:00:00Z",
    "edit_reason": "Fixed grammar"
  }
}

DELETE /api/v1/posts/{id}

Soft-delete a post.

Authorization: Required (Author or Moderator)

Response (204 No Content)

POST /api/v1/posts/{id}:approve

Approve a post in moderation queue.

Authorization: Required (Moderator or Admin)

Response (200 OK):

{
  "data": {
    "id": "uuid",
    "is_approved": true,
    "updated_at": "2026-01-20T04:00:00Z"
  }
}

POST /api/v1/posts/{id}:reject

Reject a post in moderation queue.

Authorization: Required (Moderator or Admin)

Request:

{
  "reason": "Spam content"
}

Response (200 OK)

Reaction Endpoints

POST /api/v1/posts/{id}/reactions

Add a reaction to a post.

Authorization: Required

Request:

{
  "emoji": "👍"
}

Response (200 OK):

{
  "data": {
    "post_id": "uuid",
    "emoji": "👍",
    "count": 8
  }
}

DELETE /api/v1/posts/{post_id}/reactions/{emoji}

Remove user's reaction from a post.

Authorization: Required

Response (204 No Content)

GET /api/v1/posts/{id}/reactions

Get all reactions for a post.

Response (200 OK):

{
  "data": [
    {
      "emoji": "👍",
      "count": 7,
      "users": [
        { "id": "uuid", "username": "user1" },
        { "id": "uuid", "username": "user2" }
      ]
    },
    {
      "emoji": "❤️",
      "count": 3,
      "users": [
        { "id": "uuid", "username": "user3" }
      ]
    }
  ]
}

Search Endpoints

GET /api/v1/search

Full-text search across posts and threads.

Query Parameters:

  • q (string, required): Search query
  • category_id (uuid, optional): Filter by category
  • author_id (uuid, optional): Filter by author
  • created_after (date, optional): Filter by date
  • limit (int, default: 20)
  • offset (int, default: 0)

Response (200 OK):

{
  "data": [
    {
      "type": "post",
      "post_id": "uuid",
      "thread_id": "uuid",
      "thread_title": "Welcome to ZapBB!",
      "excerpt": "...matching content...",
      "author": {
        "id": "uuid",
        "username": "user123"
      },
      "category": {
        "id": "uuid",
        "name": "General Discussion"
      },
      "relevance_score": 0.95,
      "created_at": "2026-01-10T00:00:00Z"
    }
  ],
  "pagination": {
    "limit": 20,
    "offset": 0,
    "total": 150
  }
}

Moderation Endpoints

GET /api/v1/admin/mod-logs

Get moderation action logs.

Authorization: Required (Moderator or Admin)

Query Parameters:

  • action (string, optional): Filter by action type
  • moderator_id (uuid, optional): Filter by moderator
  • target_type (string, optional): Filter by target type
  • limit (int, default: 50)
  • offset (int, default: 0)

Response (200 OK):

{
  "data": [
    {
      "id": "uuid",
      "moderator": {
        "id": "uuid",
        "username": "mod_user"
      },
      "action": "thread_lock",
      "target_type": "thread",
      "target_id": "uuid",
      "reason": "Off-topic discussion",
      "metadata": {
        "previous_state": { "is_locked": false }
      },
      "created_at": "2026-01-20T04:00:00Z"
    }
  ],
  "pagination": {
    "limit": 50,
    "offset": 0,
    "total": 250
  }
}

GET /api/v1/admin/users/{id}

Get user details including warnings and ban status.

Authorization: Required (Moderator or Admin)

Response (200 OK):

{
  "data": {
    "id": "uuid",
    "username": "user123",
    "email": "user@example.com",
    "role": {
      "id": "uuid",
      "name": "Member"
    },
    "is_banned": false,
    "warnings": [
      {
        "id": "uuid",
        "reason": "Inappropriate language",
        "severity": "warning",
        "issued_by": "mod_user",
        "issued_at": "2026-01-15T00:00:00Z"
      }
    ],
    "ban_history": [],
    "post_count": 150,
    "thread_count": 25,
    "created_at": "2026-01-01T00:00:00Z"
  }
}

POST /api/v1/admin/users/{id}:warn

Issue a warning to a user.

Authorization: Required (Moderator or Admin)

Request:

{
  "reason": "Inappropriate language",
  "severity": "warning",
  "expires_at": "2026-02-20T00:00:00Z"
}

Response (200 OK):

{
  "data": {
    "warning": {
      "id": "uuid",
      "user_id": "uuid",
      "reason": "Inappropriate language",
      "severity": "warning",
      "issued_by_id": "uuid",
      "expires_at": "2026-02-20T00:00:00Z",
      "issued_at": "2026-01-20T04:00:00Z"
    },
    "mod_log": {
      "id": "uuid",
      "action": "user_warn"
    }
  }
}

POST /api/v1/admin/users/{id}:ban

Ban a user.

Authorization: Required (Moderator or Admin)

Request:

{
  "reason": "Repeated spam",
  "is_permanent": false,
  "banned_until": "2026-02-20T00:00:00Z"
}

Response (200 OK):

{
  "data": {
    "ban": {
      "id": "uuid",
      "user_id": "uuid",
      "reason": "Repeated spam",
      "is_permanent": false,
      "banned_until": "2026-02-20T00:00:00Z",
      "issued_by_id": "uuid",
      "created_at": "2026-01-20T04:00:00Z"
    },
    "mod_log": {
      "id": "uuid",
      "action": "user_ban"
    }
  }
}

POST /api/v1/admin/users/{id}:unban

Unban a user.

Authorization: Required (Moderator or Admin)

Response (200 OK)

Admin Settings Endpoints

GET /api/v1/admin/settings

Get all site settings.

Authorization: Required (Admin only)

Response (200 OK):

{
  "data": {
    "site_name": "ZapBB Forum",
    "site_tagline": "Lightning-fast discussions",
    "registration_open": true,
    "theme_id": "default",
    "moderation_queue_enabled": false,
    "smtp_configured": true,
    "max_attachment_size": 5242880
  }
}

PATCH /api/v1/admin/settings

Update site settings.

Authorization: Required (Admin only)

Request:

{
  "site_name": "My Forum",
  "registration_open": false
}

Response (200 OK):

{
  "data": {
    "site_name": "My Forum",
    "registration_open": false,
    "updated_at": "2026-01-20T04:00:00Z"
  }
}

Rate Limiting

All endpoints are rate-limited to prevent abuse:

Endpoint Type Rate Limit
Authentication 5 requests/minute
Read Operations 100 requests/minute
Write Operations 30 requests/minute
Search 20 requests/minute
Admin Operations 50 requests/minute

Rate Limit Headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 75
X-RateLimit-Reset: 1640000000

Error Codes

Code HTTP Status Description
UNAUTHORIZED 401 Not authenticated
FORBIDDEN 403 Not authorized for this action
NOT_FOUND 404 Resource not found
VALIDATION_ERROR 422 Request validation failed
RATE_LIMIT_EXCEEDED 429 Too many requests
INTERNAL_ERROR 500 Server error
SERVICE_UNAVAILABLE 503 Service temporarily unavailable

Webhook Support (Phase 2)

Future support for webhooks to notify external systems of events:

  • thread.created
  • post.created
  • user.registered
  • user.banned
  • moderation.action

References


Document Status: Draft
Next Review: Upon implementation start