Skip to content

Example: CMS Astro - #19

Merged
echarles merged 10 commits into
mainfrom
example/astro
Sep 15, 2026
Merged

echarles merged 10 commits into
mainfrom
example/astro

Conversation

@echarles

Copy link
Copy Markdown
Member

No description provided.

Copilot AI lite review requested due to automatic review settings September 15, 2026 07:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical authorization, build, URL-safety, role-invariant, and payment-signature findings remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds a packaged Astro/SQLite CMS example with Core, Pro, and x402 extensions, an admin UI, tests, and documentation.

Changes:

  • Adds the FastAPI/SQLite CMS backend and Astro frontend.
  • Adds optional Pro and x402 extension packages.
  • Adds build commands, tests, and usage documentation.
File summaries
File Change Final review notes
Makefile Adds CMS Astro lifecycle commands. —
examples/cms-astro/x402/share/datalayer/reactor/extensions/cms-astro-x402/index.js Defines x402 frontend contributions. —
examples/cms-astro/x402/pyproject.toml Packages the x402 extension. —
examples/cms-astro/x402/cms_astro_x402/__init__.py Implements demo payment verification. Critical (3 votes): signatures do not bind payment requirements.
examples/cms-astro/tests/test_x402.py Tests x402 behavior. —
examples/cms-astro/tests/test_cms.py Tests CMS API behavior. —
examples/cms-astro/README.md Documents the example. —
examples/cms-astro/pro/share/datalayer/reactor/extensions/cms-astro-pro/index.js Defines Pro frontend contributions. —
examples/cms-astro/pro/pyproject.toml Packages the Pro extension. —
examples/cms-astro/pro/cms_astro_pro/__init__.py Defines Pro backend contributions. Moderate (1 vote): the advertised theme is not integrated into selectable themes.
examples/cms-astro/core/share/datalayer/reactor/extensions/cms-astro-core/index.js Defines the Core frontend plugin. —
examples/cms-astro/core/pyproject.toml Defines Core packaging and scripts. —
examples/cms-astro/core/frontend/tsconfig.json Configures TypeScript checking. —
examples/cms-astro/core/frontend/src/styles.css Styles the public and admin interfaces. —
examples/cms-astro/core/frontend/src/pages/premium/[slug].astro Adds the paid-content route. —
examples/cms-astro/core/frontend/src/pages/posts/[slug].astro Renders individual posts. Critical (1 vote): unresolved missing CMS client import prevents checking and builds.
examples/cms-astro/core/frontend/src/pages/index.astro Adds the homepage. —
examples/cms-astro/core/frontend/src/pages/blog.astro Adds the blog listing. —
examples/cms-astro/core/frontend/src/pages/[cms]/admin.astro Mounts the admin application. —
examples/cms-astro/core/frontend/src/pages/[cms]/[section].astro Handles CMS sections. —
examples/cms-astro/core/frontend/src/middleware.ts Adds x402 request middleware integration. Critical (2 votes): unresolved missing shared-module imports prevent builds.
examples/cms-astro/core/frontend/src/live.config.ts Declares live collections. Critical (2 votes): missing loader import; critical (3 votes): file is not a recognized Astro content configuration.
examples/cms-astro/core/frontend/src/layouts/Base.astro Defines the public site layout. —
examples/cms-astro/core/frontend/src/env.d.ts Declares Astro locals types. —
examples/cms-astro/core/frontend/src/components/AppearanceSettings.tsx Implements appearance controls. —
examples/cms-astro/core/frontend/src/components/AdminApp.tsx Implements the CMS administration UI. Three moderate findings (1 vote each): unsupported site links, inactive contribution points, and unavailable Pro theme selection.
examples/cms-astro/core/frontend/package.json Declares frontend dependencies. —
examples/cms-astro/core/frontend/astro.config.mjs Configures Astro SSR. —
examples/cms-astro/core/cms_astro_core/store.py Provides SQLite persistence and sessions. Two moderate findings (1 vote each): disabled sessions remain valid; generic PATCH bypasses publication transition logic.
examples/cms-astro/core/cms_astro_core/site.py Launches the embedded Astro server. —
examples/cms-astro/core/cms_astro_core/seed.py Seeds demo data. —
examples/cms-astro/core/cms_astro_core/plugin.py Defines Core backend contributions. —
examples/cms-astro/core/cms_astro_core/host.py Configures the Reactor host and authentication. —
examples/cms-astro/core/cms_astro_core/app.py Exposes the FastAPI app. —
examples/cms-astro/core/cms_astro_core/api.py Implements CMS and public APIs. Seven findings: critical (3 votes) cross-site/author snapshot access; moderate (1 vote) slug ambiguity; critical (1 vote) search authorization; moderate (1 vote) disabled sessions; moderate (1 vote) invalid status handling; critical (1 vote) unsafe menu URLs; critical (1 vote) last-admin demotion.
examples/cms-astro/core/cms_astro_core/__init__.py Registers the Core extension. —
examples/cms-astro/.gitignore Ignores generated artifacts. —
docs/docs/examples/index.md Links the new example. —
docs/docs/examples/cms-astro/users-and-authoring.md Documents users and authoring. —
docs/docs/examples/cms-astro/index.md Introduces the CMS example. —
docs/docs/examples/cms-astro/getting-started.md Documents setup and execution. —
docs/docs/examples/cms-astro/extensions.md Documents Pro and x402 extensions. —
docs/docs/examples/cms-astro/architecture.md Documents system architecture. —
docs/docs/examples/cms-astro/_category_.json Adds documentation navigation. —
Review details

