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:
@@ -3,6 +3,7 @@ const brand = require('../../config/brand')
|
||||
const { parseJsonSetting } = require('../../utils/settingsJson')
|
||||
const { resolveThemeTokens } = require('../../utils/themeResolve')
|
||||
const { resolveBrandAssets } = require('../../utils/brandAssets')
|
||||
const { isDemo } = require('../../demo/mode')
|
||||
|
||||
// Keys safe to expose on the public site.
|
||||
const PUBLIC_KEYS = [
|
||||
@@ -215,9 +216,30 @@ async function getPublic() {
|
||||
// before doing so (the server would otherwise reject an unallowlisted one). The
|
||||
// custom-scheme callback works regardless of this flag.
|
||||
out.mobileAppLinks = String(all[MOBILE_APP_LINKS_KEY]) === 'true'
|
||||
// The public demo's banner and sign-in panel (DEMO_MODE.md §4, §6). Present
|
||||
// only with DEMO_MODE on, so a real shard's payload is unchanged. Written by
|
||||
// `seed:demo` at every wipe; the demo lock keeps `demo_*` out of PUT
|
||||
// /admin/settings, so a visitor cannot rewrite the published passwords.
|
||||
if (isDemo()) out.demo = demoBlock(all[DEMO_STATE_KEY])
|
||||
return out
|
||||
}
|
||||
|
||||
// `demo_state` is one JSON object: `{ resetsAt, logins: [{ username, password,
|
||||
// role }] }`. The passwords are public by design (§4) — the logins are printed
|
||||
// on the sign-in page and on runicgateway.com/beta. Read forgivingly: a missing
|
||||
// or broken row is a banner with no time and no logins, never a 500.
|
||||
const DEMO_STATE_KEY = 'demo_state'
|
||||
function demoBlock(raw) {
|
||||
const state = parseJsonSetting(raw) || {}
|
||||
const resetsAt = typeof state.resetsAt === 'string' && !Number.isNaN(Date.parse(state.resetsAt))
|
||||
? state.resetsAt
|
||||
: null
|
||||
const logins = (Array.isArray(state.logins) ? state.logins : [])
|
||||
.filter((l) => l && typeof l.username === 'string' && typeof l.password === 'string')
|
||||
.map((l) => ({ username: l.username, password: l.password, role: typeof l.role === 'string' ? l.role : null }))
|
||||
return { resetsAt, logins }
|
||||
}
|
||||
|
||||
/**
|
||||
* What the HTML shell needs, resolved exactly as getPublic() resolves it: the
|
||||
* effective favicon and logo, plus the theme token map for the boot <style>
|
||||
@@ -285,4 +307,5 @@ module.exports = {
|
||||
registrationFlags,
|
||||
MOBILE_APP_LINKS_KEY,
|
||||
isMobileAppLinksEnabled,
|
||||
DEMO_STATE_KEY,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user