← back to Inline Editor

README.md

124 lines

# @dw/inline-editor

Drop-in inline page editor for the DW sister-site fleet (~46 sites). Admins
authenticated via **auth.agentabrams.com** (role=admin) can click any text
block, image, link, or HTML region on any sister site and edit it directly
on the page — Wix-style. Edits persist in `dw_unified.page_overrides` (PG)
and the source HTML is left untouched.

## Architecture

```
              ┌─────────────────────────────────────────┐
              │  auth.agentabrams.com (existing widget) │
              │  → sets cookie with { email, role }     │
              └────────────────┬────────────────────────┘
                               │
            ┌──────────────────▼──────────────────┐
            │  per-site Express app               │
            │  app.use(inlineEditor({ site }))    │
            │  ─ middleware: HTML rewriter        │
            │  ─ routes:  /api/_inline/*          │
            │  ─ static:  /_inline/editor.js,css  │
            └────────────────┬────────────────────┘
                             │ reads/writes
                             ▼
                  ┌──────────────────────────┐
                  │  dw_unified PG           │
                  │  · page_overrides        │ ← current state per (site,path,selector)
                  │  · page_edit_log         │ ← append-only audit
                  │  · page_uploads          │ ← image uploads metadata
                  └──────────────────────────┘
```

## Persistence — append-only audit (best-practice mode)

`page_overrides` holds the *current* override for each (site, path, selector).
`page_edit_log` is **append-only** — every save inserts a new row with editor
email + IP + UA. A revert is a new log row that updates `page_overrides`. No
edit is ever silently lost.

```
page_overrides
  site, path, selector  PRIMARY KEY
  kind                  'text' | 'html' | 'image' | 'href' | 'image_alt'
  value                 sanitized payload
  updated_at, updated_by_email

page_edit_log
  id, site, path, selector, kind,
  old_value, new_value,
  editor_email, editor_ip, editor_ua, ts
```

## Edit surface (v1 — "all like Wix")

| kind | trigger | persists |
|---|---|---|
| `text` | click any text-bearing leaf node | sanitized innerText |
| `html` | double-click a `[data-edit-region]` block | DOMPurify-sanitized innerHTML |
| `image` | click an `<img>` | new src (uploaded to /uploads or pasted URL) |
| `image_alt` | shift-click an `<img>` | alt text |
| `href` | right-click an `<a>` | new href (only http/https/mailto/tel) |

All HTML kinds are sanitized server-side via `sanitize-html` with a strict
allowlist (no `<script>`, no `on*` attrs, no `javascript:` urls). Image uploads
gate on MIME (`image/*`), size (≤5 MB), and re-validate at content sniff.

## Auth (role=admin via auth.agentabrams.com)

Every save endpoint checks the `dw_auth` cookie issued by the auth widget.
The cookie is a signed JWT; we verify against `AUTH_JWKS_URL` and require:
- `role === 'admin'`
- `iss === 'https://auth.agentabrams.com'`
- `exp > now`

Rate-limited to 10 edits / min per editor email.

## Wiring per site (one line)

```js
const inlineEditor = require('@dw/inline-editor');
app.use(inlineEditor({
  site: 'corkwallcovering.com',
  uploadDir: path.join(__dirname, 'public', 'uploads'),
}));
```

The middleware:
1. Wraps `res.send` for `text/html` responses, walking the DOM and applying
   any matching overrides from PG (cached for 30 s per (site, path)).
2. Injects `<script src="/_inline/editor.js" defer></script>` and a tiny
   bootstrap `<script>` that reads the auth cookie and decides whether to
   activate edit mode.
3. Mounts `/api/_inline/save`, `/state`, `/upload`, `/revert`.

Sites *not* served by Express (static nginx) can opt in by including the
script tag and pointing it at a central edit-API host.

## Security checklist

- [x] Append-only audit log; never delete, only superseding inserts
- [x] HTML sanitized server-side, not client-side
- [x] CSP-friendly: editor uses no `eval`, no inline event handlers (uses delegated listeners + dataset)
- [x] Auth cookie verified per request; admin role required
- [x] Rate limit 10 edits/min/editor; 100/day soft cap with alert
- [x] Image uploads MIME-sniffed (file signature), size-capped, stored outside web root by default
- [x] Selector validated as stable (id, data-edit-id, structural path with index); reject XPath
- [x] No raw HTML kind accepted unless region has `[data-edit-region]` opt-in
- [x] Backups: page_overrides snapshotted to `data/overrides.snapshot.json` hourly
- [x] Revert path from admin.agentabrams.com console

## Pilot

Pilot site: TBD (a single low-traffic sister site, Mac2-local first).
Production deploy to Kamatera fleet only after the pilot is green and
Steve gives explicit "deploy fleet" go-ahead.

## NOT in scope (yet)

- Cross-page reflows (editor doesn't break grids when text expands)
- Layout edits (drag-drop blocks) — that needs a real visual editor
- Multi-language overrides
- Versioning UI (we have the log; UI lands in admin console v2)