thumbnails need the token in the query string

An <img> cannot send an Authorization header, which is why user-middleware and
the permission gate both already accept `?token=` — it is exactly what the file
browser's own thumbnails do.

Built without it, every picture 401'd while the layout looked perfectly correct:
the space was reserved, the rows were right, and nothing appeared. The route was
fine and had been tested with a bearer token, which is precisely the case the
browser cannot reproduce.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 21:26:19 +01:00
co-authored by Claude Opus 5
parent 9fd5f1b3ab
commit 9c62777f5b
+15 -1
View File
@@ -4,6 +4,7 @@ import { ArrowDown, ArrowUp, FolderOpen, Images, Loader2, Plus, Trash2 } from 'l
import { useClient } from 'hooks/useClient';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { getHeaders } from 'hooks/useClient';
import { readableError } from '../errors';
import { MediaPicker } from '../MediaPicker';
@@ -27,6 +28,19 @@ import { MediaPicker } from '../MediaPicker';
type Item = { id: number; url: string; alt: string | null; sort_order: number };
type Gallery = { id: number; name: string; slug: string; items: Item[] };
/**
* A thumbnail URL, authenticated by `?token=`.
*
* An `<img>` cannot send an Authorization header, which is why `user-middleware` and the permission
* gate both accept the token in the query string — the same thing the file browser's own thumbnails do.
* Built without it first, and every picture 401'd while the layout looked perfectly correct.
*/
const thumbUrl = (path: string): string => {
const token = getHeaders()['Authorization']?.replace('Bearer ', '') ?? '';
const encoded = path.split('/').map(encodeURIComponent).join('/');
return `/api/fofos-admin/derived/${encoded}/320.webp?token=${encodeURIComponent(token)}`;
};
export const GalleriesPanel = () => {
const client = useClient();
const clientRef = useRef(client);
@@ -203,7 +217,7 @@ export const GalleriesPanel = () => {
and still sharp at this size on a retina screen.
*/}
<img
src={`/api/fofos-admin/derived/${item.url.split('/').map(encodeURIComponent).join('/')}/320.webp`}
src={thumbUrl(item.url)}
alt=""
loading="lazy"
className="size-20 shrink-0 rounded bg-muted object-cover"