v3: the docs follow deno doc's grain — the index page was a hand-kept list of some 250 links into deno doc's output layout (./api/main.ts/index.html), policed by mod.test.ts; deno doc has no module index of its own beyond all_symbols.html, which lists every module by path with its summary. mod.ts is now a prose overview of the three parts and their layers, linking that list once, and mod.test.ts is gone. Re-exporting everything from mod.ts, as deno doc's tutorial does for a library, was tried and doesn't fit: export * drops the 18 routers (default exports) and makes 17 names ambiguous, and deno 2.9 renders export * as namespaces with the root's module doc and nested ones empty, losing every module doc. A router's page showed one line: its endpoint list sat on the default symbol, whose summary was cut at the colon introducing it; the list is now the module doc of all 18 routers, and the router's JSDoc says so. 22 summaries ended in a colon (the routers, BandcampPlayer, migration 0016, useDumpListSync, usePlaylistListSync) and read cut short wherever deno doc shows a first paragraph alone; they end in a full stop, doc-lint.ts now fails on a colon summary, and the README states the rule. No behaviour changes; 204 backend tests pass.
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 50s
Build and Deploy Static Documentation Site / build-and-push (push) Successful in 21s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-09-28 19:28:24 +00:00
parent 563daa720c
commit 42e0356d21
27 changed files with 330 additions and 617 deletions

View File

