Skip to content

feat(media): move image-URL canonicalization to the ETL, open public reads - #112

Merged
ssavutu merged 1 commit into
mainfrom
patch/media-url-canonicalization-v2
Jul 30, 2026
Merged

feat(media): move image-URL canonicalization to the ETL, open public reads#112
ssavutu merged 1 commit into
mainfrom
patch/media-url-canonicalization-v2

Conversation

@ssavutu

@ssavutu ssavutu commented Jul 30, 2026

Copy link
Copy Markdown
Member

Rebuilds patch/media-url-canonicalization on current main.

That branch predates the media library (#110/#111), which already landed a superset of its upload work — POST /v1/media, MediaUploadResponse, handlers/media.go. Merging it unchanged conflicted in three files and would have read as reverting the media library, so this PR carries over only the two pieces that never landed. The old branch is left untouched on origin and can be closed/deleted.

What lands

1. photo_url is stored and served verbatim. The WordPress ETL now emits final image URLs (wordpress-etl Utils/MediaURL), so normalizePhotoURL and its /proxy/wp-content/ rewriting are removed from both the read and write paths.

2. Article, author, search and taxonomy reads are un-gated. They return the same content the public section routes already serve, and the public site needs them without a session.

Why there is more here than the original branch

Un-gating the list endpoints is not safe on its own. articleQueryFilters applied no publication filter by default, so a public /v1/articles would have allowed anyone to:

  • enumerate unpublished drafts via ?status=draft (or just the default view, which did not exclude them),
  • enumerate soft-deleted articles via ?archived=true.

Similarly /v1/authors?archived=true would have listed soft-deleted authors, and search — already published-only — did not exclude archived rows.

Exposure was excerpt-level, not full article bodies, but unpublished headlines going world-readable is not acceptable for a newsroom, so this PR closes those holes as part of the same change.

How

Anonymous callers are pinned to the published, non-archived view; status and archived are ignored for them. Editors keep the full filter set via a new OptionalAuth middleware, which resolves a session when one is present and lets the request through when it is not — so a single endpoint serves both the public site and the CMS UI. An invalid or expired credential degrades to the public view rather than erroring, so a stale cookie does not break a page.

RequireAuth and OptionalAuth now share one resolveUser helper. RequireAuth behaviour is unchanged, including its distinct 401 messages (unauthorized / invalid token / invalid token claims) and the rule that a present-but-bad session cookie is rejected rather than falling back to bearer.

Reviewer notes

  • This also tightens /v1/sections/{slug}/articles and /v1/subsections/{slug}/articles, which were already public and accepted ?status=draft from anyone. If any consumer relies on that, it needs a session now.
  • GET /v1/articles/{slug} was already public on main and is unchanged here; single-article draft access is pre-existing and out of scope.
  • Swagger regenerated: taxonomy GETs lose BearerAuth, and status/archived document that they are ignored anonymously. Routes and docs agree on paths and auth for all 44 endpoints.

Verification

go build, go vet, and go test ./... all pass. New coverage: anonymous callers cannot widen the listing via any combination of status/archived; editors retain both filters; reads are public with a verifier configured while /v1/activity, /v1/users, /v1/seo/audit and /v1/media stay 401.

🤖 Generated with Claude Code

…reads

Rebuilds patch/media-url-canonicalization on current main. That branch predates
the media library (#110/#111), which already landed a superset of its upload
work — POST /v1/media, MediaUploadResponse, handlers/media.go — so merging it
unchanged would have conflicted in three files and read as reverting the media
library. Only the two pieces that never landed are carried over.

1. photo_url is stored and served verbatim. The WordPress ETL now emits final
   image URLs (wordpress-etl Utils/MediaURL), so normalizePhotoURL and its
   /proxy/wp-content/ rewriting are removed from both the read and write paths.

2. Article, author, search and taxonomy reads are un-gated. They return the
   same content the public section routes already serve, and the public site
   needs them without a session.

Un-gating the LIST endpoints is not safe on its own, so this also closes the
holes it would otherwise open. articleQueryFilters applied no publication
filter by default, so a public /v1/articles would have let anyone enumerate
unpublished drafts (?status=draft) and soft-deleted articles (?archived=true);
/v1/authors?archived=true would have listed soft-deleted authors; and search,
while already published-only, did not exclude archived rows.

Anonymous callers are now pinned to the published, non-archived view and the
status/archived params are ignored for them. Editors keep the full filter set,
identified by a new OptionalAuth middleware: it resolves a session when one is
present and lets the request through when it is not, so one endpoint can serve
both the public site and the CMS UI. An invalid or expired credential degrades
to the public view rather than erroring, so a stale cookie does not break a
page. RequireAuth and OptionalAuth now share one resolveUser helper; RequireAuth
behaviour, including its distinct 401 messages, is unchanged.

Note this also tightens the section/subsection routes, which were already
public and accepted ?status=draft from anyone.

Swagger regenerated: the taxonomy GETs lose their BearerAuth annotation, and
the status/archived params document that they are ignored anonymously. Routes
and docs agree on paths and auth for all 44 endpoints.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ssavutu
ssavutu merged commit 74d8397 into main Jul 30, 2026
5 checks passed
@ssavutu
ssavutu deleted the patch/media-url-canonicalization-v2 branch July 30, 2026 06:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant