/** * Backend */ export interface RichContent { type: string; url: string; siteName?: string; title?: string; description?: string; thumbnailUrl?: string; videoId?: string; embedUrl?: string; } export interface Dump { id: string; kind: "url" | "file"; title: string; comment?: string; userId: string; createdAt: Date; url?: string; richContent?: RichContent; fileName?: string; fileMime?: string; fileSize?: number; voteCount: number; commentCount: number; isPrivate: boolean; } /** * Authentication */ export interface User { id: string; username: string; passwordHash: string; isAdmin: boolean; createdAt: Date; avatarMime?: string; } export interface LoginUserRequest { username: string; password: string; } export interface RegisterUserRequest { username: string; password: string; } export interface UpdateUserRequest { username?: string; password?: string; isAdmin?: boolean; } 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"; } export function isRegisterUserRequest( obj: unknown, ): obj is RegisterUserRequest { return !!obj && typeof obj === "object" && "username" in obj && typeof obj.username === "string" && "password" in obj && typeof obj.password === "string"; } export function isUpdateUserRequest(obj: unknown): obj is UpdateUserRequest { return !!obj && typeof obj === "object" && (!("username" in obj) || typeof obj.username === "string") && (!("password" in obj) || typeof obj.password === "string") && (!("isAdmin" in obj) || typeof obj.isAdmin === "boolean"); } export interface AuthResponse { token: string; user: User; } export interface AuthPayload { userId: string; username: string; isAdmin: boolean; exp: number; } export function isAuthPayload(obj: unknown): obj is AuthPayload { return !!obj && typeof obj === "object" && "userId" in obj && typeof obj.userId === "string" && "username" in obj && typeof obj.username === "string" && "isAdmin" in obj && typeof obj.isAdmin === "boolean" && "exp" in obj && typeof obj.exp === "number"; } /** * API */ export enum APIErrorCode { BAD_REQUEST = "BAD_REQUEST", NOT_FOUND = "NOT_FOUND", SERVER_ERROR = "SERVER_ERROR", TIMEOUT = "TIMEOUT", UNAUTHORIZED = "UNAUTHORIZED", VALIDATION_ERROR = "VALIDATION_ERROR", } export interface APIError { code: APIErrorCode; message: string; } export interface APISuccess { success: true; data: T; error?: never; } export interface APIFailure { success: false; data?: never; error: APIError; } export type APIResponse = APISuccess | APIFailure; export interface PaginatedData { items: T[]; total: number; hasMore: boolean; } export class APIException extends Error { readonly code: APIErrorCode; readonly status: number; constructor(code: APIErrorCode, status: number, message: string) { super(message); this.code = code; this.status = status; } } /** * Comments */ export interface Comment { id: string; dumpId: string; userId: string; parentId?: string; body: string; createdAt: Date; deleted: boolean; authorUsername: string; authorAvatarMime?: string; } export interface CreateCommentRequest { body: string; parentId?: string; } export function isCreateCommentRequest(obj: unknown): obj is CreateCommentRequest { if (!obj || typeof obj !== "object") return false; const o = obj as Record; return typeof o.body === "string" && (o.body as string).trim().length > 0 && (!("parentId" in o) || typeof o.parentId === "string" || o.parentId === null); } /** * Playlists */ export interface Playlist { id: string; userId: string; title: string; description?: string; isPublic: boolean; createdAt: Date; imageMime?: string; dumpCount?: number; } export interface PlaylistWithDumps extends Playlist { dumps: Dump[]; } export interface PlaylistMembership { playlist: Playlist; hasDump: boolean; } export interface CreatePlaylistRequest { title: string; description?: string; isPublic: boolean; } export interface UpdatePlaylistRequest { title?: string; description?: string; isPublic?: boolean; } export interface ReorderPlaylistRequest { dumpIds: string[]; } export function isCreatePlaylistRequest( obj: unknown, ): obj is CreatePlaylistRequest { return !!obj && typeof obj === "object" && "title" in obj && typeof obj.title === "string" && (!("description" in obj) || typeof obj.description === "string" || obj.description === null) && "isPublic" in obj && typeof obj.isPublic === "boolean"; } export function isUpdatePlaylistRequest( obj: unknown, ): obj is UpdatePlaylistRequest { return !!obj && typeof obj === "object" && (!("title" in obj) || typeof obj.title === "string") && (!("description" in obj) || typeof obj.description === "string" || obj.description === null) && (!("isPublic" in obj) || typeof obj.isPublic === "boolean"); } 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"); } /** * Request DTOs */ export interface CreateUrlDumpRequest { url: string; comment?: string; isPrivate?: boolean; } export function isCreateUrlDumpRequest( obj: unknown, ): obj is CreateUrlDumpRequest { return !!obj && typeof obj === "object" && "url" in obj && typeof obj.url === "string" && (!("comment" in obj) || typeof obj.comment === "string" || obj.comment === null) && (!("isPrivate" in obj) || typeof obj.isPrivate === "boolean"); } export interface UpdateDumpRequest { url?: string; comment?: string; isPrivate?: boolean; } export function isUpdateDumpRequest(obj: unknown): obj is UpdateDumpRequest { return !!obj && typeof obj === "object" && (!("url" in obj) || typeof obj.url === "string" || obj.url === null) && (!("comment" in obj) || typeof obj.comment === "string" || obj.comment === null) && (!("isPrivate" in obj) || typeof obj.isPrivate === "boolean"); } /** * WebSockets */ export interface VoteCastMessage { type: "vote_cast"; dumpId: string; userId: string; } export interface VoteAckMessageFailure { type: "vote_ack"; dumpId: string; success: false; error: APIError; } export interface VoteAckMessageSuccess { type: "vote_ack"; dumpId: string; action: "cast" | "remove"; success: true; voteCount: number; error?: never; } export type VoteAckMessage = VoteAckMessageSuccess | VoteAckMessageFailure; export interface VoteRemoveMessage { type: "vote_remove"; dumpId: string; } export interface VotesUpdateMessage { type: "votes_update"; dumpId: string; voteCount: number; } export interface OnlineUser { userId: string; username: string; hasAvatar: boolean; } export interface WelcomeMessage { type: "welcome"; users: OnlineUser[]; myVotes: string[]; } export interface PresenceUpdateMessage { type: "presence_update"; users: OnlineUser[]; } export interface PingMessage { type: "ping"; } export interface PongMessage { type: "pong"; }