Example: CMS Astro - #19
Conversation
There was a problem hiding this comment.
🟡 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/fooand/pages/fooboth 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 andsession_userdoes 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
statusis unconstrained here, so an invalid value passes validation and reaches SQLite's CHECK constraint as an unhandledIntegrityError/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. Checkusers.disabledin 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
statusandpublish_atthrough the generic PATCH path lets a client setstatus='published'without callingpublish(). That leavespublished_atunset 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
Openlink assumes every non-acmesite 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 oneCMS_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
midnighttheme is only rendered as a label here.AppearanceSettingsreceives website themes from/api/cms/themes, and the backend accepts only themes stored in SQLite, so installing Pro cannot select or applymidnightdespite 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
midnighttheme, but selectable website themes come fromGET /api/cms/themes, which reads only the seeded database themes (editorialandstudio), and the theme update endpoint also rejectsmidnight. 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.
No description provided.