Core half of the public demo (docs/website/DEMO_MODE.md §10 step 1, website#222).
Nothing here runs unless DEMO_MODE=true.
- server/src/demo/: the lock at /api/v1, ahead of every router. It covers every
Tier A row of §3.3, the shared-login conditions, POST /auth/register (DM16),
DELETE /auth/me/sessions/:id on a demo account (DM17), and the PUT
/admin/settings keys that reach a Tier A lock. Any multipart request is refused
app-wide. noindex and robots.txt. A condition that cannot decide refuses.
- Loader: demoLocks in module.json (writes only, under the module's own mounts),
ctx.demoMode, and api.onDemoSeed (DM15). MODULE_API 1.18.0 on both halves.
- GET /public/settings carries demo: { resetsAt, logins } on the demo only.
- npm run seed:demo -- <fixture>: upserts the four accounts, sets the site live
with registration disabled, seeds content on the first run only, calls each
module's onDemoSeed, and stamps demo_state. It resolves $env: strings and
refuses to run off the demo.
- Client: the banner, one DEMO_LOCKED toast fed from the request primitive (so
module screens get it too), and the published logins on both sign-in pages.
Dashboard and register say the demo message instead of blaming a role.
- OpenAPI: DemoLocked schema, the demo block, and a note that any write may
answer DEMO_LOCKED on the demo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
52 lines
2.0 KiB
JavaScript
52 lines
2.0 KiB
JavaScript
import { useSite } from '../contexts/SiteContext.jsx'
|
|
|
|
// The published sign-ins, on the sign-in pages of the public demo
|
|
// (DEMO_MODE.md §4). They are public by design — printed here and on
|
|
// runicgateway.com/beta — so "Use" fills the form rather than making anyone
|
|
// copy a password. Renders nothing off the demo.
|
|
export default function DemoLogins({ onPick }) {
|
|
const { settings } = useSite()
|
|
const logins = settings.demo?.logins || []
|
|
if (logins.length === 0) return null
|
|
|
|
return (
|
|
<section
|
|
aria-label="Demo sign-ins"
|
|
className="sans"
|
|
style={{
|
|
marginTop: 18,
|
|
padding: '14px 16px',
|
|
border: '1px solid var(--line)',
|
|
borderRadius: 10,
|
|
fontSize: '0.84rem',
|
|
// Opaque: the admin sign-in page draws it over the hero image.
|
|
background: 'var(--bg-deep)',
|
|
}}
|
|
>
|
|
<p style={{ margin: '0 0 10px', color: 'var(--head)' }}>
|
|
<strong>Demo sign-ins</strong> <span style={{ color: 'var(--dim)' }}>— shared by everyone, reset every wipe</span>
|
|
</p>
|
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
<tbody>
|
|
{logins.map((l) => (
|
|
<tr key={l.username}>
|
|
<td style={{ padding: '5px 0', verticalAlign: 'middle' }}>
|
|
<div style={{ color: 'var(--head)', whiteSpace: 'nowrap' }}>{l.username}</div>
|
|
<div style={{ color: 'var(--dim)', fontFamily: 'monospace', fontSize: '0.78rem', wordBreak: 'break-all' }}>
|
|
{l.password}
|
|
</div>
|
|
</td>
|
|
<td style={{ padding: '5px 8px', color: 'var(--dim)', verticalAlign: 'middle' }}>{l.role}</td>
|
|
<td style={{ padding: '5px 0', textAlign: 'right', verticalAlign: 'middle' }}>
|
|
<button type="button" className="btn btn-ghost" style={{ padding: '3px 10px', fontSize: '0.8rem' }} onClick={() => onPick(l.username, l.password)}>
|
|
Use
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
)
|
|
}
|