feat(media): move image-URL canonicalization to the ETL, open public reads - #112
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebuilds
patch/media-url-canonicalizationon currentmain.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_urlis stored and served verbatim. The WordPress ETL now emits final image URLs (wordpress-etlUtils/MediaURL), sonormalizePhotoURLand 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.
articleQueryFiltersapplied no publication filter by default, so a public/v1/articleswould have allowed anyone to:?status=draft(or just the default view, which did not exclude them),?archived=true.Similarly
/v1/authors?archived=truewould 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;
statusandarchivedare ignored for them. Editors keep the full filter set via a newOptionalAuthmiddleware, 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.RequireAuthandOptionalAuthnow share oneresolveUserhelper.RequireAuthbehaviour 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
/v1/sections/{slug}/articlesand/v1/subsections/{slug}/articles, which were already public and accepted?status=draftfrom anyone. If any consumer relies on that, it needs a session now.GET /v1/articles/{slug}was already public onmainand is unchanged here; single-article draft access is pre-existing and out of scope.BearerAuth, andstatus/archiveddocument that they are ignored anonymously. Routes and docs agree on paths and auth for all 44 endpoints.Verification
go build,go vet, andgo test ./...all pass. New coverage: anonymous callers cannot widen the listing via any combination ofstatus/archived; editors retain both filters; reads are public with a verifier configured while/v1/activity,/v1/users,/v1/seo/auditand/v1/mediastay 401.🤖 Generated with Claude Code