/** * A user crosses the wire in several shapes: as anyone sees them, as they see * themselves, and in the bodies and answers of the `/api/users` and * `/api/invites` endpoints. This module types them all, with the roles, for the * server and the client alike. * * @module */ /** * Sets what a user may do beyond the basics, through `ROLE_PERMISSIONS` * (`shared/permissions.ts`). An admin changes it with * `PATCH /api/users/:key/role`. */ export type Role = "user" | "moderator" | "admin"; /** Every {@link Role}, least to most privileged. */ export const ROLES: readonly Role[] = ["user", "moderator", "admin"]; /** * Checks whether `value` is a {@link Role}, as the role endpoint and the row * guards must before trusting one. */ export function isRole(value: unknown): value is Role { return typeof value === "string" && (ROLES as readonly string[]).includes(value); } /** * Returned by every endpoint that shows a user to others: profiles, the users * one follows, search results, and the `user_updated` socket message. The * server makes it with `toPublicUser`, which drops the email. */ export interface PublicUser { id: string; /** * Picked at sign-up, and changeable since, a former name staying an alias * of the user for good. Pages write it as `@name` (`userPath`). */ username: string; role: Role; createdAt: Date; /** * Bumped by any profile or avatar change. The client uses it to reload the * avatar. */ updatedAt?: Date; /** Undefined without an avatar. */ avatarMime?: string; /** Markdown. */ description?: string; invitedByUsername?: string; } /** * Returned to a user about themselves: by sign-up and sign-in in * {@link AuthResponse}, by `GET /api/me`, and by an avatar upload. */ export interface SelfUser extends PublicUser { /** Where password reset links go. It belongs to this account only. */ email: string; } /** * Returned by `GET /api/users?q=` to the editor's @mention autocomplete. */ export interface UserSuggestion { id: string; username: string; /** Null without an avatar. */ avatarMime: string | null; } /** * Returned, as a flat list in sign-up order, by * `GET /api/users/:key/invitees`. The profile page nests it into a tree * by `invitedById`. */ export interface Invitee { id: string; username: string; /** Undefined without an avatar. */ avatarMime?: string; invitedById: string; createdAt: Date; } /** Sent to `POST /api/sessions` by the login page. */ export interface LoginUserRequest { username: string; password: string; } /** Sent to `POST /api/users` by the register page. */ export interface RegisterUserRequest { username: string; password: string; /** From the invite link the register page was opened with. */ inviteToken: string; email: string; } /** * Passed to `updateUser` by the users routes and the password reset, and sent * by a client to `PATCH /api/me`, which takes only `description` and `email`; * the role and the password change through their own endpoints, and the * username through `renameUser`, by a moderator only. */ export interface UpdateUserRequest { password?: string; role?: Role; /** An empty string clears it. Null, like undefined, keeps it. */ description?: string | null; email?: string; } /** * Returned by sign-up, sign-in and a password change. The client keeps both * tokens with its session. */ export interface AuthTokens { /** Sent as `Authorization: Bearer …`, and as `?token=` to the WebSocket. */ token: string; /** * Carried as `?token=` by the URLs of private media. It can read media and * nothing else. */ mediaToken: string; } /** Returned by sign-up and sign-in. */ export interface AuthResponse extends AuthTokens { user: SelfUser; } /** * Returned by `GET /api/me/media-token` to a session from before media * tokens, which has none. */ export type MediaTokenResponse = Pick; /** * Returned by `POST /api/invites` to the profile page, which builds the invite * link from `token`. */ export interface InviteResponse { token: string; }