/** * Checks the JSON bodies that `routes/users.ts` receives before it reads them. * * @module */ import { isRole, type LoginUserRequest, type RegisterUserRequest, type UpdateUserRequest, } from "../../shared/user.ts"; import { VALIDATION } from "../../shared/validation.ts"; /** * Checks whether `obj` is a {@link LoginUserRequest}. It checks the types * only: a wrong name or password is the login route's answer to give. */ export function isLoginUserRequest(obj: unknown): obj is LoginUserRequest { return !!obj && typeof obj === "object" && "username" in obj && typeof obj.username === "string" && "password" in obj && typeof obj.password === "string"; } /** * Returns `obj[key]` when `obj` is an object that has it, else undefined. The * users routes whose body has no guard of its own read their fields with it. */ export function fieldOf(obj: unknown, key: string): unknown { return obj !== null && typeof obj === "object" && key in obj ? (obj as Record)[key] : undefined; } /** * Checks whether `obj` is a valid {@link RegisterUserRequest}. When it isn't, * {@link validateRegisterUserRequest} says why. */ export function isRegisterUserRequest( obj: unknown, ): obj is RegisterUserRequest { return validateRegisterUserRequest(obj) === null; } /** * Returns what is wrong with a sign-up body, as the message the register page * shows, or null when it is valid. It checks the shape, the email's form, and * the username and password against the `VALIDATION` limits. Whether the name, * the email or the invite is taken is `createUser`'s and `claimInvite`'s check. */ export function validateRegisterUserRequest(obj: unknown): string | null { if ( !obj || typeof obj !== "object" || !("username" in obj) || typeof obj.username !== "string" || !("password" in obj) || typeof obj.password !== "string" || !("inviteToken" in obj) || typeof obj.inviteToken !== "string" || !("email" in obj) || typeof obj.email !== "string" ) return "Invalid request"; const { username, password, email } = obj as RegisterUserRequest; if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { return "Invalid email address"; } if ( !new RegExp( `^[a-zA-Z0-9_]{${VALIDATION.USERNAME_MIN},${VALIDATION.USERNAME_MAX}}$`, ) .test(username) ) { return `Username must be ${VALIDATION.USERNAME_MIN}–${VALIDATION.USERNAME_MAX} characters and contain only letters, numbers, or underscores`; } if (password.length < VALIDATION.PASSWORD_MIN) { return `Password must be at least ${VALIDATION.PASSWORD_MIN} characters`; } if (password.length > VALIDATION.PASSWORD_MAX) { return `Password must be at most ${VALIDATION.PASSWORD_MAX} characters`; } return null; } /** * Checks whether `obj` is a valid {@link UpdateUserRequest}, for * `PATCH /api/users/me`. Every field is optional, but each one present must be * well-formed and within the `VALIDATION` limits. The username's limits here * are not those of sign-up, which the route never lets it change anyway. */ export function isUpdateUserRequest(obj: unknown): obj is UpdateUserRequest { if (!obj || typeof obj !== "object") return false; const o = obj as Record; if ("username" in o) { if (typeof o.username !== "string") return false; if (!/^[a-zA-Z0-9_]{1,32}$/.test(o.username as string)) return false; } if ("password" in o) { if (typeof o.password !== "string") return false; const len = (o.password as string).length; if (len < VALIDATION.PASSWORD_MIN || len > VALIDATION.PASSWORD_MAX) { return false; } } if ("role" in o && !isRole(o.role)) return false; if ( "description" in o && typeof o.description !== "string" && o.description !== null ) return false; if ("email" in o) { if (typeof o.email !== "string") return false; if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(o.email as string)) return false; } if ( typeof o.description === "string" && (o.description as string).length > VALIDATION.USER_DESCRIPTION_MAX ) return false; return true; }