diff --git a/BACKEND_DESIGN.md b/BACKEND_DESIGN.md deleted file mode 100644 index 2d947be..0000000 --- a/BACKEND_DESIGN.md +++ /dev/null @@ -1,329 +0,0 @@ -# UOMysticmoon Website — Backend Design - -> Phase 1 of 3: **backend design** → Claude Design (frontend mockup) → coding. -> This document is the contract the later phases build against. - -Public contact email: **UOMysticmoon@gmail.com** - ---- - -## 1. Stack & top-level decisions - -| Concern | Decision | Rationale | -|---|---|---| -| Runtime | Node.js + Express | serverlinkr pattern | -| Database | MariaDB (own container) | spec; `mariadb` pool, parameterized SQL, no ORM (keeps the lightweight `model`/`db` split from serverlinkr) | -| Auth | JWT in an **httpOnly cookie** | spec says "JWT auth" + "secure cookies when HTTPS"; httpOnly keeps the token out of JS (XSS-safe), `SameSite=Strict` covers CSRF for a same-origin admin panel | -| Frontend | React + Vite, same repo, served by Express in prod | spec | -| Hashing | bcrypt (`bcryptjs`) | spec; matches serverlinkr | -| Deploy | Docker Compose (app + db) behind Pangolin | spec | - -**Adapting serverlinkr → this project** -- `*.mongo.js` (mongoose) → `*.db.js` (MariaDB queries), exactly as the spec names them. -- Drop the session/passport hybrid (`express-session`, `passport`, `passport-local`, `connect-mongo`). Pure stateless JWT instead — simpler and matches "JWT auth". -- Routes grouped by **access level** (auth / public / admin) per spec, instead of serverlinkr's per-entity routers. Models stay grouped by **entity**. - ---- - -## 2. Folder structure - -Skeleton from the spec, with a small number of justified additions marked **(+)**. - -``` -server/ - .env.example - package.json - db/ - schema.sql (+) DDL, also auto-run by the MariaDB container - seed.js (+) seed wiki pages, default settings, first admin - src/ - server.js bootstrap: ensure schema, then listen on 0.0.0.0 - app.js express app + middleware wiring - router/ - api.router.js mounts /v1 - v1/ - v1.router.js mounts /auth /public /admin - auth/ auth.routes.js + auth.controller.js - public/ public.routes.js + public.controller.js - admin/ admin.routes.js + admin.controller.js - model/ - users/ users.model.js + users.db.js - posts/ posts.model.js + posts.db.js (news/five-on-friday/newsletter/screenshots) - wiki/ wiki.model.js + wiki.db.js - settings/ settings.model.js + settings.db.js - activity/ activity.model.js + activity.db.js (+) admin activity log - middleware/ (+) - siteMode.js LIVE/MAINTENANCE gate for public content - noindex.js X-Robots-Tag: noindex,nofollow on admin - rateLimit.js login limiter - validate.js express-validator error handler - utils/ - auth.js JWT sign/verify, isLoggedIn middleware - db.js MariaDB pool + ensureSchema() - mailer.js (+) nodemailer; mailto fallback if SMTP unset -client/ built in Phase 2/3 (React + Vite) -Dockerfile -docker-compose.yml -.env.example -.gitignore -``` - -**Why the additions:** the spec's feature list requires an activity log, a maintenance-mode -gate, login rate limiting, admin `noindex`, and SMTP email — none fit cleanly in the four -listed models/two utils. They're isolated in `middleware/` + one `activity` model + -`utils/mailer.js`, and the spec explicitly says the layout is "expandable." - ---- - -## 3. Database schema (MariaDB) - -`utf8mb4` throughout. Created idempotently on boot (`ensureSchema()`) **and** shipped as -`db/schema.sql` for the container's `/docker-entrypoint-initdb.d`. - -### users -| col | type | notes | -|---|---|---| -| id | INT PK AUTO_INCREMENT | | -| username | VARCHAR(32) UNIQUE NOT NULL | | -| password_hash | VARCHAR(72) NOT NULL | bcrypt; **never** returned by the API | -| role | ENUM('admin','editor') NOT NULL DEFAULT 'admin' | room to grow | -| created_at | DATETIME DEFAULT CURRENT_TIMESTAMP | | -| last_login_at | DATETIME NULL | shown in user management | - -### posts — one table, four categories -| col | type | notes | -|---|---|---| -| id | INT PK AUTO_INCREMENT | | -| category | ENUM('news','five_on_friday','newsletter','screenshot') NOT NULL | | -| title | VARCHAR(200) NOT NULL | | -| slug | VARCHAR(220) NULL | optional clean URL | -| excerpt | VARCHAR(400) NULL | list teaser | -| body | MEDIUMTEXT NULL | markdown/HTML; main text for news/5oF/newsletter | -| image_url | VARCHAR(500) NULL | required for `screenshot`, optional hero elsewhere | -| published | TINYINT(1) NOT NULL DEFAULT 0 | publish/unpublish toggle | -| author_id | INT NULL FK→users(id) | ON DELETE SET NULL | -| created_at | DATETIME DEFAULT CURRENT_TIMESTAMP | | -| updated_at | DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | | -| published_at | DATETIME NULL | set when first published; list order | - -Index: `(category, published, published_at DESC)`. - -### wiki_pages -| col | type | notes | -|---|---|---| -| id | INT PK AUTO_INCREMENT | | -| slug | VARCHAR(120) UNIQUE NOT NULL | e.g. `new-player-guide` | -| title | VARCHAR(200) NOT NULL | | -| body | MEDIUMTEXT NULL | markdown/HTML | -| updated_by | INT NULL FK→users(id) | | -| created_at / updated_at | DATETIME | | - -Seeded with the 8 spec categories: `new-player-guide, maps-atlas, systems, items, monsters, crafting, lore, rules`. - -### settings — key/value, expandable -| col | type | notes | -|---|---|---| -| `key` | VARCHAR(64) PK | | -| value | TEXT NULL | | -| updated_by | INT NULL FK→users(id) | | -| updated_at | DATETIME ON UPDATE CURRENT_TIMESTAMP | | - -Seeded keys: `site_mode` (default `maintenance`), `site_mode_changed_at`, -`site_mode_changed_by`, `maintenance_message`, `status_message`, `homepage_teaser`, -`contact_email` (=UOMysticmoon@gmail.com), `site_title`. - -### activity_log — append-only -| col | type | notes | -|---|---|---| -| id | INT PK AUTO_INCREMENT | | -| user_id | INT NULL FK→users(id) | | -| action | VARCHAR(64) NOT NULL | e.g. `auth.login`, `site_mode.change`, `post.create` | -| detail | TEXT NULL | JSON string of what changed | -| ip | VARCHAR(45) NULL | from `req.ip` (needs `trust proxy`) | -| created_at | DATETIME DEFAULT CURRENT_TIMESTAMP | | - ---- - -## 4. API contract - -Base path `/api/v1`. JSON in/out. Auth via httpOnly cookie (`isLoggedIn` reads it; also -accepts `Authorization: Bearer` for API testing). - -### /auth (auth.routes.js → auth.controller.js) -| Method | Path | Auth | Body | Purpose | -|---|---|---|---|---| -| POST | `/login` | — (rate-limited) | `{username,password}` | verify, set cookie, log `auth.login`, update `last_login_at` | -| POST | `/logout` | cookie | — | clear cookie | -| GET | `/me` | cookie | — | current user (no hash) or 401 — client bootstraps auth state | - -No public `register`. First admin is bootstrapped by `seed.js` from env (see §6). Further -admins are created under `/admin/users`. - -### /public (public.routes.js → public.controller.js) — all GET, no auth -| Method | Path | Notes | -|---|---|---| -| GET | `/settings` | whitelisted public keys only (mode, maintenance_message, status_message, homepage_teaser, contact_email, site_title) | -| GET | `/status` | status message + current mode | -| GET | `/posts/:category` | published only; `category` ∈ news\|five-on-friday\|newsletter\|screenshots | -| GET | `/posts/:category/:idOrSlug` | single published post | -| GET | `/wiki` | list of pages (slug + title) | -| GET | `/wiki/:slug` | single page | -| POST | `/contact` | (rate-limited) send mail via SMTP; if unconfigured, respond `{fallback:"mailto", email}` | - -Public content GETs pass through the **siteMode** gate (§5). - -### /admin (admin.routes.js → admin.controller.js) — all behind `isLoggedIn` + `noindex` -| Method | Path | Purpose | -|---|---|---| -| GET | `/dashboard` | current mode, last change time + who, content counts, recent activity | -| PUT | `/site-mode` | `{mode}` → update settings, stamp who/when, log `site_mode.change` | -| GET | `/posts?category=` | all posts incl. unpublished | -| POST | `/posts` | create | -| GET | `/posts/:id` | one | -| PUT | `/posts/:id` | edit | -| DELETE | `/posts/:id` | delete | -| PATCH | `/posts/:id/publish` | `{published}` toggle (sets `published_at`) | -| POST | `/posts/upload` | multipart image upload (multer) → `{image_url}` for screenshots | -| GET | `/wiki` · GET `/wiki/:slug` | read incl. unpublished | -| POST | `/wiki` · PUT `/wiki/:slug` · DELETE `/wiki/:slug` | manage pages | -| GET | `/settings` · PUT `/settings` | read all / update `{key:value,...}` | -| GET | `/activity?limit=&offset=` | paginated activity log | -| GET | `/users` · POST `/users` · PUT `/users/:id` · DELETE `/users/:id` | user mgmt (can't delete self / last admin; password hashed on write) | - -Every admin write logs to `activity_log`. - ---- - -## 5. Site mode (LIVE / MAINTENANCE) - -State in `settings.site_mode` (`live`|`maintenance`), default **maintenance**. - -`middleware/siteMode.js`, applied only to **public content** routes: -- `live` → pass through. -- `maintenance` → respond **503** with `{mode:"maintenance", message}` **unless** the request - carries a valid admin cookie (admin preview). This hides content server-side, not just in - the UI. - -Always reachable regardless of mode: static assets / SPA shell, `/api/v1/auth/*`, all -`/api/v1/admin/*`. So admin login + panel + the maintenance "coming soon" page always load. - -**Client behavior (Phase 3):** reads `GET /public/settings`; if `maintenance` and not an -admin previewing, render the polished dark coming-soon page (message + contact email). -Admin "preview live" simply hits the content APIs with the admin cookie, which bypass the gate. - -Dashboard reads `site_mode` + `site_mode_changed_at`/`_by` for "current mode + last change + -who"; `activity_log` provides the history feed. - ---- - -## 6. Auth & security - -- **JWT** signed with `JWT_SECRET`, `expiresIn=JWT_EXPIRES_IN` (default `1d`); payload `{id,username,role}`. -- **Cookie**: `httpOnly`, `sameSite=Lax`, `path=/`, and **`secure` decided per-request** (`COOKIE_SECURE=auto` → `secure: req.secure`). This is the key to dual access: the cookie is `Secure` when reached through Pangolin (HTTPS, `X-Forwarded-Proto: https`) but **not** `Secure` when reached directly over the LAN IP on plain HTTP — so login works in both. `COOKIE_SECURE=true|false` can force it. Requires `trust proxy` (below). `localhost:5173` (Vite) and `localhost:3000` are same-site, so the cookie flows in dev too. -- **bcrypt** hashing (cost 10+); plaintext passwords never stored, logged, or returned. -- **Rate limiting** (`express-rate-limit`) on `/auth/login` and `/public/contact`. -- **Validation** (`express-validator`) on all writes; centralized error handler. -- **helmet** with a CSP suited to the SPA (self + inline styles as needed; image sources for uploads/hero). -- **Admin not indexed**: `X-Robots-Tag: noindex, nofollow` on `/api/v1/admin` and the admin SPA routes; `robots.txt` disallows `/admin`. -- **No directory browsing** (express.static doesn't list; no `serve-index`). -- **No hardcoded credentials**: first admin via `seed.js` reading `ADMIN_USERNAME`/`ADMIN_PASSWORD` from env (created only if no users exist); `.env` git-ignored, `.env.example` committed. -- **`app.set('trust proxy', 1)`** so secure cookies, `req.ip`, and rate-limiting work behind Pangolin. -- **CORS**: same-origin in prod (SPA served by Express). Dev only: allow `CLIENT_ORIGIN` (Vite, `http://localhost:5173`) with `credentials:true`. - ---- - -## 7. Email - -`utils/mailer.js` (nodemailer) sends through **Gmail over OAuth2 (SMTP XOAUTH2)**, configured in -Admin → Settings → Email — not env. The mailbox is authorized by an in-app "Connect Gmail" consent -flow (`/admin/email/*`) that captures a refresh token, stored AES-GCM-encrypted in the `email_config` -singleton (never returned over the API). The OAuth client id/secret are reused from the `google` -auth-providers row. Recipient is the `contact_email` site setting. If email is unconfigured/disabled, -`POST /public/contact` returns `{fallback:"mailto", email}` so the client renders a `mailto:` link -instead. Errors never leak credentials. - ---- - -## 7.5 Logging & observability - -`utils/logger.js` — a small dependency-free logger with **two transports, console + file**, -and four levels (`error`/`warn`/`info`/`debug`). Each line is timestamped and tagged by -subsystem (`[server]`, `[http]`, `[db]`, `[auth]`, `[admin]`, `[ratelimit]`, …). - -- **Console**: color on a TTY, plain in Docker; verbosity = `LOG_LEVEL` (default `info`). -- **File**: plain text appended to `LOG_DIR/LOG_FILE` (default `/logs/app.log`, - `/app/logs/app.log` in Docker, bind-mounted to `./logs`); verbosity = `FILE_LOG_LEVEL` - (default `debug`, so the file keeps a complete record while the console stays readable). - Toggle with `LOG_TO_FILE`. The stream is flushed on graceful shutdown. -- **HTTP access logs** via morgan piped into the logger: real client IP (`trust proxy`), - authenticated admin username, method, URL, status, response time, size. -- **Captured events**: startup config banner, schema/seed steps, login success/failure, - rate-limit hits, site-mode changes, maintenance-gate blocks (debug), all errors with - stack traces (5xx), and SIGINT/SIGTERM shutdown. Passwords and request bodies are never - logged. `unhandledRejection`/`uncaughtException` are caught and logged. - -## 8. Deployment - -**docker-compose.yml** — two services on a private network: -- `db`: `mariadb:11`, env `MARIADB_DATABASE/USER/PASSWORD/ROOT_PASSWORD`, volume - `dbdata:/var/lib/mysql`, mounts `schema.sql` into `/docker-entrypoint-initdb.d`, healthcheck. -- `app`: builds the Dockerfile (installs client+server, builds Vite, serves via Express), - `env_file: .env`, `DB_HOST=db`, `depends_on: db (healthy)`, volume `uploads:/app/uploads`, - `ports: "3000:3000"` — **binds 0.0.0.0** (no `127.0.0.1:` prefix) so Pangolin reaches it. -- Volumes: `dbdata`, `uploads`. - -Express listens on `0.0.0.0:${PORT||3000}`. Pangolin terminates TLS and proxies to `app`. - -**.env.example** (committed; real `.env` ignored): -``` -NODE_ENV=production -PORT=3000 -DB_HOST=db -DB_PORT=3306 -DB_NAME=uomysticmoon -DB_USER=uomm -DB_PASSWORD= -DB_ROOT_PASSWORD= -JWT_SECRET= -JWT_EXPIRES_IN=1d -COOKIE_SECURE=true -COOKIE_NAME=uomm_token -ADMIN_USERNAME= -ADMIN_PASSWORD= -# Email: configured in Admin → Settings → Email (Gmail OAuth2), not via env -CLIENT_ORIGIN=http://localhost:5173 -``` - -`.gitignore`: `node_modules/`, `.env`, `_reference/`, `client/dist/`, `uploads/`. - ---- - -## 9. Dependencies (server) - -`express, cors, helmet, morgan, dotenv, mariadb, jsonwebtoken, bcryptjs, cookie-parser, -express-rate-limit, express-validator, multer, nodemailer` · dev: `nodemon`. -Removed vs serverlinkr: `mongoose, mongodb, connect-mongo, express-session, passport, -passport-local`. - ---- - -## 10. Spec coverage - -| Spec requirement | Covered by | -|---|---| -| Public pages (`/`, `/site/*`, `/wiki/*`) | `/public/*` API + Phase-3 SPA routes; content from `posts`/`wiki`/`settings` | -| News / 5-on-Friday / Newsletter / Screenshots | `posts` table, `category` column; admin CRUD + publish | -| Wiki 8 categories, editable later | `wiki_pages` seeded with 8 slugs; admin CRUD | -| Status page | `settings.status_message` + mode via `/public/status` | -| Admin dashboard (mode, last change, who) | `/admin/dashboard` + settings stamps + activity log | -| Site mode toggle | `PUT /admin/site-mode` + `siteMode` middleware | -| Admin activity log | `activity_log` + `/admin/activity` | -| Admin user management | `/admin/users` CRUD | -| Site settings editing | `/admin/settings` | -| JWT, bcrypt, rate limit, secure cookies, noindex, no dir browsing, no hardcoded creds, .env | §6 | -| Maintenance page, admin always in, static always loads, admin preview | §5 | -| SMTP via env, mailto fallback | §7 | -| Docker Compose + MariaDB + Pangolin, 0.0.0.0 bind | §8 | -| Design tokens / hero | reused from existing `assets/css/mysticmoon.css` + hero PNG in Phase 2/3 | -| Expandable | key/value settings, role enum, modular routers/models | -``` diff --git a/HERO_EDITOR.md b/HERO_EDITOR.md deleted file mode 100644 index 931d003..0000000 --- a/HERO_EDITOR.md +++ /dev/null @@ -1,134 +0,0 @@ -# UOMysticmoon — Hero Canvas Editor Spec - -> Branch: **`hero-feature`**. Build contract for the WYSIWYG portal-hero editor. -> Derived from the design doc *Hero Canvas Editor — Design Document*, **corrected -> to match the current codebase** and with the open questions resolved. -> Same workflow as the wiki upgrade: design → phased build → verify. - -## 1. Goal - -Let staff compose the portal hero (background image, overlay opacity, and floating -elements — text, CTA buttons, moon, badge, image) in-browser, then preview and -publish — no source edits. Layout persists as JSON in the existing `settings` table. - -## 2. Locked decisions - -| # | Decision | -|---|---| -| Scope | **Full v1** — background/overlay, all element types, drag/resize/z-order, draft→preview→publish (built in phases) | -| CTA buttons | **First-class `buttons` element type** (independently positioned), not baked into a text block | -| First run | **Pre-populate** the canvas with today's hero (headline, subtitle, teaser, CTAs) as editable elements so nothing changes visually until edited | -| Drag | **Native Pointer Events** (mouse/touch/pen), zero dependencies | -| Font size | Stored in **px** (fixed reference canvas) | -| Image compression | **None** server-side; client warns when a file is > ~1 MB | -| Preview | `?preview=1` renders the **draft** by reading it through the authenticated admin settings endpoint | -| Other pages | Out of scope for v1 (design allows a per-page key later) | - -## 3. Corrections to the design doc (current-code reality) - -1. **Public settings is a whitelist, not `getAll()`.** `GET /api/v1/public/settings` - → `settings.getPublic()` → `PUBLIC_KEYS` in - [settings.model.js](server/src/model/settings/settings.model.js). The doc's - "no backend changes / picked up automatically" is wrong. **Fix:** add - `hero_layout` to `PUBLIC_KEYS` (one line). `hero_layout_draft` stays out - (admin-only) — which is why preview reads the draft via `api.admin.getSettings()`. -2. **Moon is a reusable component** ([MoonDot.jsx](client/src/components/MoonDot.jsx), - props `size`/`glow`), used in logo/login/maintenance — not "only the header." - The `moon` element reuses it; it gains an optional `color`. -3. **Route vs. nav live in different files.** `/admin/hero` route → - [App.jsx](client/src/App.jsx); sidebar link/title → `NAV`/`TITLES` in - [AdminLayout.jsx](client/src/routes/admin/AdminLayout.jsx). -4. **Admin content area is `maxWidth: 1000px`** — the editor canvas renders - scaled-to-fit; percentage positions stay faithful. - -Everything else in the doc matches (hardcoded `HERO_BG` + CTAs + `homepage_teaser` -in [Portal.jsx](client/src/routes/public/Portal.jsx); `updateSettings` accepts -arbitrary keys; `/admin/uploads` exists; default hero asset present; TEXT settings -columns — no schema change). - -## 4. Data model — no schema change - -Two `settings` keys (TEXT): `hero_layout` (live) and `hero_layout_draft` (admin). - -```jsonc -{ - "version": 1, - "background": { "image_url": null, "position_x": "left", "position_y": "center", "size": "cover" }, - "overlay": { "opacity": 0.72 }, - "elements": [ - { "id": "uuid", "type": "text_block|buttons|moon|badge|image", - "x": 50, "y": 42, "z": 1, "anchor": "center", "props": { /* per type */ } } - ] -} -``` - -Positions are **% of canvas** (reference width 1080, matching `.shell`), so the -layout adapts across viewports without breakpoint data. `version` is validated -(`=== 1`) before use; anything else falls back. - -### Element props - -| Type | Props | -|---|---| -| `text_block` | `lines: [{ text, tag(h1/h2/p/span), fontSize(px), color, weight }]`, `align` | -| `buttons` | `items: [{ label, to, variant(primary/ghost) }]`, `align`, `gap` | -| `moon` | `size`, `glow`, `color` | -| `badge` | `text`, `bgColor`, `textColor`, `borderRadius` | -| `image` | `src`, `width`(%), `alt` | - -## 5. Backend changes -- **One line:** add `'hero_layout'` to `PUBLIC_KEYS`. No new routes/controllers — - layout saves through the existing `PUT /admin/settings`; images via `/admin/uploads`. - -## 6. Frontend changes -- **New** `client/src/components/HeroElement.jsx` — renders one element by type - (shared by the live portal and the editor canvas). -- **New** `client/src/routes/admin/views/HeroEditor.jsx` — canvas + element tray + - properties panel; native-pointer drag/resize; background/overlay panel; snap grid; - auto-save draft, preview, publish, revert. -- **Edit** [Portal.jsx](client/src/routes/public/Portal.jsx) — parse `hero_layout` - (or draft when `?preview=1` + admin), render elements, fall back to a - `DEFAULT_LAYOUT` built from today's hero so the page is unchanged until edited. -- **Edit** [AdminLayout.jsx](client/src/routes/admin/AdminLayout.jsx) (nav) + - [App.jsx](client/src/App.jsx) (route `/admin/hero`). -- **Edit** [MoonDot.jsx](client/src/components/MoonDot.jsx) — optional `color`. -- **No** `client/src/api/client.js` changes needed beyond what exists - (`admin.updateSettings`, `admin.getSettings`, `admin.upload`). - -## 7. Phased build (each phase: build → verify in preview → commit) - -- **Phase 0 — Spec** ✅ this document. -- **Phase 1 — Data path & renderer** ✅ (verified 2026-06-28). `hero_layout` - whitelisted; `HeroElement.jsx`; Portal renders the layout with a `DEFAULT_LAYOUT` - fallback. Default render matches the old hero; publishing a layout re-renders; - draft key not exposed publicly. Shared helpers moved to `client/src/lib/heroLayout.js`. -- **Phase 2 — Editor shell + background/overlay** ✅ (verified 2026-06-28). - `/admin/hero` view + sidebar nav; canvas live-preview; background upload + 3×3 - position + overlay opacity; debounced draft auto-save; publish; `?preview=1` - reads the draft (admin) with a banner; revert. Verified: overlay/position update - the canvas, auto-save writes the draft, publish writes live, preview shows the - draft while the normal portal shows live. -- **Phase 3 — Elements: select / drag / text_block / buttons** ✅ (verified - 2026-06-28). Element tray (+ Text / + Buttons); click-to-select with outline; - native Pointer Events drag (% of canvas); Delete key + panel delete; z-order - (send back / bring forward); text_block line editor (text/tag/size/color/bold, - add/remove lines, align) and buttons editor (label/path/variant, add/remove). - Verified: select shows the line editor, editing a line updates the canvas live, - drag moved 50%→65%, add→3/delete→2 elements, empty-canvas click deselects. -- **Phase 4 — moon + badge + image + resize + snap grid** ✅ (verified 2026-06-28). - Tray adds moon/badge/image; property panels (moon: size/glow/color; badge: - text/colors/radius; image: upload/width/alt); corner resize handle (image→width%, - moon→size, text→box width); 8px snap-grid toggle with overlay; image placeholder - until a file is chosen. Verified: each type adds + edits, resize moved a moon - 64→104px, snap grid shows, and a published moon+badge render on the live portal. - -**Status: v1 feature-complete.** All phases verified end-to-end; ready for PR. -Deferred (noted in the design doc as follow-ups): 8-point resize (only a corner -handle for now), per-viewport layouts, server-side image compression. - -## 8. Edge cases (from the doc, carried forward) -- `JSON.parse` wrapped in try/catch + `version` check → fall back to `DEFAULT_LAYOUT`. -- Element ids via `crypto.randomUUID()` (never array index). -- Empty `elements` → render `DEFAULT_LAYOUT` so the hero is never blank. -- Last-write-wins on concurrent admin edits (acceptable for this shard). -- Client-side warning for background files > ~1 MB (no hard block; 8 MB server cap). diff --git a/README.md b/README.md index 5f064b0..475a801 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ shard — a full-stack app in one repo: - **Deploy** — Docker Compose (app + MariaDB) behind a Pangolin reverse proxy. Express serves the built SPA in production. - **Shard link** — a live bridge to the in-game ServUO shard through the **uo-link** sidecar ([UOM/link](https://gitea.whitlocktech.com/UOM/link)): the site ingests a live event feed and makes server-side REST calls to show shard status, economy, staff presence, IDOCs, live activity, and per-character sheets. See [Shard integration (uo-link)](#shard-integration-uo-link). -The design reference is [BACKEND_DESIGN.md](BACKEND_DESIGN.md) (API contract, schema, security). +The design reference is [BACKEND_DESIGN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/BACKEND_DESIGN.md) (API contract, schema, security), in the [**RunicGateway/docs**](https://gitea.whitlocktech.com/RunicGateway/docs) repo — where all project documentation now lives. --- @@ -229,7 +229,7 @@ npm start # node server → serves API + SPA at http://localhost:3 Post categories (URL form): `news`, `five-on-friday`, `newsletter`, `screenshots`. `authMethod` on a session ∈ `local · totp · mobile · google · discord · oidc`. -See [BACKEND_DESIGN.md](BACKEND_DESIGN.md) §4 for the full contract, or the interactive Swagger +See [BACKEND_DESIGN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/BACKEND_DESIGN.md) §4 for the full contract, or the interactive Swagger docs below for a per-endpoint reference (parameters, request bodies, response codes). --- diff --git a/WIKI_UPGRADE.md b/WIKI_UPGRADE.md deleted file mode 100644 index 91d55f1..0000000 --- a/WIKI_UPGRADE.md +++ /dev/null @@ -1,366 +0,0 @@ -# UOMysticmoon Website — Wiki Upgrade Spec - -> Branch: **`wiki-upgrade`**. This document is the contract for upgrading the CMS -> wiki from a flat single-table page store into a feature-complete wiki. -> It follows the project workflow: **design (this doc) → build in phases → verify**. -> -> Companion to [`BACKEND_DESIGN.md`](BACKEND_DESIGN.md); reuses its stack, auth, -> logging, and Docker decisions unchanged. - ---- - -## 1. Goal & scope - -Turn the wiki into something that behaves like a typical wiki, while staying inside -the existing Node/Express + MariaDB + React/Vite architecture and the **staff-only** -auth model (admin/editor — no new roles, no public contributions). - -**In scope** - -| Feature | Summary | -|---|---| -| Rich-text editing | TipTap (ProseMirror) WYSIWYG in the admin; outputs HTML | -| Sanitization | Server-side allowlist on save **and** client-side on render (fixes today's stored-XSS gap) | -| Categories / sections | First-class `wiki_categories` table; replaces hardcoded frontend blurbs | -| Drafts & publish | `published` + `published_at`, mirroring the `posts` pattern | -| Tags | Many-to-many tags with filtering | -| Internal links | `[[slug]]`-style links authored in the editor; red-link detection | -| Backlinks | "Linked from" list, maintained on save | -| Inline images | Reuse/generalize the existing multer upload for in-body images | -| Search | MariaDB `FULLTEXT` over title + body | -| Revision history | Per-save snapshots with view / diff / restore | - -**Out of scope (this branch)** - -- Public/player editing or suggestion workflow, moderation/review queues. -- New roles or per-page ACLs (all staff with a login can edit all pages). -- Real-time collaborative editing, comments/discussion pages, file attachments - other than images, page templates/transclusion, multilingual pages. - -**Decisions locked from planning** - -- Editor: **TipTap**, storing **HTML** (not Markdown, not JSON). -- Search: **MariaDB FULLTEXT** (no new infrastructure). -- Revision history and search are **included** (recommended additions beyond the - minimum requested set). -- Authoring is **admin + editor** (`isLoggedIn`); no anonymous edits. - ---- - -## 2. Current state (baseline being replaced) - -| Layer | Today | File | -|---|---|---| -| Schema | flat `wiki_pages(slug,title,body,updated_by,timestamps)` | [server/db/schema.sql:31](server/db/schema.sql) | -| Model | thin CRUD by slug | [server/src/model/wiki/wiki.db.js](server/src/model/wiki/wiki.db.js), [wiki.model.js](server/src/model/wiki/wiki.model.js) | -| Public API | `GET /public/wiki`, `GET /public/wiki/:slug` | [public.controller.js:53](server/src/router/v1/public/public.controller.js) | -| Admin API | `GET/POST/PUT/DELETE /admin/wiki[...]` | [admin.controller.js:163](server/src/router/v1/admin/admin.controller.js), [admin.routes.js:68](server/src/router/v1/admin/admin.routes.js) | -| Public UI | card grid (hardcoded blurbs + Roman numerals), article w/ auto-TOC | [Wiki.jsx](client/src/routes/wiki/Wiki.jsx), [WikiArticle.jsx](client/src/routes/wiki/WikiArticle.jsx) | -| Admin UI | raw-HTML `