feat(demo): DEMO_MODE lock, seed:demo and the demo banner (MODULE_API 1.18.0)
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
This commit is contained in:
@@ -21,6 +21,8 @@ const createLogger = require('./utils/logger')
|
||||
const htmlShell = require('./utils/htmlShell')
|
||||
const { applyTrustProxy, trustProxyDebug } = require('./utils/trustProxy')
|
||||
const botScore = require('./middleware/botScore')
|
||||
const { isDemo } = require('./demo/mode')
|
||||
const demoLock = require('./demo/locks')
|
||||
|
||||
const httpLog = createLogger('http')
|
||||
const errLog = createLogger('error')
|
||||
@@ -84,6 +86,23 @@ app.use(morgan(accessFormat, { stream: { write: (line) => httpLog.info(line.trim
|
||||
app.use(express.json({ limit: '2mb' }))
|
||||
app.use(cookieParser())
|
||||
|
||||
// ── The public demo ───────────────────────────────────────────────────
|
||||
// DEMO_MODE=true turns this deployment into demo.runicgateway.com
|
||||
// (docs/website/DEMO_MODE.md). Unset, nothing here is mounted, so a real shard
|
||||
// runs none of it. After the body parser, because one lock reads the body of
|
||||
// PUT /admin/settings; after cookieParser, because the shared-login locks read
|
||||
// the session; ahead of `/api` and of the SPA's static files (robots.txt).
|
||||
// The module locks are asked per request: the loader runs further down.
|
||||
if (isDemo()) {
|
||||
app.use(demoLock.noindex)
|
||||
app.get('/robots.txt', (req, res) => {
|
||||
// #swagger.ignore = true
|
||||
demoLock.robots(req, res)
|
||||
})
|
||||
app.use(demoLock.refuseMultipart)
|
||||
app.use('/api/v1', demoLock.createLock(() => modules.demoLocks()))
|
||||
}
|
||||
|
||||
// ── Paths ─────────────────────────────────────────────────────────────
|
||||
const SERVER_ROOT = path.join(__dirname, '..')
|
||||
const REPO_ROOT = path.join(SERVER_ROOT, '..')
|
||||
|
||||
Reference in New Issue
Block a user