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>
83 lines
2.5 KiB
TypeScript
83 lines
2.5 KiB
TypeScript
/**
|
|
* Checks the playlist request bodies, for the playlist routes to answer a 400.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import type {
|
|
CreatePlaylistRequest,
|
|
ReorderPlaylistRequest,
|
|
UpdatePlaylistRequest,
|
|
} from "../../shared/playlist.ts";
|
|
import { VALIDATION } from "../../shared/validation.ts";
|
|
|
|
/** Checks that `title` isn't blank and, trimmed, fits the limit. */
|
|
function isValidTitle(title: string): boolean {
|
|
const trimmed = title.trim();
|
|
return trimmed.length > 0 && trimmed.length <= VALIDATION.PLAYLIST_TITLE_MAX;
|
|
}
|
|
|
|
/**
|
|
* Checks whether `obj` is a {@link CreatePlaylistRequest} with a title that
|
|
* isn't blank, and a title and description within the `VALIDATION` limits. A
|
|
* null description passes.
|
|
*/
|
|
export function isCreatePlaylistRequest(
|
|
obj: unknown,
|
|
): obj is CreatePlaylistRequest {
|
|
if (
|
|
!obj || typeof obj !== "object" ||
|
|
!("title" in obj) || typeof obj.title !== "string" ||
|
|
!("isPublic" in obj) || typeof obj.isPublic !== "boolean"
|
|
) return false;
|
|
const o = obj as Record<string, unknown>;
|
|
if (!isValidTitle(o.title as string)) return false;
|
|
if (
|
|
"description" in o && typeof o.description !== "string" &&
|
|
o.description !== null
|
|
) return false;
|
|
if (
|
|
typeof o.description === "string" &&
|
|
(o.description as string).length > VALIDATION.PLAYLIST_DESCRIPTION_MAX
|
|
) return false;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks whether `obj` is an {@link UpdatePlaylistRequest} whose title, if
|
|
* given, isn't blank, with a title and description within the `VALIDATION`
|
|
* limits.
|
|
*/
|
|
export function isUpdatePlaylistRequest(
|
|
obj: unknown,
|
|
): obj is UpdatePlaylistRequest {
|
|
if (!obj || typeof obj !== "object") return false;
|
|
const o = obj as Record<string, unknown>;
|
|
if ("title" in o) {
|
|
if (typeof o.title !== "string") return false;
|
|
if (!isValidTitle(o.title as string)) return false;
|
|
}
|
|
if (
|
|
"description" in o && typeof o.description !== "string" &&
|
|
o.description !== null
|
|
) return false;
|
|
if (
|
|
typeof o.description === "string" &&
|
|
(o.description as string).length > VALIDATION.PLAYLIST_DESCRIPTION_MAX
|
|
) return false;
|
|
if ("isPublic" in o && typeof o.isPublic !== "boolean") return false;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks whether `obj` is a {@link ReorderPlaylistRequest}. Whether its ids
|
|
* match the playlist is `reorderPlaylist`'s to check.
|
|
*/
|
|
export function isReorderPlaylistRequest(
|
|
obj: unknown,
|
|
): obj is ReorderPlaylistRequest {
|
|
return !!obj && typeof obj === "object" &&
|
|
"dumpIds" in obj && Array.isArray(obj.dumpIds) &&
|
|
(obj.dumpIds as unknown[]).every((id) => typeof id === "string");
|
|
}
|