Suppressed comments (9)

examples/cms-astro/core/cms_astro_core/api.py:497

  • The schema permits the same slug in different collections (UNIQUE(site_id, collection_id, slug)), but this endpoint filters only by site and slug. If /posts/foo and /pages/foo both exist, a live entry request can return the wrong collection's published record. Include the collection in this lookup/API contract or enforce site-wide slug uniqueness.
        row = db.execute("""SELECT e.id,e.slug,e.title,e.excerpt,e.body,e.data,e.published_at,c.slug collection
          FROM entries e JOIN sites s ON s.id=e.site_id JOIN collections c ON c.id=e.collection_id
          WHERE s.slug=? AND e.slug=? AND e.status='published'""", (site_slug, slug)).fetchone()

examples/cms-astro/core/cms_astro_core/api.py:255

  • Disabling a user only updates users.disabled; existing bearer sessions remain valid because the authentication middleware accepts any unexpired token and session_user does not check this flag. A disabled account can therefore continue using CMS APIs until its token expires. Revoke that user's sessions when disabling (and when changing the password).
        changes = payload.model_dump(exclude_none=True)
        if "password" in changes:
            changes["password_hash"] = store(request).password_hash(changes.pop("password"))
        if changes:
            assignments = ",".join(f"{column}=?" for column in changes)
            try:
                db.execute(f"UPDATE users SET {assignments} WHERE id=?", (*[int(value) if isinstance(value, bool) else value for value in changes.values()], member_id))
            except sqlite3.IntegrityError as error:
                raise HTTPException(409, "username or email is already in use") from error

examples/cms-astro/core/cms_astro_core/api.py:32

  • status is unconstrained here, so an invalid value passes validation and reaches SQLite's CHECK constraint as an unhandled IntegrityError/HTTP 500. Constrain it to the five supported statuses and return a validation error instead.
    status: str | None = None

examples/cms-astro/core/cms_astro_core/store.py:252

  • Disabling a user only changes users.disabled, while this lookup accepts any unexpired session for that user. An existing bearer token therefore continues to pass the host middleware and access site APIs for up to 12 hours after an administrator disables the account. Check users.disabled in this query (or revoke all sessions when disabling).
            row = db.execute("SELECT user_id FROM sessions WHERE token_hash=? AND expires_at>?", (hashlib.sha256(token.encode()).hexdigest(), now())).fetchone()

examples/cms-astro/core/cms_astro_core/store.py:302

  • Allowing status and publish_at through the generic PATCH path lets a client set status='published' without calling publish(). That leaves published_at unset and bypasses the dedicated publication transition, so public ordering and the documented publish workflow become inconsistent; restrict these fields or apply the same transition logic here.
            allowed = {"title", "slug", "excerpt", "body", "data", "status", "publish_at"}

