Encrypt file paths in API URLs using Fernet tokens
Raw filesystem paths were exposed in browser URLs, dev tools, and proxy logs. Now all file-serving endpoints accept an opaque encrypted token (t= param) derived from the session secret via HKDF, with a 4-hour TTL. Backend: - Add core/path_tokens.py with Fernet encrypt/decrypt (HKDF from .session_secret) - Add file_token to all list/gallery/feed/search responses across 7 routers - Accept optional t= param on all file-serving endpoints (backward compatible) Frontend: - Update 4 URL helpers in api.ts to prefer token when available - Add 4 new helpers for paid-content/embedded-metadata URLs - Update all 14 page/component files to pass file_token to URL builders - Add file_token to all relevant TypeScript interfaces Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -145,7 +145,9 @@ export default function EnhancedLightbox({
|
||||
setEmbeddedMetadataLoading(true)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/media/embedded-metadata?file_path=${encodeURIComponent(currentItem.file_path)}`,
|
||||
currentItem.file_token
|
||||
? `/api/media/embedded-metadata?t=${encodeURIComponent(currentItem.file_token)}`
|
||||
: `/api/media/embedded-metadata?file_path=${encodeURIComponent(currentItem.file_path)}`,
|
||||
{ credentials: 'include' }
|
||||
)
|
||||
if (response.ok) {
|
||||
|
||||
@@ -153,7 +153,9 @@ export default function GalleryLightbox({
|
||||
setEmbeddedMetadataLoading(true)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/media/embedded-metadata?file_path=${encodeURIComponent(currentItem.file_path)}`,
|
||||
currentItem.file_token
|
||||
? `/api/media/embedded-metadata?t=${encodeURIComponent(currentItem.file_token)}`
|
||||
: `/api/media/embedded-metadata?file_path=${encodeURIComponent(currentItem.file_path)}`,
|
||||
{ credentials: 'include' }
|
||||
)
|
||||
if (response.ok) {
|
||||
@@ -532,11 +534,15 @@ export default function GalleryLightbox({
|
||||
|
||||
// URL helpers
|
||||
const getPreviewUrl = (item: MediaGalleryItem) =>
|
||||
`/api/media/preview?file_path=${encodeURIComponent(item.file_path)}`
|
||||
item.file_token
|
||||
? `/api/media/preview?t=${encodeURIComponent(item.file_token)}`
|
||||
: `/api/media/preview?file_path=${encodeURIComponent(item.file_path)}`
|
||||
|
||||
const getThumbnailUrl = (item: MediaGalleryItem) => {
|
||||
const mediaType = isVideoFile(item) ? 'video' : 'image'
|
||||
return `/api/media/thumbnail?file_path=${encodeURIComponent(item.file_path)}&media_type=${mediaType}`
|
||||
return item.file_token
|
||||
? `/api/media/thumbnail?t=${encodeURIComponent(item.file_token)}&media_type=${mediaType}`
|
||||
: `/api/media/thumbnail?file_path=${encodeURIComponent(item.file_path)}&media_type=${mediaType}`
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
@@ -669,7 +669,7 @@ export default function BundleLightbox({
|
||||
|
||||
// URL helpers
|
||||
const getPreviewUrl = (item: PaidContentAttachment) =>
|
||||
item.local_path ? `/api/paid-content/files/serve?path=${encodeURIComponent(item.local_path)}` : ''
|
||||
item.local_path ? (item.file_token ? `/api/paid-content/files/serve?t=${encodeURIComponent(item.file_token)}` : `/api/paid-content/files/serve?path=${encodeURIComponent(item.local_path)}`) : ''
|
||||
|
||||
const getThumbnailUrl = (item: PaidContentAttachment) =>
|
||||
item.id ? `/api/paid-content/files/thumbnail/${item.id}?size=medium&${item.file_hash ? `v=${item.file_hash.slice(0, 8)}` : THUMB_CACHE_V}` : getPreviewUrl(item)
|
||||
|
||||
@@ -133,8 +133,8 @@ function PostDetailView({
|
||||
highlighted = false,
|
||||
}: PostDetailViewProps) {
|
||||
// Default URL generators for paid content
|
||||
const getVideoUrl = customGetVideoUrl || ((att: { id: number; local_path?: string | null }) =>
|
||||
att.local_path ? `/api/paid-content/files/serve?path=${encodeURIComponent(att.local_path)}` : null
|
||||
const getVideoUrl = customGetVideoUrl || ((att: { id: number; local_path?: string | null; file_token?: string | null }) =>
|
||||
att.local_path ? (att.file_token ? `/api/paid-content/files/serve?t=${encodeURIComponent(att.file_token)}` : `/api/paid-content/files/serve?path=${encodeURIComponent(att.local_path)}`) : null
|
||||
)
|
||||
const getThumbnailUrl = customGetThumbnailUrl || ((att: { id: number; file_hash?: string | null }) =>
|
||||
`/api/paid-content/files/thumbnail/${att.id}?size=large&${att.file_hash ? `v=${att.file_hash.slice(0, 8)}` : THUMB_CACHE_V}`
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface Download {
|
||||
content_type: string | null
|
||||
filename: string | null
|
||||
file_path: string | null
|
||||
file_token?: string
|
||||
file_size: number | null
|
||||
download_date: string
|
||||
status: string
|
||||
@@ -356,6 +357,7 @@ export interface MediaGalleryItem {
|
||||
height?: number
|
||||
duration?: number | null
|
||||
video_id?: string | null
|
||||
file_token?: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -603,6 +605,7 @@ export interface PaidContentAttachment {
|
||||
download_attempts: number
|
||||
downloaded_at: string | null
|
||||
created_at: string | null
|
||||
file_token?: string
|
||||
}
|
||||
|
||||
export interface PaidContentEmbed {
|
||||
@@ -809,6 +812,7 @@ export interface ReviewFile {
|
||||
height?: number
|
||||
video_id?: string | null
|
||||
original_path?: string
|
||||
file_token?: string
|
||||
face_recognition?: {
|
||||
scanned: boolean
|
||||
matched?: boolean
|
||||
@@ -1833,15 +1837,17 @@ class APIClient {
|
||||
}>(`/media/gallery/date-range${qs ? '?' + qs : ''}`).then(r => r.ranges)
|
||||
}
|
||||
|
||||
getMediaPreviewUrl(filePath: string) {
|
||||
// Security: Auth via httpOnly cookie only - no token in URL
|
||||
// Tokens in URLs are logged in browser history and server logs
|
||||
getMediaPreviewUrl(filePath: string, fileToken?: string) {
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/media/preview?t=${encodeURIComponent(fileToken)}`
|
||||
}
|
||||
return `${API_BASE}/media/preview?file_path=${encodeURIComponent(filePath)}`
|
||||
}
|
||||
|
||||
getMediaThumbnailUrl(filePath: string, mediaType: 'image' | 'video') {
|
||||
// Security: Auth via httpOnly cookie only - no token in URL
|
||||
// Tokens in URLs are logged in browser history and server logs
|
||||
getMediaThumbnailUrl(filePath: string, mediaType: 'image' | 'video', fileToken?: string) {
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/media/thumbnail?t=${encodeURIComponent(fileToken)}&media_type=${mediaType}`
|
||||
}
|
||||
return `${API_BASE}/media/thumbnail?file_path=${encodeURIComponent(filePath)}&media_type=${mediaType}`
|
||||
}
|
||||
|
||||
@@ -2216,19 +2222,51 @@ class APIClient {
|
||||
}>(`/monitoring/history?${params.toString()}`)
|
||||
}
|
||||
|
||||
getReviewThumbnailUrl(filePath: string): string {
|
||||
getReviewThumbnailUrl(filePath: string, fileToken?: string): string {
|
||||
// Determine media type from file extension
|
||||
const isVideo = filePath.match(/\.(mp4|mov|webm|avi|mkv|flv|m4v)$/i)
|
||||
const mediaType = isVideo ? 'video' : 'image'
|
||||
// Security: Auth via httpOnly cookie only - no token in URL
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/media/thumbnail?t=${encodeURIComponent(fileToken)}&media_type=${mediaType}`
|
||||
}
|
||||
return `${API_BASE}/media/thumbnail?file_path=${encodeURIComponent(filePath)}&media_type=${mediaType}`
|
||||
}
|
||||
|
||||
getReviewPreviewUrl(filePath: string): string {
|
||||
// Security: Auth via httpOnly cookie only - no token in URL
|
||||
getReviewPreviewUrl(filePath: string, fileToken?: string): string {
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/review/file?t=${encodeURIComponent(fileToken)}`
|
||||
}
|
||||
return `${API_BASE}/review/file?file_path=${encodeURIComponent(filePath)}`
|
||||
}
|
||||
|
||||
getMediaEmbeddedMetadataUrl(filePath: string, fileToken?: string): string {
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/media/embedded-metadata?t=${encodeURIComponent(fileToken)}`
|
||||
}
|
||||
return `${API_BASE}/media/embedded-metadata?file_path=${encodeURIComponent(filePath)}`
|
||||
}
|
||||
|
||||
getPaidContentServeUrl(localPath: string, fileToken?: string): string {
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/paid-content/files/serve?t=${encodeURIComponent(fileToken)}`
|
||||
}
|
||||
return `${API_BASE}/paid-content/files/serve?path=${encodeURIComponent(localPath)}`
|
||||
}
|
||||
|
||||
getPaidContentThumbnailUrl(filePath: string, fileToken?: string): string {
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/paid-content/thumbnail?t=${encodeURIComponent(fileToken)}`
|
||||
}
|
||||
return `${API_BASE}/paid-content/thumbnail?file_path=${encodeURIComponent(filePath)}`
|
||||
}
|
||||
|
||||
getPaidContentPreviewUrl(filePath: string, fileToken?: string): string {
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/paid-content/preview?t=${encodeURIComponent(fileToken)}`
|
||||
}
|
||||
return `${API_BASE}/paid-content/preview?file_path=${encodeURIComponent(filePath)}`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Smart Folders Methods
|
||||
// ============================================================================
|
||||
@@ -2239,6 +2277,7 @@ class APIClient {
|
||||
count: number
|
||||
previews: Array<{
|
||||
file_path: string
|
||||
file_token?: string
|
||||
content_type: string
|
||||
}>
|
||||
}>
|
||||
@@ -2254,6 +2293,7 @@ class APIClient {
|
||||
recent_downloads: Array<{
|
||||
id: number
|
||||
file_path: string
|
||||
file_token?: string
|
||||
filename: string
|
||||
platform: string
|
||||
source: string
|
||||
@@ -2265,6 +2305,7 @@ class APIClient {
|
||||
recent_deleted: Array<{
|
||||
id: number
|
||||
file_path: string
|
||||
file_token?: string
|
||||
filename: string
|
||||
platform: string
|
||||
source: string
|
||||
@@ -2277,6 +2318,7 @@ class APIClient {
|
||||
recent_moved_to_review: Array<{
|
||||
id: number
|
||||
file_path: string
|
||||
file_token?: string
|
||||
filename: string
|
||||
platform: string
|
||||
source: string
|
||||
@@ -2473,6 +2515,7 @@ class APIClient {
|
||||
results: Array<{
|
||||
id: number
|
||||
file_path: string
|
||||
file_token?: string
|
||||
filename: string
|
||||
platform: string
|
||||
source: string
|
||||
|
||||
@@ -2129,7 +2129,7 @@ export default function Dashboard() {
|
||||
<div className="flex items-center space-x-3 min-w-0 flex-1">
|
||||
{download.file_path ? (
|
||||
<img
|
||||
src={api.getMediaThumbnailUrl(download.file_path, mediaType)}
|
||||
src={api.getMediaThumbnailUrl(download.file_path, mediaType, download.file_token)}
|
||||
alt={download.filename || ''}
|
||||
className="w-12 h-12 object-cover rounded-lg flex-shrink-0 cursor-pointer ring-1 ring-border transition-all duration-200 hover:ring-2 hover:ring-primary hover:scale-105"
|
||||
loading="lazy"
|
||||
@@ -2449,8 +2449,8 @@ export default function Dashboard() {
|
||||
onNavigate={(index) => { setSelectedMedia(recentDownloads[index]) }}
|
||||
onDelete={handleDelete}
|
||||
onEditDate={handleSingleChangeDate}
|
||||
getPreviewUrl={(item) => api.getMediaPreviewUrl(item.file_path)}
|
||||
getThumbnailUrl={(item) => api.getMediaThumbnailUrl(item.file_path, isVideoFile(item.filename) ? 'video' : 'image')}
|
||||
getPreviewUrl={(item) => api.getMediaPreviewUrl(item.file_path, item.file_token)}
|
||||
getThumbnailUrl={(item) => api.getMediaThumbnailUrl(item.file_path, isVideoFile(item.filename) ? 'video' : 'image', item.file_token)}
|
||||
isVideo={(item) => isVideoFile(item.filename)}
|
||||
renderActions={(item) => (
|
||||
<>
|
||||
|
||||
@@ -206,7 +206,7 @@ export default function Discovery() {
|
||||
// Media action mutations
|
||||
const moveToReviewMutation = useMutation({
|
||||
mutationFn: async (filePath: string) => {
|
||||
return api.post('/media/move-to-review', { file_path: filePath })
|
||||
return api.post('/media/move-to-review', { file_paths: [filePath] })
|
||||
},
|
||||
onSuccess: (_, filePath) => {
|
||||
notificationManager.success('Moved to Review', 'File moved to review queue')
|
||||
@@ -637,7 +637,7 @@ export default function Discovery() {
|
||||
>
|
||||
{/* Thumbnail */}
|
||||
<ThrottledImage
|
||||
src={api.getMediaThumbnailUrl(result.file_path, isVideo ? 'video' : 'image')}
|
||||
src={api.getMediaThumbnailUrl(result.file_path, isVideo ? 'video' : 'image', result.file_token)}
|
||||
alt={result.filename}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
@@ -812,7 +812,7 @@ export default function Discovery() {
|
||||
{previews.map((preview, idx) => (
|
||||
<div key={idx} className="aspect-square rounded overflow-hidden bg-slate-100 dark:bg-slate-800">
|
||||
<ThrottledImage
|
||||
src={api.getMediaThumbnailUrl(preview.file_path, preview.content_type === 'video' ? 'video' : 'image')}
|
||||
src={api.getMediaThumbnailUrl(preview.file_path, preview.content_type === 'video' ? 'video' : 'image', preview.file_token)}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
@@ -1060,7 +1060,7 @@ export default function Discovery() {
|
||||
>
|
||||
<div className="w-10 h-10 rounded bg-slate-100 dark:bg-slate-700 overflow-hidden flex-shrink-0">
|
||||
<ThrottledImage
|
||||
src={api.getMediaThumbnailUrl(item.file_path, item.content_type === 'video' ? 'video' : 'image')}
|
||||
src={api.getMediaThumbnailUrl(item.file_path, item.content_type === 'video' ? 'video' : 'image', item.file_token)}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
@@ -1100,7 +1100,7 @@ export default function Discovery() {
|
||||
>
|
||||
<div className="w-10 h-10 rounded bg-slate-100 dark:bg-slate-700 overflow-hidden flex-shrink-0">
|
||||
<ThrottledImage
|
||||
src={api.getMediaThumbnailUrl(item.file_path, item.content_type === 'video' ? 'video' : 'image')}
|
||||
src={api.getMediaThumbnailUrl(item.file_path, item.content_type === 'video' ? 'video' : 'image', item.file_token)}
|
||||
alt=""
|
||||
className="w-full h-full object-cover opacity-60"
|
||||
/>
|
||||
@@ -1148,7 +1148,7 @@ export default function Discovery() {
|
||||
title={item.filename}
|
||||
>
|
||||
<ThrottledImage
|
||||
src={api.getMediaThumbnailUrl(item.file_path, item.content_type === 'video' ? 'video' : 'image')}
|
||||
src={api.getMediaThumbnailUrl(item.file_path, item.content_type === 'video' ? 'video' : 'image', item.file_token)}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
@@ -1414,6 +1414,7 @@ export default function Discovery() {
|
||||
<EnhancedLightbox
|
||||
items={searchResults.map(r => ({
|
||||
file_path: r.file_path,
|
||||
file_token: r.file_token,
|
||||
filename: r.filename,
|
||||
platform: r.platform,
|
||||
source: r.source,
|
||||
@@ -1427,8 +1428,8 @@ export default function Discovery() {
|
||||
onClose={() => setLightboxIndex(-1)}
|
||||
onNavigate={(index) => setLightboxIndex(index)}
|
||||
onDelete={handleDelete}
|
||||
getPreviewUrl={(item) => api.getMediaPreviewUrl(item.file_path)}
|
||||
getThumbnailUrl={(item) => api.getMediaThumbnailUrl(item.file_path, item.media_type as 'image' | 'video')}
|
||||
getPreviewUrl={(item) => api.getMediaPreviewUrl(item.file_path, item.file_token)}
|
||||
getThumbnailUrl={(item) => api.getMediaThumbnailUrl(item.file_path, item.media_type as 'image' | 'video', item.file_token)}
|
||||
isVideo={(item) => item.content_type === 'video' || isVideoFile(item.filename)}
|
||||
renderActions={(item: any) => (
|
||||
<>
|
||||
|
||||
@@ -28,6 +28,7 @@ interface MediaFile {
|
||||
deleted_from: string | null
|
||||
location_type?: 'media' | 'review' | 'recycle'
|
||||
video_id?: string | null
|
||||
file_token?: string
|
||||
}
|
||||
|
||||
interface DayGroup {
|
||||
@@ -507,7 +508,7 @@ export default function Downloads() {
|
||||
if (media.platform === 'youtube' && media.video_id) {
|
||||
return `/api/video/thumbnail/${media.platform}/${media.video_id}?source=downloads`
|
||||
}
|
||||
return api.getMediaThumbnailUrl(media.file_path, media.media_type)
|
||||
return api.getMediaThumbnailUrl(media.file_path, media.media_type, media.file_token)
|
||||
}
|
||||
|
||||
// Get preview URL based on location
|
||||
@@ -517,7 +518,7 @@ export default function Downloads() {
|
||||
// Security: Auth via httpOnly cookie only - no token in URL
|
||||
return `/api/recycle/file/${getNumericId(media)}`
|
||||
}
|
||||
return api.getMediaPreviewUrl(media.file_path)
|
||||
return api.getMediaPreviewUrl(media.file_path, media.file_token)
|
||||
}
|
||||
|
||||
const openLightbox = async (mediaFiles: MediaFile[], index: number) => {
|
||||
@@ -537,7 +538,8 @@ export default function Downloads() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const response = await api.get(`/media/metadata?file_path=${encodeURIComponent(file.file_path)}`) as { width?: number; height?: number; file_size?: number; duration?: number }
|
||||
const metadataUrl = file.file_token ? `/media/metadata?t=${encodeURIComponent(file.file_token)}` : `/media/metadata?file_path=${encodeURIComponent(file.file_path)}`
|
||||
const response = await api.get(metadataUrl) as { width?: number; height?: number; file_size?: number; duration?: number }
|
||||
if (response) {
|
||||
return {
|
||||
...file,
|
||||
|
||||
@@ -115,7 +115,7 @@ const JustifiedSection = memo(function JustifiedSection({
|
||||
{row.items.map(item => {
|
||||
const itemWidth = getAspectRatio(item) * row.height
|
||||
const isVideo = isVideoItem(item)
|
||||
const thumbUrl = api.getMediaThumbnailUrl(item.file_path, isVideo ? 'video' : 'image')
|
||||
const thumbUrl = api.getMediaThumbnailUrl(item.file_path, isVideo ? 'video' : 'image', item.file_token)
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function Media() {
|
||||
if (media.platform === 'youtube' && media.video_id) {
|
||||
return `/api/video/thumbnail/${media.platform}/${media.video_id}?source=downloads`
|
||||
}
|
||||
return api.getMediaThumbnailUrl(media.file_path, (media.media_type as 'image' | 'video') || 'image')
|
||||
return api.getMediaThumbnailUrl(media.file_path, (media.media_type as 'image' | 'video') || 'image', media.file_token)
|
||||
}
|
||||
|
||||
const [, setMediaResolution] = useState<string>('')
|
||||
@@ -1022,7 +1022,7 @@ export default function Media() {
|
||||
onNavigate={(index) => { setSelectedMedia(filteredMedia[index]); setMediaResolution('') }}
|
||||
onDelete={handleDelete}
|
||||
onEditDate={handleSingleChangeDate}
|
||||
getPreviewUrl={(item) => api.getMediaPreviewUrl(item.file_path)}
|
||||
getPreviewUrl={(item) => api.getMediaPreviewUrl(item.file_path, item.file_token)}
|
||||
getThumbnailUrl={(item: MediaGalleryItem) => getMediaThumbnailUrl(item)}
|
||||
isVideo={(item) => isVideoFile(item)}
|
||||
hideFaceRecognition={true}
|
||||
|
||||
@@ -22,6 +22,7 @@ interface MediaFile {
|
||||
source?: string
|
||||
download_date?: string
|
||||
added_date?: string
|
||||
file_token?: string
|
||||
}
|
||||
|
||||
interface Notification {
|
||||
@@ -356,7 +357,7 @@ export default function Notifications() {
|
||||
}
|
||||
|
||||
// Use regular thumbnail endpoint with current_path if file was moved
|
||||
return api.getMediaThumbnailUrl(statusInfo?.current_path || media.file_path, media.media_type)
|
||||
return api.getMediaThumbnailUrl(statusInfo?.current_path || media.file_path, media.media_type, media.file_token)
|
||||
}
|
||||
|
||||
// Helper to get preview URL based on file status
|
||||
@@ -370,7 +371,7 @@ export default function Notifications() {
|
||||
}
|
||||
|
||||
// Use regular preview endpoint with current_path if file was moved
|
||||
return api.getMediaPreviewUrl(statusInfo?.current_path || media.file_path)
|
||||
return api.getMediaPreviewUrl(statusInfo?.current_path || media.file_path, media.file_token)
|
||||
}
|
||||
|
||||
const openLightbox = async (mediaFiles: MediaFile[], index: number, notification: Notification) => {
|
||||
@@ -413,7 +414,8 @@ export default function Notifications() {
|
||||
} else {
|
||||
// For regular/review items, use media metadata endpoint
|
||||
const currentPath = statusInfo?.current_path || file.file_path
|
||||
const response = await api.get(`/media/metadata?file_path=${encodeURIComponent(currentPath)}`) as any
|
||||
const metadataUrl = file.file_token ? `/media/metadata?t=${encodeURIComponent(file.file_token)}` : `/media/metadata?file_path=${encodeURIComponent(currentPath)}`
|
||||
const response = await api.get(metadataUrl) as any
|
||||
if (response) {
|
||||
return {
|
||||
...file,
|
||||
|
||||
@@ -19,7 +19,7 @@ function getReviewThumbnailUrl(file: ReviewFile): string {
|
||||
if (file.platform === 'youtube' && file.video_id) {
|
||||
return `/api/video/thumbnail/${file.platform}/${file.video_id}?source=downloads`
|
||||
}
|
||||
return api.getReviewThumbnailUrl(file.file_path)
|
||||
return api.getReviewThumbnailUrl(file.file_path, file.file_token)
|
||||
}
|
||||
|
||||
export default function Review() {
|
||||
@@ -1147,7 +1147,7 @@ export default function Review() {
|
||||
onNavigate={(index) => setSelectedImage(filteredFiles[index])}
|
||||
onDelete={handleDelete}
|
||||
onEditDate={handleSingleChangeDate}
|
||||
getPreviewUrl={(item) => api.getReviewPreviewUrl(item.file_path)}
|
||||
getPreviewUrl={(item) => api.getReviewPreviewUrl(item.file_path, item.file_token)}
|
||||
getThumbnailUrl={(item: ReviewFile) => getReviewThumbnailUrl(item)}
|
||||
isVideo={(item) => isVideoFile(item.filename)}
|
||||
renderActions={(item) => (
|
||||
|
||||
@@ -1553,8 +1553,8 @@ export default function VideoDownloader() {
|
||||
handleDeleteVideo(historyItem)
|
||||
}
|
||||
}}
|
||||
getPreviewUrl={(item) => api.getMediaPreviewUrl(item.file_path)}
|
||||
getThumbnailUrl={(item) => api.getMediaThumbnailUrl(item.file_path, item.media_type === 'video' ? 'video' : 'image')}
|
||||
getPreviewUrl={(item) => api.getMediaPreviewUrl(item.file_path, item.file_token)}
|
||||
getThumbnailUrl={(item) => api.getMediaThumbnailUrl(item.file_path, item.media_type === 'video' ? 'video' : 'image', item.file_token)}
|
||||
isVideo={(item) => item.media_type === 'video'}
|
||||
renderActions={(item) => (
|
||||
<button
|
||||
|
||||
@@ -61,7 +61,7 @@ function AttachmentThumbnail({
|
||||
|
||||
const isVideo = attachment.file_type === 'video'
|
||||
const videoUrl = isVideo && attachment.local_path
|
||||
? `/api/paid-content/files/serve?path=${encodeURIComponent(attachment.local_path)}`
|
||||
? attachment.file_token ? `/api/paid-content/files/serve?t=${encodeURIComponent(attachment.file_token)}` : `/api/paid-content/files/serve?path=${encodeURIComponent(attachment.local_path)}`
|
||||
: null
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
|
||||
@@ -150,7 +150,7 @@ function AttachmentThumbnail({
|
||||
const isImage = attachment.file_type === 'image' || isPF
|
||||
const isVideo = attachment.file_type === 'video' && !isPF
|
||||
const fileUrl = attachment.local_path
|
||||
? `/api/paid-content/files/serve?path=${encodeURIComponent(attachment.local_path)}`
|
||||
? attachment.file_token ? `/api/paid-content/files/serve?t=${encodeURIComponent(attachment.file_token)}` : `/api/paid-content/files/serve?path=${encodeURIComponent(attachment.local_path)}`
|
||||
: null
|
||||
const isMissing = attachment.status === 'failed' || attachment.status === 'pending'
|
||||
|
||||
@@ -938,7 +938,7 @@ function PostCard({
|
||||
>
|
||||
{completedAttachments.filter(a => a.file_type === 'audio').map((audio) => {
|
||||
const audioUrl = audio.local_path
|
||||
? `/api/paid-content/files/serve?path=${encodeURIComponent(audio.local_path)}`
|
||||
? audio.file_token ? `/api/paid-content/files/serve?t=${encodeURIComponent(audio.file_token)}` : `/api/paid-content/files/serve?path=${encodeURIComponent(audio.local_path)}`
|
||||
: null
|
||||
const fileSizeMB = audio.file_size ? (audio.file_size / 1024 / 1024).toFixed(1) : null
|
||||
return (
|
||||
|
||||
@@ -38,6 +38,7 @@ interface MediaFile {
|
||||
duration?: number
|
||||
attachment_id?: number
|
||||
downloaded_at?: string
|
||||
file_token?: string
|
||||
}
|
||||
|
||||
// Create a minimal post object from notification data for BundleLightbox
|
||||
@@ -182,7 +183,9 @@ export default function PaidContentNotifications() {
|
||||
if (media.attachment_id) {
|
||||
return `/api/paid-content/files/thumbnail/${media.attachment_id}?size=large&${THUMB_CACHE_V}`
|
||||
}
|
||||
return `/api/paid-content/thumbnail?file_path=${encodeURIComponent(media.file_path)}`
|
||||
return media.file_token
|
||||
? `/api/paid-content/thumbnail?t=${encodeURIComponent(media.file_token)}`
|
||||
: `/api/paid-content/thumbnail?file_path=${encodeURIComponent(media.file_path)}`
|
||||
}
|
||||
|
||||
const openLightbox = (notification: PaidContentNotification, mediaFiles: MediaFile[], index: number) => {
|
||||
|
||||
Reference in New Issue
Block a user