Skip to content

Latest commit

 

History

History
101 lines (72 loc) · 5.45 KB

File metadata and controls

101 lines (72 loc) · 5.45 KB

AnkiShare REST API

Status: alpha. The API is stable in shape but may change before 1.0.

Base URL: same origin as the web app (/api/* is proxied to the API service by the reverse proxy in production). All responses are JSON with the error shape { "error": "<message>", "code": "<MACHINE_CODE>" }.

Authentication model

Two httpOnly; secure; sameSite=lax cookies (sent automatically by browsers):

Cookie Set by Lifetime Purpose
anki_site POST /api/site/unlock 30 days (sliding) Site-wide access gate (HMAC of SITE_PASSWORD)
anki_session /api/auth/login, /api/auth/register, /api/admin/login JWT_EXPIRY (default 7d) User or admin JWT

The add-on upload endpoint uses a header API key instead of cookies.

Caller How
Anonymous nothing
Visitor anki_site cookie
Registered user anki_site + anki_session (user JWT)
Admin anki_session (admin JWT) — bypasses the site gate
Add-on X-API-Key: ask_… header (no cookies)

Auth matrix (selected)

Endpoint anon site user admin api-key
GET /api/health 200 200 200 200
GET /api/decks/:slug 401 200 200 200
GET /api/decks/:slug/review/next 401 401 200 200
POST /api/upload 401 401 401 401 201
DELETE /api/admin/decks/:slug 401 403 403 204

The full matrix is enforced by tests in server/test/routes/authMatrix.test.ts.

Health

GET /api/health200 { "status": "ok" } (checks DB). No auth.

Site access

  • POST /api/site/unlock { "password": "…" }200 { "unlocked": true } (sets anki_site). 401 WRONG_SITE_PASSWORD. Rate-limited 5/min/IP.
  • GET /api/site/status200 { "unlocked": true|false }.

User auth (require site access)

  • POST /api/auth/register { "username", "password" }201 { "user": { id, username, isAdmin:false } }. Username: /^[a-zA-Z0-9_-]{3,32}$/, password ≥6 chars. Rate-limited 5/min/IP.
  • POST /api/auth/login { "username", "password" }200 { "user": … }. Rate-limited 10/min/IP.
  • POST /api/auth/logout200 { "ok": true } (clears session cookie).
  • GET /api/auth/me200 { "user": { id, username, isAdmin } }.
  • DELETE /api/auth/me204 (deletes account + all review progress/log).

Admin (require admin JWT)

  • POST /api/admin/login { "username", "password" }200 { "user": { id:0, username, isAdmin:true } }. Env-var credentials. Rate-limited 5/min/IP.
  • GET /api/admin/decks?page=1&limit=20{ items:[AdminDeck], total, page, limit }.
  • POST /api/admin/decks (multipart) file + name, optional slug?, description?, expires_at?201 { slug, deckId, cardCount, noteCount, mediaCount }. If slug exists → re-upload (update) flow preserving user progress.
  • PUT /api/admin/decks/:slug { name?, description?, allow_download?, allow_review?, expires_at? }200 { slug }.
  • DELETE /api/admin/decks/:slug204 (cascades notes/cards/media/progress; deletes files).
  • GET /api/admin/users?page=1&limit=20{ items:[User], total, page, limit }.
  • DELETE /api/admin/users/:id204.
  • POST /api/admin/api-keys { "label" }201 { id, label, key_prefix, key } (key shown once).
  • GET /api/admin/api-keys{ items:[ApiKey] } (prefix + status only).
  • DELETE /api/admin/api-keys/:id204 (sets revoked_at).

Decks (require site access)

  • GET /api/decks?page=1&limit=20{ items:[{id,slug,name,description,card_count,uploaded_at}], total, page, limit } (excludes expired).
  • GET /api/decks/:slug{ id, slug, name, description, card_count, allow_download, allow_review }.
  • GET /api/decks/:slug/cards?page=1&limit=20{ items:[{id, anki_card_id, card_type, front_html, back_html, css}], total, page, limit }.
  • GET /api/decks/:slug/download → binary .apkg (application/vnd.anki.apkg). 403 DOWNLOAD_DISABLED if disabled.
  • GET /api/decks/:slug/media/:filename → media binary. 404 for traversal/unknown names.

Upload (add-on) — API key only

POST /api/upload, header X-API-Key: ask_…, multipart file + name, optional slug?, description?201 { slug, deckId, cardCount, noteCount, mediaCount }. 401 bad/revoked key, 400 BAD_APKG / APKG_NOT_ZIP, 413 too large. Rate-limited 20/hour/IP.

Review (require site access + user/admin)

All responses set Cache-Control: no-store. Rate-limited 120/min/user.

  • GET /api/decks/:slug/review/next{ card: {id,card_type,front_html,back_html,css} | null, progress: {status,due_at,isNew} | null, reason?: "no_cards"|"daily_limit" }.
  • POST /api/decks/:slug/review { card_id, rating: "again"|"hard"|"good"|"easy", time_ms? } → next step (same shape as /next).
  • GET /api/decks/:slug/review/stats{ total, new, learning, review, due_now, reviewed_today }.
  • POST /api/decks/:slug/review/reset200 { ok: true }.

SRS summary

States: new → learning → review. Learning steps [1min, 10min]; graduate on "Good" at the last step (graduating interval 1 day). "Again" lapses a review card back to learning. Ease starts at 2500 (‰), floor 1300. 20 new cards per user per deck per UTC day. See PLAN.md § "Simplified Spaced Repetition" for the full transition table.

Pagination

All list endpoints accept ?page=1&limit=20 (limit clamped to 1–100).