Validation: the dump validators refuse the nulls their shared types don't allow (a null title or note on creation, a null URL on edit); a playlist title of spaces is refused and titles are stored trimmed; usernames and emails are checked by the same USERNAME_RE and EMAIL_RE in shared/validation.ts on sign-up, on PATCH /api/users/me (which checked other limits) and in the sign-up form. Mentions are read by shared/mention.ts's mentionedUsernames, which skips code and URLs as Markdown shows them: https://mastodon.social/@bob or a `@bob` in code notified bob; the editor's suggestions derive their trigger from the same pattern instead of a copy. Following an unknown user is a 404 rather than a foreign-key 500, and following a private playlist a 404 rather than a 403 that confirmed it exists. `ws` and `emojibase` are reserved category slugs, since the server answers them before the app. GET /api/users/me no longer turns every error into a 500. Dead code goes: adminOnlyMiddleware, verifyEmailTransport, listUsers, deleteUser with disconnectUser and the force_logout message only it sent, PATCH /api/notifications/:id/read (with markOneRead), isSlugAvailable, the unreachable 500 in fetchComment, and the `if (!ctx.state.user)` checks behind authMiddleware. 214 backend and 627 frontend tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
3.8 KiB
TypeScript
109 lines
3.8 KiB
TypeScript
/**
|
||
* 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 { EMAIL_RE, USERNAME_RE, 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<string, unknown>)[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 (!EMAIL_RE.test(email)) {
|
||
return "Invalid email address";
|
||
}
|
||
if (!USERNAME_RE.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.
|
||
*/
|
||
export function isUpdateUserRequest(obj: unknown): obj is UpdateUserRequest {
|
||
if (!obj || typeof obj !== "object") return false;
|
||
const o = obj as Record<string, unknown>;
|
||
if ("username" in o) {
|
||
if (typeof o.username !== "string") return false;
|
||
if (!USERNAME_RE.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 (!EMAIL_RE.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;
|
||
}
|