examples/cms-astro/core/frontend/src/components/AdminApp.tsx:75

  • The Open link assumes every non-acme site is served at /${site.slug}, but this frontend defines no site-prefixed routes; it only has /, /blog, and /posts/[slug], and the live configuration reads one CMS_SITE. Creating a second site therefore produces a dead link instead of a public site. Add site-aware routes/configuration or do not advertise an open link for unsupported sites.
 {tab==='Sites'&&isAdmin&&<Box className="content-grid"><Box className="site-list-column"><Box className="panel"><Box sx={{display:'flex',justifyContent:'space-between',alignItems:'center'}}><Heading as="h3" sx={{fontSize:2}}>Websites</Heading><Label>{sites.length} sites</Label></Box>{sites.map(site=><Box key={site.id} className={`site-row${site.id===siteId?' selected':''}`}><Box><Text sx={{fontWeight:'bold'}}>{site.name}</Text><Text as="p" sx={{fontSize:0,color:'fg.muted',m:0}}>{site.tagline||'No tagline'}</Text><Text as="p" sx={{fontSize:0,color:'fg.muted',m:0}}>{site.member_count} users · {site.entry_count} entries · {site.theme_slug}</Text></Box><Box className="row-actions"><Label>{site.role}</Label><Button size="small" onClick={()=>{setSiteId(site.id);setSiteSection('general')}}>{site.id===siteId?'Editing':'Manage'}</Button><a className="site-link" href={site.slug==='acme'?'/':`/${site.slug}`} target="_blank" rel="noreferrer">Open ↗</a></Box></Box>)}</Box><Box className="panel"><Heading as="h3" sx={{fontSize:2}}>Create website</Heading><form className="form-grid" onSubmit={createSite}><FormControl required><FormControl.Label>Name</FormControl.Label><TextInput block value={siteName} onChange={e=>setSiteName(e.target.value)}/></FormControl><FormControl required><FormControl.Label>Slug</FormControl.Label><TextInput block value={siteSlug} onChange={e=>setSiteSlug(e.target.value)}/></FormControl><Button type="submit" variant="primary">Create website</Button></form></Box></Box><Box className="panel site-editor"><Box className="site-editor-heading"><Box><Heading as="h3" sx={{fontSize:2}}>Edit {activeSite?.name}</Heading><Text as="p" sx={{fontSize:1,color:'fg.muted',m:0}}>Manage this website and how it looks to visitors.</Text></Box><SegmentedControl aria-label="Website settings" onChange={index=>setSiteSection(index===0?'general':'appearance')}><SegmentedControl.Button selected={siteSection==='general'}>General</SegmentedControl.Button><SegmentedControl.Button selected={siteSection==='appearance'}>Appearance</SegmentedControl.Button></SegmentedControl></Box>{siteSection==='general'?<form className="form-grid site-general-form" onSubmit={saveSite}><FormControl required><FormControl.Label>Name</FormControl.Label><TextInput block value={editSiteName} onChange={e=>setEditSiteName(e.target.value)}/></FormControl><FormControl required><FormControl.Label>Slug</FormControl.Label><TextInput block value={editSiteSlug} onChange={e=>setEditSiteSlug(e.target.value)}/><FormControl.Caption>Lowercase letters, numbers, and hyphens.</FormControl.Caption></FormControl><FormControl><FormControl.Label>Tagline</FormControl.Label><Textarea block value={editSiteTagline} onChange={e=>setEditSiteTagline(e.target.value)}/></FormControl><Button type="submit" variant="primary">Save website</Button></form>:<AppearanceSettings websiteThemes={themes} activeWebsiteTheme={activeSite?.theme_slug} onActivateWebsiteTheme={theme=>void activateTheme(theme)}/>}</Box></Box>}

examples/cms-astro/core/frontend/src/components/AdminApp.tsx:77

  • The host subscribes to editor actions and dashboard widgets but only renders their counts here; it never renders an action/widget or invokes backendAction. Consequently the Pro SEO/scheduling and x402 configure/status contributions have no usable effect after installation. Render and invoke these contribution points, or do not expose them as active UI contributions.
 {tab==='Extensions'&&isAdmin&&<Box className="cards">{plugins.map(plugin=><Box className="panel" key={plugin.name}><Heading as="h3" sx={{fontSize:2}}>{plugin.emoji} {plugin.displayName}</Heading><Text as="p">{plugin.description}</Text><Label variant={plugin.activated?'success':'secondary'}>{plugin.activated?'active':'available'}</Label></Box>)}<Box className="panel"><Text>{actions.length} editor actions · {widgets.length} dashboard widgets</Text></Box></Box>}

examples/cms-astro/core/frontend/src/components/AdminApp.tsx:76

  • The Pro midnight theme is only rendered as a label here. AppearanceSettings receives website themes from /api/cms/themes, and the backend accepts only themes stored in SQLite, so installing Pro cannot select or apply midnight despite the extension and documentation advertising it. Merge extension themes into the selectable/persisted theme set.
 {tab==='Appearance'&&isAdmin&&<AppearanceSettings websiteThemes={themes} activeWebsiteTheme={activeSite?.theme_slug} onActivateWebsiteTheme={theme=>void activateTheme(theme)}/>} {tab==='Appearance'&&extensionThemes.length>0&&<Box>{extensionThemes.map(item=><Label key={item.id}>{item.value.label??item.value.slug}</Label>)}</Box>}

examples/cms-astro/pro/cms_astro_pro/init.py:18

  • This advertises the Pro midnight theme, but selectable website themes come from GET /api/cms/themes, which reads only the seeded database themes (editorial and studio), and the theme update endpoint also rejects midnight. Installing Pro therefore cannot activate the theme contributed here. Integrate extension theme contributions into the theme API/storage or remove this contribution from the advertised UI.
  • Files reviewed: 43/44 changed files
  • Comments generated: 9
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread examples/cms-astro/core/cms_astro_core/api.py Outdated
Comment thread examples/cms-astro/core/cms_astro_core/api.py
Comment thread examples/cms-astro/core/cms_astro_core/api.py Outdated
Comment thread examples/cms-astro/core/cms_astro_core/api.py
Comment thread examples/cms-astro/core/frontend/src/live.config.ts
Comment thread examples/cms-astro/core/frontend/src/live.config.ts
Comment thread examples/cms-astro/core/frontend/src/middleware.ts
Comment thread examples/cms-astro/core/frontend/src/pages/posts/[slug].astro
Comment thread examples/cms-astro/x402/cms_astro_x402/__init__.py
@echarles
echarles merged commit 535976f into main Sep 15, 2026
5 checks passed
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.

2 participants