the preview iframe was rendering Officer inside Officer

Two mistakes, and the second would have bitten immediately after the first was fixed.

The iframe pointed at /fofos-admin/preview/layout. But /fofos-admin is a FRONTEND route — the
single-page app owns it, and anything under it no router claims is answered with the app's own
index.html. So the iframe was served Officer. The plugin's API is mounted at /api/fofos-admin, which
every other call already knew: useClient defaults its base to /api, and thumbnail.ts spells the full
path out for <img> tags that cannot use it.

And an iframe cannot send an Authorization header. Neither can the stylesheet, the fonts or the images
the page goes on to request — those are all Officer requests now, so all of them need ?token=, the
same reason every <img> in this plugin carries one. The token comes off the iframe's own query string
and is propagated into every URL the rewrite touches, with the separator decided per URL because many
already carry the site's ?v= stamp.

It is stripped before the request reaches the site. The token is Officer's and the site has never
heard of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 02:01:18 +01:00
co-authored by Claude Opus 5
parent b56c8375c3
commit 6f8dc83984
3 changed files with 77 additions and 30 deletions
+15
View File
@@ -48,3 +48,18 @@ export const originalUrl = (path: string, width: number): string => {
const encoded = path.split('/').map(encodeURIComponent).join('/');
return `/api/fofos-admin/original/${encoded}?width=${Math.round(width)}&token=${encodeURIComponent(token)}`;
};
/**
* The site's own page, proxied through Officer, for the layout preview's iframe.
*
* Here rather than in the panel for the reason this module exists at all: it is a URL shape carrying a
* token, and a second copy of one is a second place to be wrong about it.
*
* `/api/fofos-admin/...` and NOT `/fofos-admin/...`. The second is the single-page app's own route, so
* anything under it that no router claims is answered with Officer's index.html — an iframe pointed
* there renders Officer inside Officer, which is precisely what it did.
*/
export const previewUrl = (path: string): string => {
const token = getHeaders()['Authorization']?.replace('Bearer ', '') ?? '';
return `/api/fofos-admin/preview/${path.replace(/^\/+/, '')}?token=${encodeURIComponent(token)}`;
};