119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
/**
|
|
* The `users` table: its row type, its guard, its mappers to and from the
|
|
* stored {@link User}, and the views of a user the API sends.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import type { SQLOutputValue } from "node:sqlite";
|
|
import {
|
|
isRole,
|
|
type PublicUser,
|
|
type Role,
|
|
type SelfUser,
|
|
} from "../../shared/user.ts";
|
|
|
|
/** A user as stored: what they see of themselves, plus the password hash. */
|
|
export interface User extends SelfUser {
|
|
/** The scrypt hash of the password, as `hash.salt`. */
|
|
passwordHash: string;
|
|
}
|
|
|
|
/** Returns what anyone may see of `user`: no hash, no email. */
|
|
export function toPublicUser(
|
|
{ passwordHash: _, email: _email, ...user }: User,
|
|
): PublicUser {
|
|
return user;
|
|
}
|
|
|
|
/** Returns what `user` sees of themselves: everything but the hash. */
|
|
export function toSelfUser({ passwordHash: _, ...user }: User): SelfUser {
|
|
return user;
|
|
}
|
|
|
|
/** A row of the `users` table. */
|
|
export interface UserRow {
|
|
/** The user's id. */
|
|
id: string;
|
|
/** The user's name, unique. */
|
|
username: string;
|
|
/** The scrypt hash of the password, as `hash.salt`. */
|
|
password_hash: string;
|
|
/** The user's role. */
|
|
role: Role;
|
|
/** When the user registered. */
|
|
created_at: string;
|
|
/** When the profile was last changed, if ever. */
|
|
updated_at: string | null;
|
|
/** The avatar's type, null without one. */
|
|
avatar_mime: string | null;
|
|
/** The user's profile text, in Markdown. */
|
|
description: string | null;
|
|
/** The id of the user whose invite they registered with. */
|
|
invited_by: string | null;
|
|
/**
|
|
* The inviter's name; present only when the query joins it (`LEFT JOIN users
|
|
* i ON i.id = u.invited_by`).
|
|
*/
|
|
invited_by_username: string | null;
|
|
/** The user's email address. */
|
|
email: string;
|
|
/** Any other column the query selects. */
|
|
[key: string]: SQLOutputValue;
|
|
}
|
|
|
|
/** Checks whether `obj` is a {@link UserRow}. */
|
|
export function isUserRow(obj: unknown): obj is UserRow {
|
|
return !!obj &&
|
|
typeof obj === "object" &&
|
|
"id" in obj && typeof obj.id === "string" &&
|
|
"username" in obj && typeof obj.username === "string" &&
|
|
"password_hash" in obj && typeof obj.password_hash === "string" &&
|
|
"role" in obj && isRole(obj.role) &&
|
|
"created_at" in obj && typeof obj.created_at === "string" &&
|
|
"updated_at" in obj &&
|
|
(typeof obj.updated_at === "string" || obj.updated_at === null) &&
|
|
"avatar_mime" in obj &&
|
|
(typeof obj.avatar_mime === "string" || obj.avatar_mime === null) &&
|
|
"description" in obj &&
|
|
(typeof obj.description === "string" || obj.description === null) &&
|
|
"invited_by" in obj &&
|
|
(typeof obj.invited_by === "string" || obj.invited_by === null) &&
|
|
"email" in obj && typeof obj.email === "string";
|
|
}
|
|
|
|
/** Converts a row to the stored {@link User}. */
|
|
export function userRowToApi(row: UserRow): User {
|
|
return {
|
|
id: row.id,
|
|
username: row.username,
|
|
passwordHash: row.password_hash,
|
|
role: row.role,
|
|
createdAt: new Date(row.created_at),
|
|
updatedAt: row.updated_at ? new Date(row.updated_at) : undefined,
|
|
avatarMime: row.avatar_mime ?? undefined,
|
|
description: row.description ?? undefined,
|
|
invitedByUsername: typeof row.invited_by_username === "string"
|
|
? row.invited_by_username
|
|
: undefined,
|
|
email: row.email,
|
|
};
|
|
}
|
|
|
|
/** Converts a {@link User} back to a row, without the join-only columns. */
|
|
export function userApiToRow(user: User): UserRow {
|
|
return {
|
|
id: user.id,
|
|
username: user.username,
|
|
password_hash: user.passwordHash,
|
|
role: user.role,
|
|
created_at: user.createdAt.toISOString(),
|
|
updated_at: user.updatedAt?.toISOString() ?? null,
|
|
avatar_mime: user.avatarMime ?? null,
|
|
description: user.description ?? null,
|
|
invited_by: null,
|
|
invited_by_username: null,
|
|
email: user.email,
|
|
};
|
|
}
|