@@ -1,6 +1,11 @@
/**
* The attachment routes, for images pasted into Markdown bodies.
*
* - `POST /api/attachments` — signed in. Stores an image (multipart `file`, at
* most 5 MB) and answers its id; it is bound to a resource once a saved body
* links it.
* - `GET /api/attachments/:attachmentId` — public. Serves the image.
*
* @module
*/
@@ -19,14 +24,7 @@ import {
validateImageUpload,
} from "../lib/upload.ts";
/**
* The attachment routes, for images pasted into Markdown bodies:
*
* - `POST /api/attachments` — signed in. Stores an image (multipart `file`, at
* most 5 MB) and answers its id; it is bound to a resource once a saved body
* links it.
* - `GET /api/attachments/:attachmentId` — public. Serves the image.
*/
/** The router serving the endpoints the module doc lists. */
const router: Router = new Router();
router.post("/api/attachments", authMiddleware, async (ctx) => {

View File

@@ -1,6 +1,11 @@
/**
* The avatar routes.
*
* - `POST /api/avatars/me` — signed in. Replaces the caller's avatar (multipart
* `file`, at most 5 MB), announces it, and answers the caller's user.
* - `GET /api/avatars/:userId` — public. Serves a user's avatar; 404 without
* one.
*
* @module
*/
@@ -21,14 +26,7 @@ import {
validateImageUpload,
} from "../lib/upload.ts";
/**
* The avatar routes:
*
* - `POST /api/avatars/me` — signed in. Replaces the caller's avatar (multipart
* `file`, at most 5 MB), announces it, and answers the caller's user.
* - `GET /api/avatars/:userId` — public. Serves a user's avatar; 404 without
* one.
*/
/** The router serving the endpoints the module doc lists. */
const router: Router = new Router();
router.post("/api/avatars/me", authMiddleware, async (ctx) => {

View File

@@ -1,5 +1,8 @@
/**
* The Bandcamp route.
* The Bandcamp route, for the native player.
*
* - `GET /api/bandcamp/tracks?url=&force=` — public. Resolves a Bandcamp page
* to its streamable tracks, at play time; `force=1` bypasses the cache.
*
* @module
*/
@@ -9,12 +12,7 @@ import type { APIResponse } from "../../shared/api.ts";
import { resolveBandcamp } from "../remote/bandcamp-streams.ts";
import type { Tralbum } from "../../shared/player.ts";
/**
* The Bandcamp route, for the native player:
*
* - `GET /api/bandcamp/tracks?url=&force=` — public. Resolves a Bandcamp page
* to its streamable tracks, at play time; `force=1` bypasses the cache.
*/
/** The router serving the endpoints the module doc lists. */
const bandcampRouter: Router = new Router();
// Public: it only reads a public page and returns URLs the same page already

View File

@@ -1,6 +1,16 @@
/**
* The category routes.
*
* - `GET /api/categories` — public. Answers every category, in order.
* - `GET /api/categories/:slug` — public. Answers one category.
* - `GET /api/categories/:slug/dumps?page=&limit=` — optional sign-in. Answers
* a page of its dumps, newest first, with the caller's private ones.
* - `POST /api/categories` — permission `category:manage`. Creates a category.
* - `PUT /api/categories/:id` — permission `category:manage`. Updates a
* category.
* - `DELETE /api/categories/:id` — permission `category:manage`. Deletes a
* category; its dumps stay.
*
* @module
*/
@@ -31,19 +41,7 @@ import {
updateCategory,
} from "../services/category-service.ts";
/**
* The category routes:
*
* - `GET /api/categories` — public. Answers every category, in order.
* - `GET /api/categories/:slug` — public. Answers one category.
* - `GET /api/categories/:slug/dumps?page=&limit=` — optional sign-in. Answers
* a page of its dumps, newest first, with the caller's private ones.
* - `POST /api/categories` — permission `category:manage`. Creates a category.
* - `PUT /api/categories/:id` — permission `category:manage`. Updates a
* category.
* - `DELETE /api/categories/:id` — permission `category:manage`. Deletes a
* category; its dumps stay.
*/
/** The router serving the endpoints the module doc lists. */
const router: Router = new Router({ prefix: "/api/categories" });
router.get("/", (ctx) => {

View File

@@ -1,5 +1,13 @@
/**
* The chat history route.
* The chat history route; messages are posted and edited over the socket (see
* routes/ws.ts).
*
* - `GET /api/chat?limit=&before=&beforeId=&after=&afterId=` — signed in.
* Answers messages in ascending order: the most recent by default; with
* `before`, the page of older messages preceding that cursor ("load older");
* with `after`, the messages following it (resync after a reconnect). A
* cursor is an ISO `created_at` plus the message id to break ties; without
* `afterId` the `after` timestamp itself is included.
*
* @module
*/
@@ -13,17 +21,7 @@ import {
getRecentChatMessages,
} from "../services/chat-service.ts";
/**
* The chat history route; messages are posted and edited over the socket (see
* routes/ws.ts):
*
* - `GET /api/chat?limit=&before=&beforeId=&after=&afterId=` — signed in.
* Answers messages in ascending order: the most recent by default; with
* `before`, the page of older messages preceding that cursor ("load older");
* with `after`, the messages following it (resync after a reconnect). A
* cursor is an ISO `created_at` plus the message id to break ties; without
* `afterId` the `after` timestamp itself is included.
*/
/** The router serving the endpoints the module doc lists. */
const router: Router = new Router({ prefix: "/api/chat" });
router.get("/", authMiddleware, (ctx) => {

View File

@@ -1,5 +1,17 @@
/**
* The comment routes.
* The comment routes. A private dump's comments exist only for its poster, and
* changes to them aren't broadcast.
*
* - `GET /api/dumps/:dumpId/comments` — optional sign-in. Answers every comment
* on a dump, oldest first.
* - `POST /api/dumps/:dumpId/comments` — signed in. Posts a comment, replying
* to `parentId` if given.
* - `PATCH /api/comments/:commentId` — signed in, as its author or with
* `comment:moderate`. Edits a comment.
* - `GET /api/comments/:commentId/likers?page=&limit=` — optional sign-in.
* Answers a page of the users who like a comment.
* - `DELETE /api/comments/:commentId` — signed in, as its author or with
* `comment:moderate`. Deletes a comment, leaving its place in the thread.
*
* @module
*/
@@ -37,21 +49,7 @@ import {
broadcastCommentUpdated,
} from "../services/ws-service.ts";
/**
* The comment routes. A private dump's comments exist only for its poster, and
* changes to them aren't broadcast:
*
* - `GET /api/dumps/:dumpId/comments` — optional sign-in. Answers every comment
* on a dump, oldest first.
* - `POST /api/dumps/:dumpId/comments` — signed in. Posts a comment, replying
* to `parentId` if given.
* - `PATCH /api/comments/:commentId` — signed in, as its author or with
* `comment:moderate`. Edits a comment.
* - `GET /api/comments/:commentId/likers?page=&limit=` — optional sign-in.
* Answers a page of the users who like a comment.
* - `DELETE /api/comments/:commentId` — signed in, as its author or with
* `comment:moderate`. Deletes a comment, leaving its place in the thread.
*/
/** The router serving the endpoints the module doc lists. */
const router: Router = new Router({ prefix: "/api" });
router.get("/dumps/:dumpId/comments", async (ctx) => {

View File

@@ -1,5 +1,34 @@
/**
* The dump routes.
* The dump routes. `:dumpId` takes an id or a slug; a private dump exists only
* for its poster.
*
* - `POST /api/dumps` — signed in. Posts a link (JSON body) or a file
* (multipart `file`, with `title`, `comment`, `isPrivate` and `categoryIds`).
* - `GET /api/dumps/by-url?url=` — optional sign-in. Answers the dumps already
* pointing at the same thing as `url`, for the create form's duplicate hint.
* - `GET /api/dumps/:dumpId` — optional sign-in. Answers a dump.
* - `GET /api/dumps/:dumpId/voters?page=&limit=` — optional sign-in. Answers a
* page of the users who upvoted it.
* - `GET /api/dumps/:dumpId/related` — optional sign-in. Answers the dumps
* linked to or from it.
* - `GET /api/dumps/:dumpId/playlists` — optional sign-in. Answers the
* playlists it appears in.
* - `GET /api/dumps?page=&limit=` — optional sign-in. Answers a page of the
* dumps, newest first.
* - `PUT /api/dumps/:dumpId/file` — signed in, as its poster or with
* `dump:moderate`. Replaces a file dump's upload (multipart).
* - `PUT /api/dumps/:dumpId` — signed in, as its poster or with
* `dump:moderate`. Updates its title, comment, visibility, categories, and a
* link's URL.
* - `PUT /api/dumps/:dumpId/thumbnail` — signed in, as its poster or with
* `dump:moderate`. Replaces its custom thumbnail (multipart `file`, at most 5
* MB).
* - `DELETE /api/dumps/:dumpId/thumbnail` — signed in, as its poster or with
* `dump:moderate`. Removes its custom thumbnail.
* - `POST /api/dumps/:dumpId/refresh-metadata` — signed in, as its poster or
* with `dump:moderate`. Fetches a link's preview again.
* - `DELETE /api/dumps/:dumpId` — signed in, as its poster or with
* `dump:moderate`. Deletes it.
*
* @module
*/
@@ -44,38 +73,7 @@ import { getDumpVoters } from "../services/vote-service.ts";
import { getRelatedDumps } from "../services/backlink-service.ts";
import { getPlaylistsForDump } from "../services/playlist-service.ts";
/**
* The dump routes. `:dumpId` takes an id or a slug; a private dump exists only
* for its poster:
*
* - `POST /api/dumps` — signed in. Posts a link (JSON body) or a file
* (multipart `file`, with `title`, `comment`, `isPrivate` and `categoryIds`).
* - `GET /api/dumps/by-url?url=` — optional sign-in. Answers the dumps already
* pointing at the same thing as `url`, for the create form's duplicate hint.
* - `GET /api/dumps/:dumpId` — optional sign-in. Answers a dump.
* - `GET /api/dumps/:dumpId/voters?page=&limit=` — optional sign-in. Answers a
* page of the users who upvoted it.
* - `GET /api/dumps/:dumpId/related` — optional sign-in. Answers the dumps
* linked to or from it.
* - `GET /api/dumps/:dumpId/playlists` — optional sign-in. Answers the
* playlists it appears in.
* - `GET /api/dumps?page=&limit=` — optional sign-in. Answers a page of the
* dumps, newest first.
* - `PUT /api/dumps/:dumpId/file` — signed in, as its poster or with
* `dump:moderate`. Replaces a file dump's upload (multipart).
* - `PUT /api/dumps/:dumpId` — signed in, as its poster or with
* `dump:moderate`. Updates its title, comment, visibility, categories, and a
* link's URL.
* - `PUT /api/dumps/:dumpId/thumbnail` — signed in, as its poster or with
* `dump:moderate`. Replaces its custom thumbnail (multipart `file`, at most 5
* MB).
* - `DELETE /api/dumps/:dumpId/thumbnail` — signed in, as its poster or with
* `dump:moderate`. Removes its custom thumbnail.
* - `POST /api/dumps/:dumpId/refresh-metadata` — signed in, as its poster or
* with `dump:moderate`. Fetches a link's preview again.
* - `DELETE /api/dumps/:dumpId` — signed in, as its poster or with
* `dump:moderate`. Deletes it.
*/
/** The router serving the endpoints the module doc lists. */
const router: Router = new Router({ prefix: "/api/dumps" });
// Every route here takes a slug or an id; the services below them key on the

View File

@@ -1,6 +1,10 @@
/**
* The route serving file dumps' uploads.
*
* - `GET /api/files/:dumpId?token=` — public; a private dump's file needs its
* poster's media `token`. Serves the file, inline when that is safe and as a
* download otherwise, with byte ranges.
*
* @module
*/
@@ -12,13 +16,7 @@ import { authenticateMedia } from "../lib/auth.ts";
import { serveFile } from "../lib/serve-file.ts";
import { dumpFileVersion, getDump } from "../services/dump-service.ts";
/**
* The route serving file dumps' uploads:
*
* - `GET /api/files/:dumpId?token=` — public; a private dump's file needs its
* poster's media `token`. Serves the file, inline when that is safe and as a
* download otherwise, with byte ranges.
*/
/** The router serving the endpoints the module doc lists. */
const router: Router = new Router({ prefix: "/api/files" });
/**

View File

@@ -1,5 +1,20 @@
/**
* The follow routes.
* The follow routes, all for what the caller follows.
*
* - `GET /api/follows/status` — signed in. Answers the ids of the users and
* playlists they follow.
* - `GET /api/follows/feed/users?page=&limit=&category=` — signed in. Answers a
* page of the public dumps of the users they follow, newest first, optionally
* in one category.
* - `GET /api/follows/feed/playlists?page=&limit=&category=` — signed in.
* Answers a page of the public dumps in the public playlists they follow,
* most recently added first, optionally in one category.
* - `POST /api/follows/users/:userId` — signed in. Follows a user.
* - `DELETE /api/follows/users/:userId` — signed in. Stops following a user.
* - `POST /api/follows/playlists/:playlistId` — signed in. Follows a public
* playlist.
* - `DELETE /api/follows/playlists/:playlistId` — signed in. Stops following a
* playlist.
*
* @module
*/
@@ -38,24 +53,7 @@ function resolveCategoryFilter(
/** The feed a filter naming no category yields. */
const EMPTY_FEED: Page<Dump> = { items: [], hasMore: false };
/**
* The follow routes, all for what the caller follows:
*
* - `GET /api/follows/status` — signed in. Answers the ids of the users and
* playlists they follow.
* - `GET /api/follows/feed/users?page=&limit=&category=` — signed in. Answers a
* page of the public dumps of the users they follow, newest first, optionally
* in one category.
* - `GET /api/follows/feed/playlists?page=&limit=&category=` — signed in.
* Answers a page of the public dumps in the public playlists they follow,
* most recently added first, optionally in one category.
* - `POST /api/follows/users/:userId` — signed in. Follows a user.
* - `DELETE /api/follows/users/:userId` — signed in. Stops following a user.
* - `POST /api/follows/playlists/:playlistId` — signed in. Follows a public
* playlist.
* - `DELETE /api/follows/playlists/:playlistId` — signed in. Stops following a
* playlist.
*/
/** The router serving the endpoints the module doc lists. */
const router: Router = new Router({ prefix: "/api/follows" });
// Static routes first to prevent Oak matching "status"/"feed" as a :param

View File

@@ -1,6 +1,10 @@
/**
* The invite routes.
*
* - `POST /api/invites` — signed in. Makes an invite and answers its token.
* - `GET /api/invites/:token` — public. Checks an invite is valid, before the
* register page shows its form; 404 when it isn't.
*
* @module
*/
@@ -11,13 +15,7 @@ import type { InviteResponse } from "../../shared/user.ts";
import { type AuthContext, authMiddleware } from "../middleware/auth.ts";
import { createInvite, validateInvite } from "../services/invite-service.ts";
/**
* The invite routes:
*
* - `POST /api/invites` — signed in. Makes an invite and answers its token.
* - `GET /api/invites/:token` — public. Checks an invite is valid, before the
* register page shows its form; 404 when it isn't.
*/
/** The router serving the endpoints the module doc lists. */
const router: Router = new Router({ prefix: "/api/invites" });
router.post("/", authMiddleware, (ctx: AuthContext) => {

View File

@@ -1,5 +1,10 @@
/**
* The notification routes.
* The notification routes, all for the caller's own notifications.
*
* - `GET /api/notifications?page=&limit=` — signed in. Answers a page of them,
* newest first.
* - `POST /api/notifications/read-all` — signed in. Marks them all read.
* - `PATCH /api/notifications/:id/read` — signed in. Marks one read.
*
* @module
*/
@@ -20,14 +25,7 @@ import {
markOneRead,
} from "../services/notification-service.ts";
/**
* The notification routes, all for the caller's own notifications:
*
* - `GET /api/notifications?page=&limit=` — signed in. Answers a page of them,
* newest first.
* - `POST /api/notifications/read-all` — signed in. Marks them all read.
* - `PATCH /api/notifications/:id/read` — signed in. Marks one read.
*/
/** The router serving the endpoints the module doc lists. */
const router: Router = new Router({ prefix: "/api/notifications" });
router.get("/", authMiddleware, (ctx: AuthContext) => {

View File

@@ -1,5 +1,26 @@
/**
* The playlist routes.
* The playlist routes. `:playlistId` takes an id or a slug.
*
* - `GET /api/playlists/by-dump/:dumpId/memberships` — signed in. Answers each
* of the caller's playlists and whether it holds the dump.
* - `POST /api/playlists` — signed in. Creates a playlist.
* - `GET /api/playlists/:playlistId` — optional sign-in. Answers a playlist
* with the dumps the caller may see in it; a private one only to its owner.
* - `PUT /api/playlists/:playlistId` — signed in, as its owner or with
* `playlist:moderate`. Updates its title, description and visibility.
* - `DELETE /api/playlists/:playlistId` — signed in, as its owner or with
* `playlist:moderate`. Deletes it.
* - `POST /api/playlists/:playlistId/dumps/:dumpId` — signed in, as its owner
* or with `playlist:moderate`. Adds a dump at the top.
* - `DELETE /api/playlists/:playlistId/dumps/:dumpId` — signed in, as its owner
* or with `playlist:moderate`. Removes a dump.
* - `POST /api/playlists/:playlistId/image` — signed in, as its owner or with
* `playlist:moderate`. Replaces its cover (multipart `file`, at most 5 MB).
* - `GET /api/playlists/:playlistId/image?token=` — public; a private
* playlist's cover needs its owner's media `token`. Serves its cover; 404
* without one.
* - `PUT /api/playlists/:playlistId/order` — signed in, as its owner or with
* `playlist:moderate`. Reorders the dumps the caller can see in it.
*
* @module
*/
@@ -39,30 +60,7 @@ import {
validateImageUpload,
} from "../lib/upload.ts";
/**
* The playlist routes. `:playlistId` takes an id or a slug:
*
* - `GET /api/playlists/by-dump/:dumpId/memberships` — signed in. Answers each
* of the caller's playlists and whether it holds the dump.
* - `POST /api/playlists` — signed in. Creates a playlist.
* - `GET /api/playlists/:playlistId` — optional sign-in. Answers a playlist
* with the dumps the caller may see in it; a private one only to its owner.
* - `PUT /api/playlists/:playlistId` — signed in, as its owner or with
* `playlist:moderate`. Updates its title, description and visibility.
* - `DELETE /api/playlists/:playlistId` — signed in, as its owner or with
* `playlist:moderate`. Deletes it.
* - `POST /api/playlists/:playlistId/dumps/:dumpId` — signed in, as its owner
* or with `playlist:moderate`. Adds a dump at the top.
* - `DELETE /api/playlists/:playlistId/dumps/:dumpId` — signed in, as its owner
* or with `playlist:moderate`. Removes a dump.
* - `POST /api/playlists/:playlistId/image` — signed in, as its owner or with
* `playlist:moderate`. Replaces its cover (multipart `file`, at most 5 MB).
* - `GET /api/playlists/:playlistId/image?token=` — public; a private
* playlist's cover needs its owner's media `token`. Serves its cover; 404
* without one.
* - `PUT /api/playlists/:playlistId/order` — signed in, as its owner or with
* `playlist:moderate`. Reorders the dumps the caller can see in it.
*/
/** The router serving the endpoints the module doc lists. */
const router: Router<AuthState> = new Router<AuthState>({
prefix: "/api/playlists",
});

View File

@@ -1,6 +1,11 @@
/**
* The link preview routes.
*
* - `GET /api/preview?url=` — signed in. Answers the preview of a link, and
* whether its page could be reached.
* - `GET /api/proxy-image?url=` — public. Serves a remote raster image through
* this site; 400 when it isn't one, 502 when its host can't be reached.
*
* @module
*/
@@ -16,14 +21,7 @@ import {
import type { LinkPreview } from "../../shared/dump.ts";
import { authMiddleware } from "../middleware/auth.ts";
/**
* The link preview routes:
*
* - `GET /api/preview?url=` — signed in. Answers the preview of a link, and
* whether its page could be reached.
* - `GET /api/proxy-image?url=` — public. Serves a remote raster image through
* this site; 400 when it isn't one, 502 when its host can't be reached.
*/
/** The router serving the endpoints the module doc lists. */
const previewRouter: Router = new Router();
// Signed-in only: it's the create form's preview, and an open one lets anyone

View File

@@ -1,6 +1,10 @@
/**
* The search route.
*
* - `GET /api/search?q=&page=&limit=` — optional sign-in. Answers a page of the
* matching dumps with their total, and up to 20 matching users and playlists;
* private dumps and playlists only the caller's own.
*
* @module
*/
@@ -13,13 +17,7 @@ import { searchPlaylists } from "../services/playlist-service.ts";
import type { APIResponse } from "../../shared/api.ts";
import type { SearchResults } from "../../shared/search.ts";
/**
* The search route:
*
* - `GET /api/search?q=&page=&limit=` — optional sign-in. Answers a page of the
* matching dumps with their total, and up to 20 matching users and playlists;
* private dumps and playlists only the caller's own.
*/
/** The router serving the endpoints the module doc lists. */
const router: Router = new Router({ prefix: "/api/search" });
router.get("/", async (ctx) => {

View File

@@ -1,6 +1,12 @@
/**
* The route serving dumps' thumbnails, generated with ffmpeg where needed.
*
* - `GET /api/thumbnails/:dumpId?token=&v=` — public; a private dump's
* thumbnail needs its poster's media `token`. Serves the dump's custom
* thumbnail, else one made from its upload: a still of a video, or a
* scaled-down copy of an image (the image itself when none can be made); 404
* when there is neither.
*
* @module
*/
@@ -17,15 +23,7 @@ import { serveUploadedFile } from "../lib/upload.ts";
import { DUMPS_DIR, THUMBNAILS_DIR } from "../config.ts";
import { authenticateMedia } from "../lib/auth.ts";
/**
* The route serving dumps' thumbnails:
*
* - `GET /api/thumbnails/:dumpId?token=&v=` — public; a private dump's
* thumbnail needs its poster's media `token`. Serves the dump's custom
* thumbnail, else one made from its upload: a still of a video, or a
* scaled-down copy of an image (the image itself when none can be made); 404
* when there is neither.
*/
/** The router serving the endpoints the module doc lists. */
const router: Router = new Router({ prefix: "/api/thumbnails" });
/**

View File

@@ -1,5 +1,41 @@
/**
* The user and account routes.
* The user and account routes. Public profiles never carry the password hash or
* the email.
*
* - `POST /api/users/register` — public, with a valid invite. Creates an
* account, sends the welcome email, and answers the user and their tokens.
* - `POST /api/users/login` — public. Answers the user and their tokens;
* rate-limited per account.
* - `GET /api/users/me` — signed in. Answers the caller's own user.
* - `GET /api/users/me/media-token` — signed in. Answers a media token, for a
* session that predates them.
* - `POST /api/users/request-password-reset` — public. Emails a reset link to
* the account with that address, if any; always answers 200.
* - `POST /api/users/reset-password` — public, with a reset token. Sets a new
* password.
* - `POST /api/users/me/change-password` — signed in, with the current
* password. Sets a new one, ending every other session, and answers fresh
* tokens.
* - `PATCH /api/users/me` — signed in. Updates the caller's profile fields:
* description and email.
* - `PATCH /api/users/:userId/role` — permission `user:manage`. Sets another
* user's role.
* - `GET /api/users/search?q=` — public. Answers up to 8 users whose name
* starts with `q`, for @mention suggestions.
* - `GET /api/users/by-id/:userId` — public. Answers a user's profile by id.
* - `GET /api/users/:username/followed-users?page=&limit=` — public. Answers a
* page of the users they follow.
* - `GET /api/users/:username/followed-playlists?page=&limit=` — public.
* Answers a page of the public playlists they follow.
* - `GET /api/users/:username/playlists?page=&limit=` — optional sign-in.
* Answers a page of their playlists, the private ones only to themselves.
* - `GET /api/users/:username/invitees` — public. Answers the tree of users
* they invited.
* - `GET /api/users/:username` — public. Answers a user's profile.
* - `GET /api/users/:username/dumps?page=&limit=` — optional sign-in. Answers a
* page of their dumps, the private ones only to themselves.
* - `GET /api/users/:username/votes?page=&limit=` — optional sign-in. Answers a
* page of the dumps they upvoted, private ones only to themselves.
*
* @module
*/
@@ -70,45 +106,7 @@ import {
getFollowedUsersByUser,
} from "../services/follow-service.ts";
/**
* The user and account routes. Public profiles never carry the password hash or
* the email:
*
* - `POST /api/users/register` — public, with a valid invite. Creates an
* account, sends the welcome email, and answers the user and their tokens.
* - `POST /api/users/login` — public. Answers the user and their tokens;
* rate-limited per account.
* - `GET /api/users/me` — signed in. Answers the caller's own user.
* - `GET /api/users/me/media-token` — signed in. Answers a media token, for a
* session that predates them.
* - `POST /api/users/request-password-reset` — public. Emails a reset link to
* the account with that address, if any; always answers 200.
* - `POST /api/users/reset-password` — public, with a reset token. Sets a new
* password.
* - `POST /api/users/me/change-password` — signed in, with the current
* password. Sets a new one, ending every other session, and answers fresh
* tokens.
* - `PATCH /api/users/me` — signed in. Updates the caller's profile fields:
* description and email.
* - `PATCH /api/users/:userId/role` — permission `user:manage`. Sets another
* user's role.
* - `GET /api/users/search?q=` — public. Answers up to 8 users whose name
* starts with `q`, for @mention suggestions.
* - `GET /api/users/by-id/:userId` — public. Answers a user's profile by id.
* - `GET /api/users/:username/followed-users?page=&limit=` — public. Answers a
* page of the users they follow.
* - `GET /api/users/:username/followed-playlists?page=&limit=` — public.
* Answers a page of the public playlists they follow.
* - `GET /api/users/:username/playlists?page=&limit=` — optional sign-in.
* Answers a page of their playlists, the private ones only to themselves.
* - `GET /api/users/:username/invitees` — public. Answers the tree of users
* they invited.
* - `GET /api/users/:username` — public. Answers a user's profile.
* - `GET /api/users/:username/dumps?page=&limit=` — optional sign-in. Answers a
* page of their dumps, the private ones only to themselves.
* - `GET /api/users/:username/votes?page=&limit=` — optional sign-in. Answers a
* page of the dumps they upvoted, private ones only to themselves.
*/
/** The router serving the endpoints the module doc lists. */
const router: Router = new Router({ prefix: "/api/users" });
/** Login attempts per account: a burst of 10, then one every 30s. */

View File

@@ -1,6 +1,12 @@
/**
* The WebSocket route, and the handling of what clients send over it.
*
* - `GET /ws?token=` — public; a session `token` signs the socket in, and an
* invalid one leaves it anonymous. Opens the socket that carries presence,
* live updates, votes, comment likes and the chat (see
* `ClientToServerMessage` and `ServerToClientMessage` in shared/ws.ts); 403
* from an origin not in {@link ALLOWED_ORIGINS}.
*
* @module
*/
@@ -56,15 +62,7 @@ import type {
} from "../../shared/ws.ts";
import type { User } from "../model/user.ts";
/**
* The WebSocket route:
*
* - `GET /ws?token=` — public; a session `token` signs the socket in, and an
* invalid one leaves it anonymous. Opens the socket that carries presence,
* live updates, votes, comment likes and the chat (see
* `ClientToServerMessage` and `ServerToClientMessage` in shared/ws.ts); 403
* from an origin not in {@link ALLOWED_ORIGINS}.
*/
/** The router serving the endpoints the module doc lists. */
const router: Router = new Router();
/** Checks whether a socket may be opened from `origin`. */

View File

@@ -1,5 +1,8 @@
/**
* The YouTube route.
* The YouTube route, for the player's tracklist.
*
* - `GET /api/youtube/playlist?list=` — public. Resolves a YouTube playlist id
* to its tracklist (titles and lengths).
*
* @module
*/
@@ -9,12 +12,7 @@ import type { APIResponse } from "../../shared/api.ts";
import { resolveYouTubePlaylist } from "../remote/youtube-playlist.ts";
import type { YouTubePlaylist } from "../../shared/player.ts";
/**
* The YouTube route, for the player's tracklist:
*
* - `GET /api/youtube/playlist?list=` — public. Resolves a YouTube playlist id
* to its tracklist (titles and lengths).
*/
/** The router serving the endpoints the module doc lists. */
const youtubeRouter: Router = new Router();
// Public, like /api/bandcamp/tracks: it only reads a public page. The client