1051 lines
27 KiB
Markdown
1051 lines
27 KiB
Markdown
# PedsHub API Reference
|
|
|
|
Base URL: `/api`
|
|
|
|
All endpoints return JSON unless noted otherwise. Authentication is via Bearer token in the `Authorization` header. Role hierarchy: **admin > moderator > user**.
|
|
|
|
---
|
|
|
|
## Auth (`/api/auth`)
|
|
|
|
### POST `/api/auth/register`
|
|
|
|
Register a new user account.
|
|
|
|
- **Auth:** None
|
|
- **Rate limit:** Turnstile verification required if configured
|
|
- **Request body:**
|
|
```json
|
|
{
|
|
"email": "string",
|
|
"password": "string (min 8 chars)",
|
|
"name": "string",
|
|
"turnstile_token": "string | null"
|
|
}
|
|
```
|
|
- **Response:**
|
|
- First user ever: `{"access_token": "...", "token_type": "bearer"}` (auto-admin, auto-verified)
|
|
- Subsequent users: `{"requires_verification": true, "message": "..."}`
|
|
- **Notes:** Registration can be disabled by admin via settings. First registered user is always admin and auto-verified.
|
|
|
|
### POST `/api/auth/login`
|
|
|
|
Authenticate and receive a JWT token.
|
|
|
|
- **Auth:** None
|
|
- **Rate limit:** 10 attempts per IP per 15 minutes (Redis). Turnstile verification if configured.
|
|
- **Request body:**
|
|
```json
|
|
{
|
|
"email": "string",
|
|
"password": "string",
|
|
"turnstile_token": "string | null"
|
|
}
|
|
```
|
|
- **Response:** `{"access_token": "string", "token_type": "bearer"}`
|
|
- **Errors:** 401 invalid credentials, 403 email not verified, 429 rate limited
|
|
|
|
### GET `/api/auth/verify-email`
|
|
|
|
Verify a user's email address via token link.
|
|
|
|
- **Auth:** None
|
|
- **Query params:** `token` (string, required)
|
|
- **Response:** `{"message": "Email verified successfully! You can now log in."}`
|
|
- **Errors:** 400 invalid/expired token
|
|
|
|
### POST `/api/auth/resend-verification`
|
|
|
|
Resend the email verification link.
|
|
|
|
- **Auth:** None
|
|
- **Request body:** `{"email": "string"}`
|
|
- **Response:** `{"message": "If that email exists, a verification link has been sent."}`
|
|
- **Notes:** Deliberately vague response to prevent email enumeration.
|
|
|
|
### POST `/api/auth/forgot-password`
|
|
|
|
Request a password reset link.
|
|
|
|
- **Auth:** None
|
|
- **Rate limit:** 3 reset requests per email per hour
|
|
- **Request body:** `{"email": "string"}`
|
|
- **Response:** `{"message": "If that email is registered, a reset link has been sent."}`
|
|
- **Notes:** Deliberately vague response to prevent email enumeration. Sends email in background.
|
|
|
|
### POST `/api/auth/reset-password`
|
|
|
|
Reset password using a valid token.
|
|
|
|
- **Auth:** None
|
|
- **Request body:**
|
|
```json
|
|
{
|
|
"token": "string",
|
|
"new_password": "string (min 8 chars)"
|
|
}
|
|
```
|
|
- **Response:** `{"message": "Password reset successfully. You can now log in."}`
|
|
- **Errors:** 400 invalid/expired/used token
|
|
|
|
### GET `/api/auth/me`
|
|
|
|
Get the current authenticated user's profile.
|
|
|
|
- **Auth:** User
|
|
- **Response:** `UserResponse` — `{id, email, name, role, created_at}`
|
|
|
|
### PUT `/api/auth/me`
|
|
|
|
Update current user's profile (name, password).
|
|
|
|
- **Auth:** User
|
|
- **Request body:**
|
|
```json
|
|
{
|
|
"name": "string | null",
|
|
"current_password": "string | null (required to change password)",
|
|
"new_password": "string | null (min 8 chars)"
|
|
}
|
|
```
|
|
- **Response:** `UserResponse`
|
|
|
|
### GET `/api/auth/me/settings`
|
|
|
|
Get user settings stored in Redis (e.g. Nextcloud config).
|
|
|
|
- **Auth:** User
|
|
- **Response:** `{}` or a JSON object of user settings
|
|
|
|
### PUT `/api/auth/me/settings`
|
|
|
|
Save user settings to Redis.
|
|
|
|
- **Auth:** User
|
|
- **Request body:** Any JSON object
|
|
- **Response:** `{"saved": true}`
|
|
|
|
---
|
|
|
|
## Documents (`/api/documents`)
|
|
|
|
### POST `/api/documents/upload`
|
|
|
|
Upload a PDF document for processing.
|
|
|
|
- **Auth:** Moderator
|
|
- **Content-Type:** `multipart/form-data`
|
|
- **Request:** `file` (PDF, max size from `MAX_UPLOAD_SIZE` setting)
|
|
- **Response:** `DocumentResponse` — `{id, user_id, filename, original_filename, status, total_pages, error_message, uploaded_at}`
|
|
- **Notes:** Processing dispatched to Celery (falls back to sync). Status starts as `"processing"`.
|
|
|
|
### GET `/api/documents/`
|
|
|
|
List all documents. Moderators see all; regular users see only their own.
|
|
|
|
- **Auth:** User
|
|
- **Response:** `DocumentResponse[]`
|
|
|
|
### GET `/api/documents/{document_id}`
|
|
|
|
Get a single document by ID.
|
|
|
|
- **Auth:** User (own documents only; moderators can access any)
|
|
- **Response:** `DocumentResponse`
|
|
- **Errors:** 404
|
|
|
|
### GET `/api/documents/{document_id}/status`
|
|
|
|
Poll document processing status.
|
|
|
|
- **Auth:** User (own documents only; moderators can access any)
|
|
- **Response:** `{id, status, total_pages, error_message}`
|
|
|
|
### GET `/api/documents/{document_id}/processing-steps`
|
|
|
|
Get PDF processing progress steps from Redis.
|
|
|
|
- **Auth:** User (own documents only; moderators can access any)
|
|
- **Response:** `{"steps": [{step object}, ...]}`
|
|
- **Notes:** Steps are stored in Redis list `pdf:steps:{document_id}`.
|
|
|
|
### POST `/api/documents/{document_id}/sections`
|
|
|
|
Create a named page range section within a document.
|
|
|
|
- **Auth:** Moderator
|
|
- **Request body:**
|
|
```json
|
|
{
|
|
"name": "string",
|
|
"start_page": "int (>= 1)",
|
|
"end_page": "int (> start_page, <= total_pages)"
|
|
}
|
|
```
|
|
- **Response:** `SectionResponse` — `{id, document_id, name, start_page, end_page}`
|
|
- **Errors:** 400 if document not ready, invalid page range
|
|
|
|
### DELETE `/api/documents/{document_id}/sections/{section_id}`
|
|
|
|
Delete a section.
|
|
|
|
- **Auth:** Moderator
|
|
- **Response:** 204 No Content
|
|
|
|
### DELETE `/api/documents/{document_id}`
|
|
|
|
Delete a document, its file, and its ChromaDB vector collection.
|
|
|
|
- **Auth:** Moderator
|
|
- **Response:** 204 No Content
|
|
|
|
---
|
|
|
|
## Quizzes (`/api/quizzes`)
|
|
|
|
### POST `/api/quizzes/`
|
|
|
|
Start async quiz extraction from a document section.
|
|
|
|
- **Auth:** Moderator
|
|
- **Request body:**
|
|
```json
|
|
{
|
|
"section_id": "int",
|
|
"title": "string",
|
|
"mode": "timed | learning",
|
|
"time_limit_minutes": "int | null",
|
|
"model_id": "int | null (AIModelConfig.id)",
|
|
"question_category_id": "int | null",
|
|
"extraction_mode": "string | null (standard, questions_only, two_step, regex, ai_decide, generate)"
|
|
}
|
|
```
|
|
- **Response:** `{"job_id": "uuid", "status": "pending"}`
|
|
- **Notes:** Poll `/api/quizzes/job/{job_id}` for progress. Falls back to sync if Celery unavailable.
|
|
|
|
### GET `/api/quizzes/jobs`
|
|
|
|
List all recent extraction jobs for the current user (any browser session).
|
|
|
|
- **Auth:** User
|
|
- **Response:**
|
|
```json
|
|
[
|
|
{
|
|
"job_id": "uuid",
|
|
"title": "string",
|
|
"status": "pending | running | completed | failed | cancelled",
|
|
"steps_count": "int",
|
|
"last_step": "string (truncated to 80 chars)",
|
|
"quiz_id": "int | null"
|
|
}
|
|
]
|
|
```
|
|
|
|
### GET `/api/quizzes/job/{job_id}`
|
|
|
|
Poll extraction job progress.
|
|
|
|
- **Auth:** Moderator
|
|
- **Response:** `{"job_id", "status", "steps": [...], "quiz_id" (if completed), "error" (if failed)}`
|
|
|
|
### POST `/api/quizzes/job/{job_id}/cancel`
|
|
|
|
Cancel a running extraction job.
|
|
|
|
- **Auth:** User (must own the job)
|
|
- **Response:** `{"status": "cancelled"}`
|
|
- **Errors:** 403 if not owner, 400 if not running
|
|
|
|
### GET `/api/quizzes/search`
|
|
|
|
Hybrid semantic + keyword search across quiz titles and questions.
|
|
|
|
- **Auth:** User
|
|
- **Query params:**
|
|
- `q` (string, required, 2-200 chars)
|
|
- `mode` (string: `"title"` | `"questions"` | `"all"`, default `"all"`)
|
|
- **Response:** Array of search results with `quiz_id`, `quiz_title`, `match_type`, `matching_questions[]`
|
|
- **Notes:** Uses pgvector cosine similarity (threshold 0.30) for semantic search, plus ILIKE for keyword matching.
|
|
|
|
### GET `/api/quizzes/`
|
|
|
|
List all quizzes. Moderators see all (non-deleted); regular users see only published.
|
|
|
|
- **Auth:** User
|
|
- **Response:** `QuizResponse[]` — `{id, section_id, user_id, title, questions_count, mode, time_limit_minutes, is_published, created_at}`
|
|
|
|
### GET `/api/quizzes/{quiz_id}`
|
|
|
|
Get a quiz for taking. Learning-mode quizzes include answers/explanations.
|
|
|
|
- **Auth:** User
|
|
- **Query params:** `study` (bool, default false) — forces answers/explanations to be included
|
|
- **Response:** `QuizDetail` (timed) or `QuizLearningDetail` (learning/study)
|
|
- **Notes:** Excludes soft-deleted quizzes.
|
|
|
|
### PATCH `/api/quizzes/{quiz_id}`
|
|
|
|
Update quiz metadata (title).
|
|
|
|
- **Auth:** Moderator
|
|
- **Request body:** `{"title": "string"}`
|
|
- **Response:** `QuizResponse`
|
|
|
|
### POST `/api/quizzes/{quiz_id}/shuffle`
|
|
|
|
Return quiz with shuffled question and option order (does not modify DB).
|
|
|
|
- **Auth:** User
|
|
- **Query params:** `shuffle_options` (bool, default true)
|
|
- **Response:** `QuizDetail` with shuffled questions
|
|
|
|
### GET `/api/quizzes/{quiz_id}/review`
|
|
|
|
Get quiz with full answers. Requires a completed attempt (or moderator role).
|
|
|
|
- **Auth:** User (must have completed attempt) or Moderator
|
|
- **Response:** `QuizReview`
|
|
|
|
### GET `/api/quizzes/{quiz_id}/questions`
|
|
|
|
Get all questions with answers for editing.
|
|
|
|
- **Auth:** Moderator
|
|
- **Response:** Array of `{id, question_text, question_type, options, correct_answer, explanation}`
|
|
|
|
### PATCH `/api/quizzes/{quiz_id}/questions/{question_id}`
|
|
|
|
Update a question. Changes are shared across all quizzes referencing this question.
|
|
|
|
- **Auth:** Moderator
|
|
- **Request body:** Any of `{question_text, options, correct_answer, explanation, question_type}`
|
|
- **Response:** Updated question object
|
|
- **Errors:** 400 if correct_answer does not match one of the options
|
|
|
|
### DELETE `/api/quizzes/{quiz_id}/questions/{question_id}`
|
|
|
|
Remove a question from this quiz. If shared with other quizzes, only the link is removed. If exclusive, the question is deleted from the bank.
|
|
|
|
- **Auth:** Moderator
|
|
- **Response:** 204 No Content
|
|
|
|
### PATCH `/api/quizzes/{quiz_id}/publish`
|
|
|
|
Show or hide a quiz for regular users.
|
|
|
|
- **Auth:** Moderator
|
|
- **Query params:** `published` (bool, default true)
|
|
- **Response:** `{"quiz_id": int, "is_published": int}`
|
|
|
|
### GET `/api/quizzes/trash`
|
|
|
|
List soft-deleted quizzes.
|
|
|
|
- **Auth:** Moderator
|
|
- **Response:** `QuizResponse[]`
|
|
|
|
### DELETE `/api/quizzes/{quiz_id}`
|
|
|
|
Soft-delete a quiz (move to trash).
|
|
|
|
- **Auth:** Moderator
|
|
- **Response:** 204 No Content
|
|
|
|
### PATCH `/api/quizzes/{quiz_id}/restore`
|
|
|
|
Restore a soft-deleted quiz from trash.
|
|
|
|
- **Auth:** Moderator
|
|
- **Response:** `QuizResponse`
|
|
|
|
### DELETE `/api/quizzes/{quiz_id}/permanent`
|
|
|
|
Permanently delete a quiz that is already in trash. Exclusive questions are orphaned to the bank.
|
|
|
|
- **Auth:** Moderator
|
|
- **Response:** 204 No Content
|
|
- **Errors:** 400 if quiz is not in trash
|
|
|
|
---
|
|
|
|
## Questions (`/api/questions`)
|
|
|
|
### GET `/api/questions/bank`
|
|
|
|
List all questions across all quizzes with filtering, search, and pagination.
|
|
|
|
- **Auth:** User
|
|
- **Query params:**
|
|
- `q` (string, optional) — keyword/semantic search
|
|
- `quiz_id` (int, optional) — filter by source quiz
|
|
- `category_id` (int, optional) — filter by question category
|
|
- `category_ids` (string, optional) — comma-separated category IDs (OR filter)
|
|
- `uncategorized` (bool, default false)
|
|
- `favorites_only` (bool, default false)
|
|
- `tag_ids` (string, optional) — comma-separated tag IDs (AND filter)
|
|
- `search_mode` (string: `"keyword"` | `"semantic"` | `"hybrid"`, default `"hybrid"`)
|
|
- `limit` (int, default 50, max 200)
|
|
- `offset` (int, default 0)
|
|
- **Response:**
|
|
```json
|
|
{
|
|
"total": "int",
|
|
"questions": [
|
|
{
|
|
"id": "int",
|
|
"quiz_id": "int",
|
|
"quiz_title": "string",
|
|
"question_category_id": "int | null",
|
|
"question_category_name": "string | null",
|
|
"question_text": "string",
|
|
"question_type": "string",
|
|
"options": ["string"],
|
|
"correct_answer": "string",
|
|
"explanation": "string",
|
|
"image_path": "string | null"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
- **Notes:** Hybrid search returns semantic matches first (pgvector cosine similarity >= 0.55), then keyword matches.
|
|
|
|
### GET `/api/questions/bank/ids`
|
|
|
|
Return just question IDs for all matching questions (for server-side select-all).
|
|
|
|
- **Auth:** User
|
|
- **Query params:** Same filters as `/bank` (except `limit`, `offset`, `search_mode`)
|
|
- **Response:** `[int]`
|
|
|
|
### PATCH `/api/questions/{question_id}/category`
|
|
|
|
Assign or remove a question category.
|
|
|
|
- **Auth:** Moderator
|
|
- **Query params:** `category_id` (int | null)
|
|
- **Response:** `{"question_id": int, "question_category_id": int | null}`
|
|
|
|
### POST `/api/questions/from-bank`
|
|
|
|
Create a new quiz by referencing existing bank questions (no copying -- edits propagate).
|
|
|
|
- **Auth:** Moderator
|
|
- **Request body:**
|
|
```json
|
|
{
|
|
"title": "string",
|
|
"question_ids": [1, 2, 3],
|
|
"mode": "timed | learning",
|
|
"time_limit_minutes": "int | null"
|
|
}
|
|
```
|
|
- **Response:** `{"id": int, "title": "string", "questions_count": int}`
|
|
|
|
### POST `/api/questions/bulk-category`
|
|
|
|
Assign or remove a category from multiple questions at once.
|
|
|
|
- **Auth:** Moderator
|
|
- **Request body:**
|
|
```json
|
|
{
|
|
"question_ids": [1, 2, 3],
|
|
"category_id": "int | null"
|
|
}
|
|
```
|
|
- **Response:** `{"updated": int, "question_category_id": int | null}`
|
|
|
|
---
|
|
|
|
## Attempts (`/api/attempts`)
|
|
|
|
### POST `/api/attempts/start`
|
|
|
|
Start a new quiz attempt (or resume the most recent incomplete one).
|
|
|
|
- **Auth:** User
|
|
- **Query params:**
|
|
- `quiz_id` (int, required)
|
|
- `fresh` (bool, default false) — force a new attempt even if one is in progress
|
|
- **Response:** `AttemptResponse` — `{id, quiz_id, score, total_questions, percentage, started_at, completed_at}`
|
|
|
|
### POST `/api/attempts/{attempt_id}/submit`
|
|
|
|
Submit answers for a quiz attempt.
|
|
|
|
- **Auth:** User (must own the attempt)
|
|
- **Request body:**
|
|
```json
|
|
{
|
|
"answers": [
|
|
{"question_id": "int", "user_answer": "string"}
|
|
]
|
|
}
|
|
```
|
|
- **Response:** `AttemptDetail` — includes `answers[]` with full review (question text, options, user answer, correct answer, is_correct, explanation)
|
|
- **Notes:** Updates spaced repetition reminder schedule. Clears saved progress from Redis. Unanswered questions are marked incorrect.
|
|
|
|
### GET `/api/attempts/`
|
|
|
|
List all attempts for the current user.
|
|
|
|
- **Auth:** User
|
|
- **Query params:** `quiz_id` (int, optional)
|
|
- **Response:** `AttemptResponse[]`
|
|
|
|
### GET `/api/attempts/{attempt_id}`
|
|
|
|
Get a specific attempt with full answer details.
|
|
|
|
- **Auth:** User (must own the attempt)
|
|
- **Response:** `AttemptDetail`
|
|
|
|
### GET `/api/attempts/stats/dashboard`
|
|
|
|
Get dashboard statistics for the current user.
|
|
|
|
- **Auth:** User
|
|
- **Response:**
|
|
```json
|
|
{
|
|
"total_documents": "int",
|
|
"total_quizzes": "int (distinct quizzes attempted)",
|
|
"total_attempts": "int",
|
|
"average_score": "float",
|
|
"quiz_stats": [
|
|
{
|
|
"quiz_id": "int",
|
|
"quiz_title": "string",
|
|
"attempts_count": "int",
|
|
"best_score": "float",
|
|
"latest_score": "float",
|
|
"average_score": "float"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### GET `/api/attempts/history`
|
|
|
|
Get per-quiz attempt history for the performance line graph.
|
|
|
|
- **Auth:** User
|
|
- **Response:**
|
|
```json
|
|
[
|
|
{
|
|
"quiz_id": "int",
|
|
"title": "string",
|
|
"attempts": [
|
|
{"attempt_id": "int", "date": "ISO string", "percentage": "float", "score": "int", "total": "int"}
|
|
]
|
|
}
|
|
]
|
|
```
|
|
|
|
### GET `/api/attempts/in-progress`
|
|
|
|
Return all incomplete (not yet submitted) attempts for the current user.
|
|
|
|
- **Auth:** User
|
|
- **Response:**
|
|
```json
|
|
[
|
|
{
|
|
"attempt_id": "int",
|
|
"quiz_id": "int",
|
|
"quiz_title": "string",
|
|
"total_questions": "int",
|
|
"started_at": "ISO string"
|
|
}
|
|
]
|
|
```
|
|
|
|
### GET `/api/attempts/{attempt_id}/in-progress`
|
|
|
|
Return the latest incomplete attempt for a specific quiz.
|
|
|
|
- **Auth:** User
|
|
- **Query params:** `quiz_id` (int, required)
|
|
- **Response:** `AttemptResponse | null`
|
|
|
|
### POST `/api/attempts/progress`
|
|
|
|
Save in-progress quiz answers to Redis (survives logout/browser change).
|
|
|
|
- **Auth:** User
|
|
- **Headers:** `x-quiz-session` (optional, for device session lock)
|
|
- **Request body:**
|
|
```json
|
|
{
|
|
"quiz_id": "int",
|
|
"attempt_id": "int",
|
|
"answers": {"question_id": "answer"},
|
|
"current_idx": "int",
|
|
"mode": "string",
|
|
"voice": "string | null",
|
|
"time_left": "int | null",
|
|
"started_at": "string | null (ISO timestamp)",
|
|
"total_time": "int | null (seconds)"
|
|
}
|
|
```
|
|
- **Response:** `{"saved": true}`
|
|
- **Notes:** Progress expires after 7 days. Session lock (30s TTL) prevents concurrent resume on another device.
|
|
|
|
### GET `/api/attempts/progress`
|
|
|
|
Retrieve saved progress for a quiz. Auto-submits timed quizzes if timer expired.
|
|
|
|
- **Auth:** User
|
|
- **Headers:** `x-quiz-session` (optional, for device session lock)
|
|
- **Query params:** `quiz_id` (int, required)
|
|
- **Response:** Saved progress object or `null`
|
|
- **Errors:** 409 if another device is actively using this attempt
|
|
|
|
### DELETE `/api/attempts/progress/{attempt_id}`
|
|
|
|
Clear saved progress.
|
|
|
|
- **Auth:** User
|
|
- **Response:** 204 No Content
|
|
|
|
### DELETE `/api/attempts/{attempt_id}`
|
|
|
|
Delete an attempt, its answers, and associated reminder schedule.
|
|
|
|
- **Auth:** User (must own the attempt)
|
|
- **Response:** 204 No Content
|
|
|
|
---
|
|
|
|
## Tags (`/api/tags`)
|
|
|
|
### GET `/api/tags`
|
|
|
|
Get all tags grouped by type (subjects, diseases, keywords), with question counts.
|
|
|
|
- **Auth:** User
|
|
- **Response:**
|
|
```json
|
|
{
|
|
"subjects": [{"id": "int", "name": "string", "count": "int"}],
|
|
"diseases": [{"id": "int", "name": "string", "count": "int"}],
|
|
"keywords": [{"id": "int", "name": "string", "count": "int"}]
|
|
}
|
|
```
|
|
|
|
### POST `/api/tags/classify`
|
|
|
|
Trigger background AI classification of untagged questions.
|
|
|
|
- **Auth:** Moderator
|
|
- **Response:** `{"job_id": "uuid", "status": "started"}`
|
|
|
|
### GET `/api/tags/status/{job_id}`
|
|
|
|
Check classification job progress.
|
|
|
|
- **Auth:** User
|
|
- **Response:** `{"job_id": "string", "status": "string", "steps": [...], "error": "string | null"}`
|
|
|
|
---
|
|
|
|
## Teach (`/api/teach`)
|
|
|
|
### GET `/api/teach/models`
|
|
|
|
Return available teach AI models.
|
|
|
|
- **Auth:** User
|
|
- **Response:** `[{"id": int, "name": "string", "model_id": "string", "is_default": bool}]`
|
|
|
|
### POST `/api/teach/chat`
|
|
|
|
Send a message to the AI tutor with full question context.
|
|
|
|
- **Auth:** User
|
|
- **Rate limit:** 30 messages per user per 10 minutes (admins and unthrottled users exempt)
|
|
- **Request body:**
|
|
```json
|
|
{
|
|
"question_id": "int",
|
|
"messages": [
|
|
{"role": "user | assistant", "content": "string"}
|
|
],
|
|
"model_id": "int | null (AIModelConfig.id)"
|
|
}
|
|
```
|
|
- **Response:** `{"reply": "string (markdown)", "suggestions": ["string", "string", "string"]}`
|
|
- **Notes:** System prompt includes the question, correct answer, explanation, and up to 4 semantically similar questions for broader context. Follow-up suggestions are parsed from lines starting with `> `.
|
|
- **Errors:** 503 if no teach model configured, 502 if AI temporarily unavailable
|
|
|
|
---
|
|
|
|
## TTS (`/api/tts`)
|
|
|
|
### GET `/api/tts/voices`
|
|
|
|
Return available TTS voices. Excludes Polly voices if Polly is disabled.
|
|
|
|
- **Auth:** User
|
|
- **Response:** `[{"id": "string (model_id)", "name": "string", "is_default": bool}]`
|
|
|
|
### POST `/api/tts/speak`
|
|
|
|
Convert text to speech audio.
|
|
|
|
- **Auth:** User
|
|
- **Rate limit:** 60 requests per user per hour (admins and unthrottled users exempt)
|
|
- **Request body:**
|
|
```json
|
|
{
|
|
"text": "string (max 2000 chars)",
|
|
"voice": "string | null (model_id override)"
|
|
}
|
|
```
|
|
- **Response:** Binary `audio/mpeg` data
|
|
- **Notes:** Supports LiteLLM-routed local TTS voices, e.g. `local-kokoro-tts:am_adam`. The prefix before `:` is the LiteLLM model route; the suffix is the speaker voice sent to that route.
|
|
|
|
---
|
|
|
|
## Favorites (`/api/favorites`)
|
|
|
|
### POST `/api/favorites`
|
|
|
|
Add a question to the current user's favorites.
|
|
|
|
- **Auth:** User
|
|
- **Request body:** `{"question_id": "int"}`
|
|
- **Response (201):** `FavoriteResponse` — `{id, user_id, question_id, created_at}`
|
|
- **Notes:** Idempotent -- returns existing favorite if already favorited.
|
|
|
|
### DELETE `/api/favorites/{question_id}`
|
|
|
|
Remove a question from favorites.
|
|
|
|
- **Auth:** User
|
|
- **Response:** 204 No Content
|
|
|
|
### GET `/api/favorites`
|
|
|
|
Get list of favorited question IDs for the current user.
|
|
|
|
- **Auth:** User
|
|
- **Response:** `[int]`
|
|
|
|
---
|
|
|
|
## Categories (`/api/categories`)
|
|
|
|
Quiz-level categories for organizing quizzes.
|
|
|
|
### GET `/api/categories/`
|
|
|
|
List all quiz categories with quiz counts.
|
|
|
|
- **Auth:** User
|
|
- **Response:** `[{"id": int, "name": "string", "quiz_count": int}]`
|
|
|
|
### POST `/api/categories/`
|
|
|
|
Create a new quiz category.
|
|
|
|
- **Auth:** Moderator
|
|
- **Request body:** `{"name": "string"}`
|
|
- **Response:** `CategoryResponse`
|
|
|
|
### DELETE `/api/categories/{category_id}`
|
|
|
|
Delete a quiz category. Unassigns all quizzes in the category.
|
|
|
|
- **Auth:** Moderator
|
|
- **Response:** 204 No Content
|
|
|
|
### PATCH `/api/categories/quizzes/{quiz_id}`
|
|
|
|
Assign or remove a category from a quiz.
|
|
|
|
- **Auth:** Moderator
|
|
- **Query params:** `category_id` (int | null)
|
|
- **Response:** `{"quiz_id": int, "category_id": int | null}`
|
|
|
|
---
|
|
|
|
## Question Categories (`/api/question-categories`)
|
|
|
|
Question-level categories for organizing the question bank.
|
|
|
|
### GET `/api/question-categories/`
|
|
|
|
List all question categories with question counts.
|
|
|
|
- **Auth:** User
|
|
- **Response:** `[{"id": int, "name": "string", "description": "string | null", "question_count": int}]`
|
|
|
|
### POST `/api/question-categories/`
|
|
|
|
Create a new question category.
|
|
|
|
- **Auth:** Moderator
|
|
- **Request body:** `{"name": "string", "description": "string | null"}`
|
|
- **Response:** `QCatResponse`
|
|
|
|
### PATCH `/api/question-categories/{cat_id}`
|
|
|
|
Update a question category.
|
|
|
|
- **Auth:** Moderator
|
|
- **Request body:** `{"name": "string", "description": "string | null"}`
|
|
- **Response:** `QCatResponse`
|
|
|
|
### DELETE `/api/question-categories/{cat_id}`
|
|
|
|
Delete a question category. Optionally move its questions to another category.
|
|
|
|
- **Auth:** Moderator
|
|
- **Query params:** `move_to` (int | null) -- target category ID, or null to uncategorize
|
|
- **Response:** 204 No Content
|
|
|
|
### POST `/api/question-categories/{cat_id}/create-quiz`
|
|
|
|
Create a new quiz from all questions in a question category.
|
|
|
|
- **Auth:** Moderator
|
|
- **Query params:**
|
|
- `title` (string, required)
|
|
- `mode` (string: `"timed"` | `"learning"`, default `"timed"`)
|
|
- `time_limit_minutes` (int | null)
|
|
- **Response:** `{"id": int, "title": "string", "questions_count": int}`
|
|
|
|
---
|
|
|
|
## Contact (`/api/contact`)
|
|
|
|
### POST `/api/contact`
|
|
|
|
Submit a contact form (public, no auth).
|
|
|
|
- **Auth:** None
|
|
- **Request body:**
|
|
```json
|
|
{
|
|
"name": "string (max 120)",
|
|
"email": "valid email",
|
|
"type": "question | moderator",
|
|
"message": "string (max 2000)",
|
|
"turnstile_token": "string | null"
|
|
}
|
|
```
|
|
- **Response:** `{"success": true}`
|
|
- **Notes:** Stores submission in DB and emails admin. Turnstile verification if configured.
|
|
|
|
### GET `/api/contact/submissions`
|
|
|
|
List all contact submissions.
|
|
|
|
- **Auth:** Admin (enforced in endpoint body, not decorator)
|
|
- **Response:** `[{id, name, email, type, message, read, created_at}]`
|
|
|
|
### PUT `/api/contact/submissions/{submission_id}/read`
|
|
|
|
Mark a contact submission as read.
|
|
|
|
- **Auth:** Admin (enforced in endpoint body, not decorator)
|
|
- **Response:** `{"success": true}`
|
|
|
|
---
|
|
|
|
## Admin (`/api/admin`)
|
|
|
|
### User Management
|
|
|
|
#### GET `/api/admin/users`
|
|
|
|
List all users.
|
|
|
|
- **Auth:** Admin
|
|
- **Response:** `UserResponse[]`
|
|
|
|
#### POST `/api/admin/users`
|
|
|
|
Create a user directly (email auto-verified).
|
|
|
|
- **Auth:** Admin
|
|
- **Request body:** `{"email": "string", "password": "string", "name": "string"}`
|
|
- **Response:** `UserResponse`
|
|
|
|
#### PUT `/api/admin/users/{user_id}/role`
|
|
|
|
Change a user's role.
|
|
|
|
- **Auth:** Admin
|
|
- **Request body:** `{"role": "admin | moderator | user"}`
|
|
- **Response:** `UserResponse`
|
|
- **Errors:** 400 cannot change own role
|
|
|
|
#### PUT `/api/admin/users/{user_id}/unthrottle`
|
|
|
|
Set or clear the unthrottle flag (exempt from AI/TTS rate limits).
|
|
|
|
- **Auth:** Admin
|
|
- **Request body:** `{"unthrottled": true | false}`
|
|
- **Response:** `UserResponse`
|
|
|
|
#### DELETE `/api/admin/users/{user_id}`
|
|
|
|
Delete a user and all their data.
|
|
|
|
- **Auth:** Admin
|
|
- **Response:** 204 No Content
|
|
- **Errors:** 400 cannot delete yourself
|
|
|
|
### AI Model Configuration
|
|
|
|
#### GET `/api/admin/models/available`
|
|
|
|
List active models for a task (for users when creating/taking quizzes).
|
|
|
|
- **Auth:** User
|
|
- **Query params:** `task` (string, default `"extraction"`)
|
|
- **Response:** `[{"id": int | null, "name": "string", "model_id": "string", "is_default": bool}]`
|
|
- **Notes:** Falls back to env `LITELLM_MODEL` if nothing configured.
|
|
|
|
#### GET `/api/admin/models`
|
|
|
|
List all AI model configurations.
|
|
|
|
- **Auth:** Admin
|
|
- **Response:** `AIModelConfigResponse[]` -- `{id, name, model_id, task, is_active, is_default, api_key (masked)}`
|
|
|
|
#### POST `/api/admin/models`
|
|
|
|
Create an AI model configuration.
|
|
|
|
- **Auth:** Admin
|
|
- **Request body:**
|
|
```json
|
|
{
|
|
"name": "string",
|
|
"model_id": "string",
|
|
"task": "extraction | tts | teach | keyword",
|
|
"is_active": "bool",
|
|
"is_default": "bool",
|
|
"api_key": "string | null"
|
|
}
|
|
```
|
|
- **Response:** `AIModelConfigResponse`
|
|
- **Notes:** Auto-sets as default if first model for that task. Unique constraint on (model_id, task).
|
|
|
|
#### PUT `/api/admin/models/{model_id}`
|
|
|
|
Update an AI model configuration.
|
|
|
|
- **Auth:** Admin
|
|
- **Request body:** Partial `AIModelConfigUpdate`
|
|
- **Response:** `AIModelConfigResponse`
|
|
|
|
#### DELETE `/api/admin/models/{model_id}`
|
|
|
|
Delete an AI model configuration.
|
|
|
|
- **Auth:** Admin
|
|
- **Response:** 204 No Content
|
|
|
|
#### POST `/api/admin/models/{model_id}/test`
|
|
|
|
Send a test completion to verify an LLM model is reachable.
|
|
|
|
- **Auth:** Admin
|
|
- **Response:** `{"message": "model_id -> 'OK'"}`
|
|
- **Errors:** 400 for TTS models (use Preview button instead), 502 if unreachable
|
|
|
|
#### POST `/api/admin/litellm/models`
|
|
|
|
Query available models from LiteLLM proxy or OpenAI-compatible API.
|
|
|
|
- **Auth:** Admin
|
|
- **Request body:** `{"api_key": "string | null", "api_base": "string | null"}`
|
|
- **Response:** `{"models": ["string"], "source": "string"}`
|
|
|
|
### TTS Voice Search
|
|
|
|
#### POST `/api/admin/tts/voices`
|
|
|
|
Discover available TTS voices from a provider.
|
|
|
|
- **Auth:** Admin
|
|
- **Request body:**
|
|
```json
|
|
{
|
|
"provider": "elevenlabs | polly | openai",
|
|
"api_key": "string | null",
|
|
"region": "string | null"
|
|
}
|
|
```
|
|
- **Response:** `{"voices": [{"model_id": "string", "name": "string", "labels": {}}]}`
|
|
- **Notes:** OpenAI returns hardcoded list of tts-1 and tts-1-hd voices.
|
|
|
|
### System Settings
|
|
|
|
#### GET `/api/admin/settings`
|
|
|
|
Get system settings (registration enabled, embedding model, Polly enabled).
|
|
|
|
- **Auth:** Admin
|
|
- **Response:** `{"registration_enabled": bool, "embedding_model": "string"}`
|
|
|
|
#### PUT `/api/admin/settings`
|
|
|
|
Update system settings.
|
|
|
|
- **Auth:** Admin
|
|
- **Request body:** `{"registration_enabled": bool, "embedding_model": "string"}`
|
|
- **Response:** `{"success": true, "message": "Settings updated"}`
|
|
|
|
### Embedding Management
|
|
|
|
#### POST `/api/admin/embedding/test`
|
|
|
|
Test the currently configured embedding model.
|
|
|
|
- **Auth:** Admin
|
|
- **Response:** `{"model": "string", "dimensions": int, "status": "ok"}`
|
|
|
|
#### POST `/api/admin/embedding/regenerate`
|
|
|
|
Queue a Celery task to regenerate all question embeddings.
|
|
|
|
- **Auth:** Admin
|
|
- **Response:** `{"job_id": "uuid", "message": "Regeneration started..."}`
|
|
|
|
---
|
|
|
|
## Nextcloud (`/api/nextcloud`)
|
|
|
|
WebDAV proxy for importing PDFs from Nextcloud (avoids browser CORS issues).
|
|
|
|
### POST `/api/nextcloud/test`
|
|
|
|
Test Nextcloud credentials.
|
|
|
|
- **Auth:** Moderator
|
|
- **Request body:** `{"server": "string", "username": "string", "password": "string", "path": "/"}`
|
|
- **Response:** `{"ok": true, "message": "Connected successfully"}`
|
|
|
|
### POST `/api/nextcloud/files`
|
|
|
|
List PDFs and folders at a given Nextcloud path.
|
|
|
|
- **Auth:** Moderator
|
|
- **Request body:** `{"server": "string", "username": "string", "password": "string", "path": "/"}`
|
|
- **Response:** `{"path": "string", "items": [{"name", "path", "type": "dir | pdf", "size"}]}`
|
|
|
|
### POST `/api/nextcloud/download`
|
|
|
|
Download a PDF from Nextcloud and stream it back.
|
|
|
|
- **Auth:** Moderator
|
|
- **Request body:** `{"server": "string", "username": "string", "password": "string", "path": "/some/file.pdf"}`
|
|
- **Response:** Streamed `application/pdf` binary
|
|
- **Errors:** 400 if not a PDF file
|