feat: Wave 3 — 4 large features (plugins CRUD, PAT, customizer preview, Pages real)#529
Open
tayebmokni wants to merge 4 commits into
Open
feat: Wave 3 — 4 large features (plugins CRUD, PAT, customizer preview, Pages real)#529tayebmokni wants to merge 4 commits into
tayebmokni wants to merge 4 commits into
Conversation
The admin Plugins page was showing \"Couldn't load plugins from the
API (HTTP 404)\" — the lifecycle Manager existed but no HTTP layer
sat on top of it.
New package: \`apps/api/internal/admin/plugins/\`
Routes (all RequireSession-wrapped at mount):
- GET /api/v1/plugins — list (router.Page envelope)
- GET /api/v1/plugins/{name} — detail w/ manifestRaw
- POST /api/v1/plugins/install — multipart upload, 80 MiB cap, cap:install_plugins
- POST /api/v1/plugins/{name}/activate — cap:activate_plugins
- POST /api/v1/plugins/{name}/deactivate — cap:activate_plugins
- DELETE /api/v1/plugins/{name} — cap:install_plugins (uninstall)
HTTP status mapping: 401 (unauth), 403 (no-cap), 404 (unknown),
409 (invalid state transition).
Install streams the multipart \"bundle\" part straight into
\`Manager.Install\` — no temp file. Audit events fire from the
lifecycle Manager (plugin.installed/activated/deactivated/uninstalled).
14 tests: empty list shape, list projection, 401 unauthed list, 404
unknown get, activate auth/cap/success/conflict, deactivate, uninstall,
install content-type + cap gates.
Wired in main.go: dedicated lifecycle.NewManager against
NewPostgresStorage(pool) (memory fallback for pool-less tests),
wrapped in authmw.RequireSession. Mount registers both
\`/api/v1/plugins\` and \`/api/v1/plugins/\` so the literal-list and
\`{name}\` subtree both hit the gated handler.
Verified live:
curl /api/v1/plugins → 200 {\"data\":[],\"pagination\":{...}}
Note: the \`plugins\` table is missing from migrations.
PostgresStorage.List handles 42P01 gracefully (returns empty),
but Get/Install/UpdateState would 500. Migration follows in a
separate commit.
Closes #500.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
Admin Settings → Tokens page was calling \`/api/v1/me/tokens\` and
getting 404. PAT issuance is table-stakes for any CI / automation
integration against a remote GoNext install.
New package: \`apps/api/internal/auth/pat/\`
Routes (RequireSession-wrapped):
- GET /api/v1/me/tokens — list (no secrets, only prefix)
- POST /api/v1/me/tokens — create + return raw secret ONCE
- DELETE /api/v1/me/tokens/{id} — revoke
Token format: \`gn_pat_<32-byte-base64>\` (24 raw bytes from crypto/
rand, base64.RawURLEncoding). Hashed via \`packages/go/auth/password.
Hash\` with the configured \`GONEXT_AUTH_PEPPER\`. Prefix is first 8
chars of the secret tail for display.
Owner isolation: every query scoped by caller's UserID from
policy.FromContext. \"Wrong owner\" and \"not found\" both return 404
— no existence oracle for cross-user enumeration.
Migration: \`000026_personal_access_tokens.up.sql\` already shipped.
Audit: emits \`auth.pat.created\` / \`auth.pat.revoked\`.
7 tests against testcontainers Postgres: empty list, create+raw-
token-once, revoke removes row, revoke other-user returns 404, 401
anonymous, empty-name rejection, audit emission.
Verified live: full create/list/revoke round-trip works against the
running stack.
Note: an older \`apps/api/internal/admin/tokens\` package exists with
similar surface but different token format. It's currently unmounted;
choice of which to keep + repointing admin/users/{id}/tokens at one
of them is a follow-up.
Closes #511.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
Two endpoints the admin Customizer + Site Editor pages were rendering
against — both 404'd previously.
### POST /api/v1/admin/customizer/preview
Extends existing internal/admin/customizer/handler.go. Body shape
permissive — accepts both \`{settings: {...}}\` (direct override) and
\`{overrides: {...}}\` (envelope) since the admin client posts both.
Returns \`{themeSlug, cssCustomProperties}\` after merging override
onto the active theme manifest. Empty body returns base CSS.
Permissive JSON decode (no DisallowUnknownFields) since preview is
keystroke-grained; strict validation lives on PUT. Gated by
theme.customize.
6 new test cases (base CSS, override applied, envelope variant,
forbidden, unauth, invalid JSON).
### GET/PUT /api/v1/admin/site-editor/parts(/{name})
New package: internal/admin/siteeditor/. File-based — reads/writes
\`themes/{active}/parts/{name}.html\` for theme template parts the
operator edits in-browser.
Part shape: {name, area: header|footer|general, title, content}.
Security: validates name against active theme.json templateParts
declaration (undeclared name → 404 — no arbitrary file writes
allowed). Path-traversal guard rejects /, \\, .., control chars,
uppercase → 400. Atomic write via temp file + rename.
Gated by \`manage_themes\` OR \`theme.edit_parts\` (either grants).
17 tests covering list/get/put, undeclared name 404, traversal flavors
400, unauth 401, forbidden 403, missing-file empty content, MkdirAll
for fresh installs.
### main.go wiring
Both endpoints now go through sub-mux + RequireSession wrap (was
missing on the existing customizer mount — that's why
/api/v1/admin/customizer/active was previously returning 401).
Verified live:
GET /api/v1/admin/site-editor/parts → 200 + header + footer
POST /api/v1/admin/customizer/preview {} → 200 base CSS
POST /api/v1/admin/customizer/preview {paper:#112233} → 200 w/ color flipped
Closes #512.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
Pages list was hardcoded \`SEED_PAGES\` and the detail save handler was
\`console.log\`. Replace both with real CRUD against the existing
/api/v1/posts surface.
### API: post_type filter
apps/api/internal/rest/posts/model.go grows a \`PostType\` field on
ListFilter. handlers.go's parseListQuery accepts \`?post_type=post|page\`
with closed-set validation (anything else → 400 invalid_post_type).
list() resolves the discriminator at request time: filter override
wins over the mount's default PostType.
This lets the single /api/v1/posts mount serve both Posts list and
Pages list — no second mount required.
### Admin Pages list
apps/admin/src/app/(authenticated)/pages/page.tsx:
- Drops SEED_PAGES entirely.
- Server Component fetching \`/api/v1/posts?post_type=page&status=any\`
via serverApiFetch (cookie-forwarding).
- Exported \`adaptApiPage\` and \`ApiPagePost\` types — testable in
isolation.
- Status normalization (UI \`published\` ↔ API \`publish\`).
- Updated-at fallback chain: updated_at → published_at → created_at.
- Empty/error states stay inside the table card so layout doesn't
shift.
### Admin Pages detail
apps/admin/src/app/(authenticated)/pages/[id]/page.tsx:
- Replaced console.log save with \`api.patch('/api/v1/posts/{id}',
{title, slug, status})\`.
- UI status maps at the boundary (\`publish\` → \`published\` on the wire).
- Try/catch with ApiError branch surfaces an amber error banner.
- Success indicator: 3s "Saved" pip next to the Save button.
- Block editor button stays disabled (separate issue).
### Tests
22 new tests across pages/page + pages/[id]/page covering: adapter
status normalization, updated-at fallback, untitled placeholder,
slug passthrough, PATCH URL + body shape, status mapping, ApiError
path, generic Error path. 638/638 admin tests pass.
API: TestList_FilterByPostType + TestList_FilterByPostType_InvalidRejected.
### Known follow-up
The /pages/new flow built in PR #528 POSTs {post_type:'page'} in the
body but the create handler is hard-coded to PostTypePost (ignores
body). New pages today insert as post_type='post' and disappear from
the pages list. Body-override on create is a small follow-up.
Closes #506 (list + save). Create-side gap tracked separately.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
This was referenced May 28, 2026
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.
Stacked on PR #528 (Wave 2) → PR #527 (Wave 1) → PR #523 (post-session cleanup).
Closes 4 issues from the #522 tracker
4 commits
feat(api): mount /api/v1/plugins lifecycle CRUD (#500)feat(auth): mount /api/v1/me/tokens — Personal Access Tokens (#511)feat(api): customizer preview + site-editor parts endpoints (#512)feat(admin+api): wire admin Pages module to real API (#506)— adds post_type filter to posts RESTVerification
go build ./...cleanpnpm exec tsc --noEmit0 errorsKnown follow-ups
pluginstable. Today only List handles 42P01 gracefully; Get/Install/UpdateState would 500 on a fresh DB. Filing separately.apps/api/internal/auth/pat/. An olderapps/api/internal/admin/tokenspackage exists with different token format — currently unmounted. Pick one + repoint admin/users/{id}/tokens./pages/newPOSTs{post_type:'page'}but the create handler ignores the body field (hard-coded to PostTypePost). Small body-override follow-up.What's next
Wave 4: #514 generated api-types from OpenAPI, #521 regression tests backfill. Then close #522 tracker.
🤖 Generated with Claude Code