v3: comments say what the code can't — the documentation pass wanted JSDoc on every export and every field of an exported interface, which filled the code with restatements ("The dump.", "Whether it is loading.", "The props of X.", "Renders the X.") and module docs that told a reader nothing ("The hot and new feeds, and the props every main feed of the index takes."); some were wrong as well as vague. Every comment in api/, shared/ and src/ is rewritten under one rule, now stated in the README's "Comments" section: every module and export still has a JSDoc, and it gives context (who uses it and where), usage (what a caller must know: a precondition, what it leaves to the caller, a side effect) or its place in the chain (where its input comes from, where its output goes); a field, parameter, private function or local gets one only when it adds to its parent's, and context about a whole type goes on the type; a type's doc is phrased from the value's side ("Returned to Y by X when Z", "Passed to Y by X"); full sentences, as short as they can be, no label-colon-list; // goes wherever the code isn't obvious and says why, and the section banners and narration go. Every claim was checked against the code, which turned up some forty comments that were wrong: DumpCard's claimed it was only used by the hot and new feeds, the WebSocket welcome claimed to be a socket's first message (presence comes first), UpdateUserRequest said null removes a description (it keeps it), deletedDumpIds leaves out that it also holds dumps made private, THEME_COLOR claimed to mirror index.html's, and the like. The routers' JSDoc now says where they are mounted and what becomes of a thrown APIException. doc-lint.ts lets a field without a doc through; a module or export without one still fails. No code changes; 204 backend and 628 frontend tests pass, and doc:lint, deno check, deno lint and tsc are clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
/**
|
||||
* Users: accounts, profiles, lookups and search, and the invite tree.
|
||||
* Reads and writes the `users` table. Its `User`s carry the password hash and
|
||||
* email, so a route strips them with `toPublicUser` or `toSelfUser` before
|
||||
* answering. `routes/users.ts` is its main caller; `lib/auth.ts` checks every
|
||||
* token against {@link getSessionState}.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
@@ -27,7 +30,7 @@ import { closeUserSockets, disconnectUser } from "./ws-service.ts";
|
||||
import { hashPassword } from "../lib/jwt.ts";
|
||||
import { linkAttachments } from "./attachment-service.ts";
|
||||
|
||||
/** The query selecting full `UserRow`s from `users u`, with the inviter's name. */
|
||||
/** Selects full `UserRow`s from `users u`, with the inviter's name joined. */
|
||||
const USER_SELECT =
|
||||
`SELECT u.id, u.username, u.password_hash, u.role, u.created_at, u.updated_at, u.avatar_mime, u.description, u.invited_by, u.email,
|
||||
i.username as invited_by_username
|
||||
@@ -35,7 +38,9 @@ const USER_SELECT =
|
||||
LEFT JOIN users i ON i.id = u.invited_by`;
|
||||
|
||||
/**
|
||||
* Creates a user with the `user` role, invited by `inviterId`.
|
||||
* Creates a user with the `user` role, invited by `inviterId`, for
|
||||
* `POST /api/users/register`. The caller has validated `request` and claimed
|
||||
* the invite.
|
||||
*
|
||||
* @throws {APIException} 400 when the username is taken, 409 when the email
|
||||
* is.
|
||||
@@ -77,8 +82,8 @@ export async function createUser(
|
||||
request.email,
|
||||
);
|
||||
} catch (err) {
|
||||
// The check above ran before the hash yielded, so a parallel registration
|
||||
// can still take the name first; the UNIQUE constraint is the real guard.
|
||||
// A parallel sign-up can take the name while the hash is computed; the
|
||||
// UNIQUE constraint is the real guard. Emails have no such constraint.
|
||||
if (err instanceof Error && err.message.includes("UNIQUE")) {
|
||||
throw new APIException(
|
||||
APIErrorCode.VALIDATION_ERROR,
|
||||
@@ -100,7 +105,7 @@ export async function createUser(
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns user `userId`.
|
||||
* Returns user `userId`, with their hash and email.
|
||||
*
|
||||
* @throws {APIException} 404 when there is no such user.
|
||||
*/
|
||||
@@ -117,9 +122,9 @@ export function getUserById(userId: string): User {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns what a session token is checked against: the user's live role, and
|
||||
* the moment older tokens stopped counting (their last password change).
|
||||
* Undefined when the user is gone.
|
||||
* Returns the user's live role and their last password change, before which
|
||||
* their tokens don't count, or undefined when the user is gone. `lib/auth.ts`
|
||||
* checks every session and media token against it.
|
||||
*/
|
||||
export function getSessionState(
|
||||
userId: string,
|
||||
@@ -136,7 +141,8 @@ export function getSessionState(
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user named `username`.
|
||||
* Returns the user named `username`, with their hash and email. The name
|
||||
* matches case-sensitively.
|
||||
*
|
||||
* @throws {APIException} 404 when there is no such user.
|
||||
*/
|
||||
@@ -153,9 +159,9 @@ export function getUserByUsername(username: string): User {
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws a 409 when another account than `exceptUserId` has `email`. Password
|
||||
* resets are looked up by email, so an address can belong to one account only
|
||||
* — otherwise which account a reset link resets is up to row order.
|
||||
* Throws a 409 when an account other than `exceptUserId` has `email`, in any
|
||||
* case. Password resets are looked up by email, so an address must belong to
|
||||
* one account only.
|
||||
*/
|
||||
function assertEmailAvailable(email: string, exceptUserId?: string): void {
|
||||
const row = db.prepare(
|
||||
@@ -170,7 +176,10 @@ function assertEmailAvailable(email: string, exceptUserId?: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the user whose email is `email`, or null. */
|
||||
/**
|
||||
* Returns the user whose email is exactly `email`, or null, for
|
||||
* `requestPasswordReset`. Unlike the uniqueness check, it is case-sensitive.
|
||||
*/
|
||||
export function getUserByEmail(email: string): User | null {
|
||||
const userRow = db.prepare(
|
||||
`${USER_SELECT} WHERE u.email = ?`,
|
||||
@@ -181,8 +190,8 @@ export function getUserByEmail(email: string): User | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns up to `limit` users whose name starts with `query`, for @mention
|
||||
* suggestions.
|
||||
* Returns up to `limit` users whose name starts with `query`, for the editor's
|
||||
* @mention suggestions (`GET /api/users/search`). An empty query returns none.
|
||||
*/
|
||||
export function searchUsers(
|
||||
query: string,
|
||||
@@ -204,8 +213,8 @@ export function searchUsers(
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns up to `limit` users whose name or profile contains `query`, for the
|
||||
* public search page: profiles only, never hashes or emails.
|
||||
* Returns up to `limit` users whose name or description contains `query`, for
|
||||
* the `/search` page (`GET /api/search`). They are already public profiles.
|
||||
*/
|
||||
export function searchUsersGlobal(query: string, limit: number): PublicUser[] {
|
||||
if (!query.trim()) return [];
|
||||
@@ -216,7 +225,11 @@ export function searchUsersGlobal(query: string, limit: number): PublicUser[] {
|
||||
return rows.filter(isUserRow).map(userRowToApi).map(toPublicUser);
|
||||
}
|
||||
|
||||
/** Returns every user. */
|
||||
/**
|
||||
* Returns every user. Nothing calls it.
|
||||
*
|
||||
* @throws {APIException} 404 when a row doesn't parse as a user.
|
||||
*/
|
||||
export function listUsers(): User[] {
|
||||
const userRows = db.prepare(
|
||||
`${USER_SELECT}`,
|
||||
@@ -230,8 +243,11 @@ export function listUsers(): User[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies `request` to user `userId`. A new password also ends every session
|
||||
* opened before it and closes the user's sockets.
|
||||
* Applies `request` to user `userId` and returns the updated user. It trusts
|
||||
* `request`: the routes pick the fields a caller may change. A new password
|
||||
* also ends every session opened before it and closes the user's sockets, so
|
||||
* the caller must issue fresh tokens if the user is to stay signed in. A
|
||||
* `description` of null keeps the current one.
|
||||
*
|
||||
* @throws {APIException} 404 when there is no such user, 409 when the new
|
||||
* email belongs to another account.
|
||||
@@ -275,7 +291,6 @@ export async function updateUser(
|
||||
throw new APIException(APIErrorCode.NOT_FOUND, 404, "User not found");
|
||||
}
|
||||
|
||||
// A new password ends every session opened with the old one.
|
||||
if (password) {
|
||||
db.prepare(`UPDATE users SET tokens_valid_after = ? WHERE id = ?;`).run(
|
||||
now.toISOString(),
|
||||
@@ -292,7 +307,9 @@ export async function updateUser(
|
||||
}
|
||||
|
||||
/**
|
||||
* Records that user `userId` now has an avatar of type `mime`.
|
||||
* Records that user `userId` now has an avatar of type `mime`, once
|
||||
* `POST /api/avatars/me` has written the file. It bumps `updated_at`, which
|
||||
* the client uses to reload the image.
|
||||
*
|
||||
* @throws {APIException} 404 when there is no such user.
|
||||
*/
|
||||
@@ -307,8 +324,9 @@ export function updateUserAvatar(userId: string, mime: string): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns everyone `userId` invited, and everyone they invited in turn, down
|
||||
* to ten levels, in registration order.
|
||||
* Returns everyone `userId` invited, and everyone they invited in turn, eleven
|
||||
* levels deep at most, in sign-up order, for the profile page's invite tree
|
||||
* (`GET /api/users/:username/invitees`).
|
||||
*/
|
||||
export function getInviteTree(userId: string): Invitee[] {
|
||||
const rows = db.prepare(`
|
||||
@@ -341,7 +359,7 @@ export function getInviteTree(userId: string): Invitee[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes user `userId` and disconnects their sockets.
|
||||
* Deletes user `userId` and disconnects their sockets. Nothing calls it.
|
||||
*
|
||||
* @throws {APIException} 404 when there is no such user.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user