mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
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:
@@ -79,9 +79,11 @@ export const ticketsApi = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get media URL for display
|
// Get media URL for display. The signed `token` comes from the ticket
|
||||||
getMediaUrl: (fileId: string): string => {
|
// 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 || '';
|
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}`;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ export interface MediaItem {
|
|||||||
type: string;
|
type: string;
|
||||||
file_id: string;
|
file_id: string;
|
||||||
caption?: string | null;
|
caption?: string | null;
|
||||||
|
/** Signed, expiring download token from the ticket response. */
|
||||||
|
token?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageLike {
|
interface MessageLike {
|
||||||
has_media?: boolean;
|
has_media?: boolean;
|
||||||
media_type?: string | null;
|
media_type?: string | null;
|
||||||
media_file_id?: string | null;
|
media_file_id?: string | null;
|
||||||
|
media_token?: string | null;
|
||||||
media_caption?: string | null;
|
media_caption?: string | null;
|
||||||
media_items?: MediaItem[] | null;
|
media_items?: MediaItem[] | null;
|
||||||
}
|
}
|
||||||
@@ -33,6 +36,7 @@ function getItems(message: MessageLike): MediaItem[] {
|
|||||||
type: message.media_type,
|
type: message.media_type,
|
||||||
file_id: message.media_file_id,
|
file_id: message.media_file_id,
|
||||||
caption: message.media_caption,
|
caption: message.media_caption,
|
||||||
|
token: message.media_token,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -113,7 +117,7 @@ export function MessageMediaGrid({
|
|||||||
onClick={() => openFullscreen(originalIdx)}
|
onClick={() => openFullscreen(originalIdx)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={ticketsApi.getMediaUrl(item.file_id)}
|
src={ticketsApi.getMediaUrl(item.file_id, item.token)}
|
||||||
alt={item.caption || 'Attached photo'}
|
alt={item.caption || 'Attached photo'}
|
||||||
className="h-full w-full object-cover transition-opacity group-hover:opacity-90"
|
className="h-full w-full object-cover transition-opacity group-hover:opacity-90"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@@ -131,7 +135,7 @@ export function MessageMediaGrid({
|
|||||||
|
|
||||||
{/* Non-photo media rendered inline */}
|
{/* Non-photo media rendered inline */}
|
||||||
{otherItems.map((item) => {
|
{otherItems.map((item) => {
|
||||||
const mediaUrl = ticketsApi.getMediaUrl(item.file_id);
|
const mediaUrl = ticketsApi.getMediaUrl(item.file_id, item.token);
|
||||||
if (item.type === 'video') {
|
if (item.type === 'video') {
|
||||||
return (
|
return (
|
||||||
<div key={item.file_id}>
|
<div key={item.file_id}>
|
||||||
@@ -203,7 +207,10 @@ export function MessageMediaGrid({
|
|||||||
onClick={closeFullscreen}
|
onClick={closeFullscreen}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={ticketsApi.getMediaUrl(photoItems[fullscreenIndex].file_id)}
|
src={ticketsApi.getMediaUrl(
|
||||||
|
photoItems[fullscreenIndex].file_id,
|
||||||
|
photoItems[fullscreenIndex].token,
|
||||||
|
)}
|
||||||
alt={photoItems[fullscreenIndex].caption || 'Attached photo'}
|
alt={photoItems[fullscreenIndex].caption || 'Attached photo'}
|
||||||
className="max-h-full max-w-full object-contain"
|
className="max-h-full max-w-full object-contain"
|
||||||
style={{ touchAction: 'pinch-zoom' }}
|
style={{ touchAction: 'pinch-zoom' }}
|
||||||
|
|||||||
@@ -486,6 +486,8 @@ export interface TicketMediaItem {
|
|||||||
type: 'photo' | 'video' | 'document';
|
type: 'photo' | 'video' | 'document';
|
||||||
file_id: string;
|
file_id: string;
|
||||||
caption?: string | null;
|
caption?: string | null;
|
||||||
|
/** Signed, expiring download token (response only). */
|
||||||
|
token?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TicketMessage {
|
export interface TicketMessage {
|
||||||
@@ -495,6 +497,8 @@ export interface TicketMessage {
|
|||||||
has_media: boolean;
|
has_media: boolean;
|
||||||
media_type: string | null;
|
media_type: string | null;
|
||||||
media_file_id: 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_caption: string | null;
|
||||||
media_items?: TicketMediaItem[] | null;
|
media_items?: TicketMediaItem[] | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user