fix(security): use signed media tokens for ticket attachments

Backend now requires a signed, expiring token to download media (a raw file_id
404s). Carry the per-attachment `token` (and the legacy `media_token`) from the
ticket response through MessageMediaGrid and append it in ticketsApi.getMediaUrl,
so attachments keep rendering while leaked URLs expire. Must deploy together with
the backend change.
This commit is contained in:
c0mrade
2026-06-04 17:45:43 +03:00
parent b4f066942a
commit c6fc167d80
3 changed files with 19 additions and 6 deletions

View File

@@ -79,9 +79,11 @@ export const ticketsApi = {
return response.data;
},
// Get media URL for display
getMediaUrl: (fileId: string): string => {
// Get media URL for display. The signed `token` comes from the ticket
// response and is required by the backend (a raw file_id alone 404s).
getMediaUrl: (fileId: string, token?: string | null): string => {
const baseUrl = import.meta.env.VITE_API_URL || '';
return `${baseUrl}/cabinet/media/${fileId}`;
const suffix = token ? `?token=${encodeURIComponent(token)}` : '';
return `${baseUrl}/cabinet/media/${fileId}${suffix}`;
},
};

View File

@@ -9,12 +9,15 @@ export interface MediaItem {
type: string;
file_id: string;
caption?: string | null;
/** Signed, expiring download token from the ticket response. */
token?: string | null;
}
interface MessageLike {
has_media?: boolean;
media_type?: string | null;
media_file_id?: string | null;
media_token?: string | null;
media_caption?: string | null;
media_items?: MediaItem[] | null;
}
@@ -33,6 +36,7 @@ function getItems(message: MessageLike): MediaItem[] {
type: message.media_type,
file_id: message.media_file_id,
caption: message.media_caption,
token: message.media_token,
},
];
}
@@ -113,7 +117,7 @@ export function MessageMediaGrid({
onClick={() => openFullscreen(originalIdx)}
>
<img
src={ticketsApi.getMediaUrl(item.file_id)}
src={ticketsApi.getMediaUrl(item.file_id, item.token)}
alt={item.caption || 'Attached photo'}
className="h-full w-full object-cover transition-opacity group-hover:opacity-90"
loading="lazy"
@@ -131,7 +135,7 @@ export function MessageMediaGrid({
{/* Non-photo media rendered inline */}
{otherItems.map((item) => {
const mediaUrl = ticketsApi.getMediaUrl(item.file_id);
const mediaUrl = ticketsApi.getMediaUrl(item.file_id, item.token);
if (item.type === 'video') {
return (
<div key={item.file_id}>
@@ -203,7 +207,10 @@ export function MessageMediaGrid({
onClick={closeFullscreen}
>
<img
src={ticketsApi.getMediaUrl(photoItems[fullscreenIndex].file_id)}
src={ticketsApi.getMediaUrl(
photoItems[fullscreenIndex].file_id,
photoItems[fullscreenIndex].token,
)}
alt={photoItems[fullscreenIndex].caption || 'Attached photo'}
className="max-h-full max-w-full object-contain"
style={{ touchAction: 'pinch-zoom' }}

View File

@@ -486,6 +486,8 @@ export interface TicketMediaItem {
type: 'photo' | 'video' | 'document';
file_id: string;
caption?: string | null;
/** Signed, expiring download token (response only). */
token?: string | null;
}
export interface TicketMessage {
@@ -495,6 +497,8 @@ export interface TicketMessage {
has_media: boolean;
media_type: string | null;
media_file_id: string | null;
/** Signed, expiring download token for the legacy single media_file_id. */
media_token?: string | null;
media_caption: string | null;
media_items?: TicketMediaItem[] | null;
created_at: string;