fix: post-session regressions + mount /api/v1/settings + /themes static#523
fix: post-session regressions + mount /api/v1/settings + /themes static#523tayebmokni wants to merge 15 commits into
Conversation
|
Heads up — this PR touches strings that often signal a security disclosure ( If this PR fixes or describes a real vulnerability that has not yet been publicly disclosed, please stop and use the private path:
See If this is a false positive (test fixture, doc update, release notes, etc.) please ignore this comment — the check is advisory only and does not block the PR. Matched files:
|
The standalone Next.js build emitted by `output: 'standalone'` does not auto-copy `.next/static` or `public/` into the standalone tree — the operator must. Without these, every JS chunk and theme asset 404s on the running container. - Copy `.next/static` and `public/` into the standalone bundle. - Restore `rewrites()` so `/api/*` proxies to GONEXT_API_URL (browser- side fetches stay same-origin; the rewrite hops to the docker-internal hostname). - Thread `GONEXT_API_URL` through as a build-arg so `rewrites()` (which evaluates at build time) sees it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
`apiBaseUrl` was exported as a module-load constant. Next.js bundles Client Components at build time, where `typeof window === 'undefined'` evaluates TRUE on the bundler's Node process, so the SERVER branch won and `http://api:8080` (docker-internal) was baked into every browser bundle — producing DNS failures on every client-side fetch. Convert `apiBaseUrl` from a const to a function called at request time. Server Components resolve via `GONEXT_API_URL`; Client Components resolve via `NEXT_PUBLIC_API_URL` (empty string preserved for same-origin + rewrites). Updated 16 call sites across plugins, themes, marketplace, migrate, media, setup. Added api-client.test.ts with three cases: server branch, client branch, empty-string preservation. server-api.ts also imports apiBaseUrl — updated. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
Two parallel directories had Next.js refusing to start:
"different slug names for the same dynamic path ('name' != 'plugin')".
plugins/[name] is the canonical route — it carries the PluginDetailView
and the parent /plugins/[name] detail page. The duplicate
plugins/[plugin]/[slug] subdirectory was rebased in by a stale branch
and made the param disagree at the same path depth.
Content of plugins/[plugin]/[slug]/* was byte-equivalent to
plugins/[name]/[slug]/* except for one destructure rename
(\`{ plugin, slug }\` vs \`{ name, slug }\` at params destructure) and
the param type. Adopted the [name]/[slug] copy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
The API used to emit \`data: null\` for empty pgx result sets, which crashed admin Client Components reading \`items.length\` or \`[...items, ...res.data]\`. Defensive coerce on three surfaces: - media/page.tsx server prefetch (initialData) - MediaGrid.tsx fetchPage (subsequent pages) - FolderTree.tsx listCollections refresh The root cause is fixed at the API layer in a separate commit (Page[T].MarshalJSON nil → []), but keep these guards as belt-and-braces for any non-Page[T] endpoint that returns the same nullable shape. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
…nk fix
Three related fixes around the posts list surface:
1. Admin posts page.tsx now adapts the API envelope (\`{data,
pagination}\`) to the flatter shape PostListClient expects
(\`{posts, nextCursor, total}\`) — and maps \`published_at|updated_at\`
onto the UI's single \`date\` field. (#515 will replace this with
generated api-types.)
2. \`status=any\` is the admin's documented "all statuses" sentinel.
Aliased to empty in both rest/posts/handlers.go and
admin/comments/list.go so the chips don't 400. (#516 tracks the
missing alias in admin/search.)
3. \`postEditHref\` returns \`/posts/\${id}\` (the actual route) — not
\`/posts/\${id}/edit\` (404s). Propagated to two dead-linking sites
in the comments table that were missed in the original rename
(#504). Updated PostListClient.test.tsx assertion. Added a
regression test in CommentListClient.test.tsx.
4. Escaped two stray \`"\` characters in posts/[id]/page.tsx:237
(react/no-unescaped-entities ESLint error).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
…ookie forwarding Three small admin polish fixes: - TopHeader \`View site\` now opens NEXT_PUBLIC_SITE_URL (or localhost:3000 fallback) in a new tab, instead of doing nothing (was \`href=\"/\"\` which routed back to the admin home). - /pages/[id] block-editor button is disabled with a \"coming soon\" label. The previous Link pointed at /pages/[id]/edit which 404s — the dedicated block editor for pages was never built. Track full Pages module work in #506. - /appearance/customizer is a Server Component but its \`fetchActive\` called the client-side \`api.get\` which sends \`credentials: 'include'\` (a no-op on Node.js). Cookies never reached the API → 401 → \"customizer unavailable\". Split: client surface stays in api.ts (for the save/reset mutations from the client form), new api-server.ts exports \`fetchActiveServer\` using serverApiFetch which forwards cookies via next/headers. page.tsx now imports the server variant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
\`json.Marshal(Page[T]{Data: nil})\` emits \`\"data\":null\`. Admin
Client Components then \`.map\` / spread / \`.length\` on null and
crash. Confirmed leaks: /api/v1/admin/media and /api/v1/admin/webhooks
both returned \`data:null\` for empty result sets.
One-line MarshalJSON on Page[T] coerces nil → \`[]T{}\` before
encoding, using an inner-struct alias trick to avoid recursing into
its own MarshalJSON. Unblocks 8+ admin Client Components that were
defensively guarding with \`Array.isArray(x.data) ? x.data : []\` —
those guards remain as belt-and-braces but the API stops emitting
the landmine.
Two tests: nil Data marshals as \`[]\`; non-nil Data round-trips
correctly with cursor pagination preserved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
…496) Sessions.Create was called with \`data: nil\` regardless of the user's role grants. Even though \`gonext init\` writes \`{\"roles\": [\"super_admin\"]}\` into users.meta, the session principal had no roles, so every capability check 403'd — including for the bootstrap super_admin. - UserRecord gets a Roles []string field, projected from users.meta.roles via \`COALESCE(u.meta->'roles', '[]'::jsonb)\` and JSON-decoded in the SQL adapter. - login.UserByIDLookup type added for the TOTP finalize path which only has userID (recovered from the intermediate token), not email. - adapters.go grows userLookupByID(pool) mirroring the by-email variant. - main.go wires UserByID into the login.Deps literal. - service.go threads user.Roles through completeLogin in BOTH paths: the password-only path AND finalizeTOTP's two completion sites. TOTP path re-fetches via UserByID; nil-safe degradation (no roles) if the lookup fails for any reason. Without this, a super_admin who enrolled in TOTP would lose admin permanently on next sign-in. Tests: TestFinalizeTOTP_ThreadsRolesFromUserByID asserts roles flow through the data map. TestFinalizeTOTP_NilUserByIDDoesNotPanic confirms the zero-Deps fallback. Existing tests unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
The worker's \`taskspec.Dispatch\` was buried inside four layers of \`else\` branches under storage.New + NewAbortOrphansSpec + SeedDefaults. If MinIO init or scheduler seed failed, the worker silently registered ZERO asynq task handlers — including the content publisher and GC that the surrounding comments promise. The boot logs still said \"worker started\" so the failure looked like a healthy worker that didn't process tasks. Enqueued tasks just dead-lettered after their retry budget. Restructure to call Dispatch ONCE, unconditionally, after every Register attempt. Failed registers logged warnings — they won't be in taskspec.Default(), so Dispatch is a no-op for them. cronReg declared at outer scope so scheduler.SeedDefaults runs even when storage failed. Double-registration panic (the original bug this code was avoiding) remains impossible: each spec is registered into Default() at most once. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
Every admin Settings sub-page (general, permalinks, reading, writing,
privacy) was calling /api/v1/settings?group=<group> and getting 404
— the settings.Mount call was missing from main.go. The package
existed at packages/go/settings with Registry + PostgresStore +
MemoryStore implementations, but no HTTP layer.
Added apps/api/internal/admin/settings/ with:
- handler.go: Deps + Mount + GET/PATCH handlers + groupPrefix mapper.
GET filters by ?group= against the registered key prefixes
(core.site, core.permalinks, core.reading, core.writing, privacy);
empty groups return {} (200), not 500. PATCH validates against
the registry schema, capability-gates writes through policy.
- handler_test.go: 11 cases. Empty group returns {}. Auth required.
Capability gate denies subscriber writes. Unknown keys 400.
Validation errors 400. Bad JSON 400. Nil-Deps Mount errors.
Wired into main.go via a per-process registry seeded with
RegisterCore + RegisterPrivacy. Store falls back to MemoryStore
when pool is nil (test boot contexts). Routes wrapped with
authmw.RequireSession because the OptionalSession middleware was
reverted in #31.
Verified live: curl /api/v1/settings?group=core.site → 200 with
seeded core.site.{name,tagline,url,default_role}. core.reading +
core.writing return {} (no keys registered yet — fix in a separate
package change).
Known follow-up: packages/go/settings/postgres.go::upsertSQL writes
to a `namespace` column that does not exist in migration 000008.
PATCH will 500 against the live DB until that's resolved. Same bug
in themes/store.go and customizer/store.go. To be filed separately.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
The early-hints provider at apps/api/cmd/server/main.go:431 preloads
/themes/active/style.css, but nothing serves it — every theme asset
404'd. The public site at apps/web fell back to its globals.css
defaults; no theme tokens reached the browser.
apps/api/internal/themes/static/ (new):
- handler.go: Mount(mux, "/themes", Deps{ThemeDir, ActiveResolver,
Logger}). Path layout: /themes/{slug}/{file...}. Sentinel slug
"active" resolves through ActiveResolver. Path traversal rejected
before any FS read. Content-Type per file extension. 1h
Cache-Control. Missing/invalid → 404 (transient DB hiccups on the
resolver shouldn't bring down public CSS).
- handler_test.go: 14 cases. Real file, nested path, HEAD short-
circuit, path-traversal flavors, ..-segment guard, ActiveResolver
wiring, empty resolver → 404, missing file → 404, directory →
404, slug syntax check, Mount Deps validation, isSafeRelPath
units, contentType units.
Wired into main.go:
- Extracted &adminthemes.PgxActiveStore{Pool: pool} to a local var
so it's shared with both the admin themes Mount and the static
resolver closure (single source of truth on which slug is
currently active).
- Mount block after the admin/themes block.
apps/web/src/app/layout.tsx:
- Added <link rel="stylesheet" href="${API_URL}/themes/active/style.css">
inside a new <head> block so the public site actually pulls the
active theme CSS. PUBLIC_API_URL resolves via NEXT_PUBLIC_API_URL
with localhost:8080 fallback.
Verified live:
/themes/gn-hello/style.css → 200, text/css, 4683 bytes, cache 1h
/themes/active/style.css → 200, same body (resolver wired)
/themes/.../etc/passwd → 400 invalid path
/themes/missing/style.css → 404
Follow-up: ActiveResolver hits Postgres on every CSS request. The
options table autoloads so the query is microsecond-cheap today,
but a TTL cache would be appropriate as traffic grows.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
…verride (#517) Working docker-compose.override.yml is generated by each developer locally (postgres port override, admin build-args, aligned auth secrets, custom entrypoints). It is intentionally untracked so each operator can set their own secrets — committing the file would multiply the same dev secrets across every developer's machine and set a bad precedent. - .gitignore: ignore docker-compose.override.yml at repo root. - docker-compose.override.example.yml: template with the exact same structure + placeholder secrets + inline comments explaining each override. - README: a "Local dev setup" subsection documenting the copy step. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
next.config.ts no longer carries \`typescript: { ignoreBuildErrors:
true }\`, so the 12 pre-existing errors that flag was hiding now
fail \`next build\` and CI's \`pnpm lint\`. Address them all here.
- media/types.ts: \`MediaAsset\` was missing four fields the detail
view (\`MediaDetailClient.tsx\`) actually reads — \`hls_url\`,
\`source_url\`, \`is_proxied\`, \`has_extracted_text\`. Added as
optional per the on-wire shape (worker pipeline #476 populates
them on a subset of asset types). Resolves 11/12 errors across
MediaDetailClient.tsx and its test fixture.
- appearance/menus/MenusClient.tsx:157: \`Array.splice\` returns
\`T[]\` so the destructured element typechecks as \`T | undefined\`
under noUncheckedIndexedAccess. Added an explicit guard before
re-inserting so the array can't get a stray undefined.
\`pnpm exec tsc --noEmit\` is now clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
cli/gonext/cmd/audit/audit.go and verify.go both declared the same package-level symbols — ExitOK, ExitFail, ExitUsage, usage, Run, RunOS — because the verify.go landed in PR #492 carrying its own copy of the entry-point boilerplate that was already in audit.go. \`go vet\` rejects this with \"X redeclared in this block\" and CI lint-go has been red on main since the merge. Make audit.go the single owner of the package surface: - Add \`case \"verify\": return runVerify(args[1:], stdout, stderr)\` to the dispatch switch. - Extend the usage banner with the verify subcommand and its env requirement (GONEXT_AUDIT_HMAC_KEY). Strip verify.go down to just \`runVerify\` and its imports — everything else (exit codes, Run, RunOS, usage) is owned by audit.go. go vet ./... clean on cli/gonext after this change. cli/gonext/cmd/ audit unit tests still pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
2ad3b22 to
fe63030
Compare
CI status updateRe-pushed with two fixes that close the CI bar for the code this PR touches:
Now passing ✅
Still failing — but pre-existing on main, not introduced here ❌
Verified on origin/main: all 6 remaining failures reproduce identically without my code. The latest main CI run (`1294d92`) is also red on all of these. Code reviewManual review + agent review both confirm no blockers:
Recommend filing follow-ups for the pre-existing CI failures rather than expanding scope here. Happy to do so if requested. 🤖 PR-management automation |
#524) Three writer SQLs INSERTed into a \`namespace\` column that the live schema doesn't have. Migration 000008_options.up.sql defines: CREATE TABLE options ( key, value, autoload, is_protected, created_at, updated_at, version ); No \`namespace\`. Live INSERT reproduces: ERROR: column "namespace" of relation "options" does not exist So every admin Save through the Settings tab, theme activate, and customizer override Save returned 500 the moment they hit Postgres. GET worked because SELECT never referenced the column. Drop \`namespace\` from all three upserts: - packages/go/settings/postgres.go::upsertSQL - apps/api/internal/admin/themes/store.go::writeActiveSQL - apps/api/internal/admin/customizer/store.go::writeOverridesSQL Also drop the explicit \`updated_at = now()\` — the table default covers INSERT and the options_touch trigger bumps it on UPDATE. The \`namespaceFor\` helper in settings/postgres.go becomes unused but is retained with a comment so the per-plugin uninstall sweep (which will ship a migration adding the column) can pass it back in. The package-level docstring is corrected to match the actual migration schema. Update TestPostgresStore_WriteValidatesAndUpserts: - args[3] namespace assertion removed (panic'd index-out-of-range after the SQL change) - assert arg count == 3 (key, value, autoload) - assert the SQL string itself does NOT contain "namespace" — a regression guard so a future re-introduction blows up in unit tests before it ever hits Postgres again End-to-end verified live: $ curl -X PATCH -d '{"core.site.name":"Verified Live Save"}' \ http://localhost:8080/api/v1/settings HTTP 200 $ psql -c "SELECT value FROM options WHERE key='core.site.name'" "Verified Live Save" Same fix applies to the themes activate + customizer save endpoints even though the bug was identified through #499's PATCH path. Closes #524. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
Summary
13 commits cleaning up regressions introduced during the recent admin-sweep
session, plus two new feature mounts that unblock the admin Settings tab
and let the public site finally load theme CSS.
Driven by the multi-agent review tracked in #522. Each commit maps to
one or more of the GitHub issues filed there.
Reverts / regression fixes
chore(admin): standalone build copies static + public; restore rewrites— closes Ship docker-compose.override.example.yml + .gitignore the working override #517 partialfix(admin): resolve API base URL at runtime, not module-load (#498)— converts the module-load constant into a function so Client Components don't bake the server URL into the browser bundle. Closes REGRESSION: apiBaseUrl in admin api-client may bake server URL into client bundles #498.fix(admin): rename plugin admin route param [plugin] → [name]— Next.js refused to boot. Confirmed identical content modulo the param rename.fix(admin): tolerate nil-slice JSON in media + folder listings— defensive band-aid before the API-layer fix.fix(admin+api): posts list shape adapter + status=any alias + dead-link fix— closes REGRESSION: comments table dead-links to deleted /posts/[id]/edit route #504 + Test broken: PostListClient.test.tsx asserts old /posts/[id]/edit href #519 + Fix react/no-unescaped-entities ESLint errors at posts/[id]/page.tsx:237 #520 + a sliver of Posts list adapter encodes fields the API doesn't emit — empty Author cells #515.fix(admin): View site link, disable broken pages editor, customizer cookie forwarding— Admin Pages module is non-functional — SEED_PAGES list, console.log save, disabled editor #506 (partial), Ship docker-compose.override.example.yml + .gitignore the working override #517 (View site), customizer 401.fix(api): coerce nil Data to empty slice in router.Page[T] JSON— Closes BUG: nil-slice JSON in router.Page[T] — admin Client Components crash on empty lists #505. Kills the data:null landmine for allPage[T]consumers in one place.fix(api): seed user roles into session on login, incl. TOTP finalize— Closes REGRESSION: TOTP finalize path drops user roles → super_admin loses admin on re-login #496. Without this any super_admin who enrolls in TOTP loses admin permanently on next sign-in.fix(worker): single Dispatch regardless of storage init— Closes REGRESSION: Worker Dispatch nested in storage-init success — MinIO failure disables ALL handlers #497. Worker no longer silently registers zero handlers when MinIO is misconfigured.Features (P0 architectural gaps)
feat(api): mount /api/v1/settings registry endpoint— Closes Mount /api/v1/settings registry — admin Settings tab is a UI mockup until this lands #499. Unblocks every admin Settings sub-page (general, permalinks, reading, writing, privacy).feat(api+web): mount /themes/{slug}/{file} static handler— Closes Mount /themes/active/style.css static handler — public site has zero theme CSS #501. Public site can finally load active theme CSS. apps/web layout.tsx pulls /themes/active/style.css.Build / dev hygiene
chore(dev): docker-compose.override.example.yml + gitignore working override— Closes Ship docker-compose.override.example.yml + .gitignore the working override #517. Working override carries dev secrets; ship a template instead.fix(admin): resolve TS errors hidden by ignoreBuildErrors flag— Closes TypeScript debt: 12 errors silenced by ignoreBuildErrors flag in admin next.config #518. The previous session stuckignoreBuildErrors: truein next.config.ts. Stripped the flag this PR; this commit resolves the 12 errors that surfaced (4 missing fields on MediaAsset + 1 array-index narrowing miss).NOT in this PR (deliberately deferred)
if !okauth gates #495 OptionalSession middleware was REVERTED in commitchore(admin): standalone build...(the next.config.ts change in that commit removes ignoreBuildErrors; the same commit area in another file removed the OptionalSession line from main.go's middleware chain). The privilege gate regression is gone.Verification
go build ./...clean (api + worker)go test ./internal/auth/login/... ./internal/rest/posts/... ./internal/admin/comments/... ./internal/admin/settings/... ./internal/themes/static/... ./cmd/server/...all passpnpm exec tsc --noEmit(admin) clean — no errors after stripping ignoreBuildErrors/api/v1/settings?group=core.site→ 200 with seeded defaults (was 404)/themes/active/style.css→ 200 (was 404)/themes/../etc/passwd→ 400 invalid pathKnown follow-ups surfaced by this work
packages/go/settings/postgres.go::upsertSQLwrites anamespacecolumn that doesn't exist in migration 000008. Same bug inthemes/store.goandcustomizer/store.go. GET works fine; PATCH explodes. Will file a follow-up issue.core.reading/core.writingsetting groups have no registered keys yet — empty{}response is fine but the admin form renders blank. Extendpackages/go/settings/core.goin a follow-up./themes/statichits Postgres per CSS request — microsecond cost today (single-row autoloadedoptions), but a TTL cache would be appropriate as traffic grows.Test plan
go build ./...in apps/api + apps/workergo test ./...in apps/api + apps/workerpnpm --filter @gonext/admin run buildsucceeds (no TS errors, no ESLint errors)pnpm --filter @gonext/admin test --run— 600+ passcurl /themes/active/style.cssreturns 200 with text/csslocalhost:3000HTML head includes<link rel="stylesheet" href=".../themes/active/style.css">🤖 Generated with Claude Code