Commit Graph

194 Commits

Author SHA1 Message Date
b474303052 Merge pull request 'ci(sonarqube): generate and report server test coverage' (#85) from ci/sonar-test-coverage into main
All checks were successful
Build container images / build (push) Successful in 2m54s
SonarQube / analysis (push) Successful in 2m55s
Build container images / deploy (push) Successful in 47s
Reviewed-on: #85
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-21 04:57:06 +00:00
e515fc0c9d ci(sonarqube): generate and report server test coverage
All checks were successful
PR Checks / bot-install (pull_request) Successful in 18s
PR Checks / server-tests (pull_request) Successful in 44s
PR Checks / client-build (pull_request) Successful in 9m24s
SonarQube reported 0% coverage because the analysis workflow never ran
the test suite — the scanner does static analysis only and was handed no
coverage report, and sonar-project.properties defined no report path.

Generate an LCOV report in sonarqube.yml before the scan using Node's
built-in test coverage (run from the repo root so SF: paths are
server/src/... and resolve against the project base dir), and point
the scanner at it via sonar.javascript.lcov.reportPaths. Node's lcov
coverage reporter needs Node >= 22, so the coverage job pins node 22.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 23:46:44 -05:00
c73273f1bc Merge pull request 'fix(security): add SPA CSP, drop x-powered-by, strengthen dedupe hash' (#84) from fix/security-headers-csp into main
All checks were successful
Build container images / build (push) Successful in 1m31s
Build container images / deploy (push) Successful in 38s
SonarQube / analysis (push) Successful in 2m15s
Reviewed-on: #84
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-21 04:13:41 +00:00
5e5e0d7a91 docs(readme): add SonarQube project badges
All checks were successful
PR Checks / bot-install (pull_request) Successful in 15s
PR Checks / client-build (pull_request) Successful in 9m23s
PR Checks / server-tests (pull_request) Successful in 10m10s
Bugs, code smells, duplicated lines, LOC, security hotspots, security rating,
and vulnerabilities badges linking to the project dashboard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
2026-07-20 23:02:11 -05:00
e1461d9161 fix(security): add SPA CSP, drop x-powered-by, strengthen dedupe hash
Address SonarQube security hotspots on the website:

- server/src/app.js: replace `contentSecurityPolicy: false` with a helmet CSP
  tuned for the built React SPA (script-src 'self'; style-src adds 'unsafe-inline'
  for React inline styles + the Google Fonts stylesheet; font-src gstatic; img-src
  allows data:/https: for uploads, embedded body images and BRAND_* assets;
  connect-src 'self' for REST+SSE). upgrade-insecure-requests is intentionally
  omitted (TLS terminates at the proxy; keeps local `npm start` over http working).
  The /api/docs Swagger UI route gets a scoped looser policy (inline script/style)
  since swagger-ui-express injects an inline bootstrap.
- client/vite.config.js: disable the inline module-preload polyfill so code-split
  builds keep `script-src 'self'` valid (RichTextEditor is a separate chunk).
- bot/src/app.js, server/src/internalApp.js: disable x-powered-by on the two
  internal-only listeners (the public app already strips it via helmet).
- shardEvents dedupe key: SHA-1 -> SHA-256 truncated to 40 hex chars (fits the
  existing CHAR(40) column, no migration; it is a content fingerprint, not a
  security value). schema.sql comment updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
2026-07-20 23:02:11 -05:00
82bf2c972c Merge pull request 'ci(sonarqube): non-blocking SonarQube analysis on push to main' (#83) from ci/sonarqube-analysis into main
All checks were successful
Build container images / build (push) Successful in 1m18s
Build container images / deploy (push) Successful in 36s
SonarQube / analysis (push) Successful in 2m13s
Reviewed-on: #83
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-21 02:54:14 +00:00
c54bb54834 ci(sonarqube): add non-blocking SonarQube analysis on push to main
All checks were successful
PR Checks / bot-install (pull_request) Successful in 1m16s
PR Checks / server-tests (pull_request) Successful in 9m41s
PR Checks / client-build (pull_request) Successful in 10m39s
Wire the self-hosted SonarQube server into Gitea via a Gitea Actions
workflow. Runs on push to `main` (post-merge) and workflow_dispatch, so
it feeds the dashboard without gating any PR. Adds sonar-project.properties
(project key runic-gateway-website; server/client/bot sources, server tests,
node_modules/dist/generated excluded).

Requires two one-time Gitea settings: secret SONAR_TOKEN and variable
SONAR_HOST_URL. The scan does not wait on the Quality Gate, keeping it
fully non-blocking.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 21:43:00 -05:00
86420661b5 Merge pull request 'fix(db): strip inline -- comments before splitting schema statements' (#82) from fix/schema-loader-inline-comment-split into main
All checks were successful
Build container images / build (push) Successful in 1m19s
Build container images / deploy (push) Successful in 45s
Reviewed-on: #82
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-21 00:31:13 +00:00
d1b3351360 fix(db): strip inline -- comments before splitting schema statements
All checks were successful
PR Checks / server-tests (pull_request) Successful in 10m18s
PR Checks / client-build (pull_request) Successful in 9m32s
PR Checks / bot-install (pull_request) Successful in 9m26s
The schema loader stripped only full-line -- comments, then split the
file on ';'. A trailing comment containing a semicolon (e.g. the
mobile_auth_sessions.session_id column: `-- uuid; carried inside...`)
chopped the CREATE TABLE in half, so MariaDB got the fragment and failed
with `error ... near '' at line 3`, crash-looping the server on boot.

Strip -- comments on every line (full-line and trailing) before the ';'
split. Safe because the schema never places -- inside a string literal.

Verified by running ensureSchema() against a fresh MariaDB: all 49 tables
create cleanly and mobile_auth_sessions has all 11 columns.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 19:10:53 -05:00
dacc1bd4f7 Merge pull request 'feat(mobile-sso): serve assetlinks.json + App Links redirect allowlist' (#81) from feat/mobile-app-links into main
All checks were successful
Build container images / build (push) Successful in 1m7s
Build container images / deploy (push) Successful in 34s
Reviewed-on: #81
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-20 23:57:53 +00:00
bcc96e7cfb feat(mobile-sso): serve assetlinks.json + App Links redirect allowlist
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m23s
PR Checks / client-build (pull_request) Successful in 10m6s
PR Checks / bot-install (pull_request) Successful in 9m16s
Add the server side of Android App Links (M9 follow-up, docs/android/APP_LINKS.md):

- GET /.well-known/assetlinks.json at the web root, gated by the new admin
  setting `mobile_app_links_enabled` (default off -> 404; on-but-no-fingerprint
  -> 404). Emits the Digital Asset Links statement for the fixed published
  package (MOBILE_APP_PACKAGE) + MOBILE_APP_CERT_SHA256 fingerprint(s).
- mobileSso `/start` additionally accepts this shard's own self-origin
  https://<host>/mobile/callback when App Links are enabled — one additive
  exact-match entry, derived from APP_BASE_URL/request origin, never client
  input; the custom-scheme allowlist is never narrowed. The settings lookup is
  short-circuited for non-https redirects so custom-scheme rejections stay fast.
- settings.isMobileAppLinksEnabled() (fail-closed) + getPublic().mobileAppLinks;
  admin updateSettings validates the boolean; seed default off.

Tests: test/appLinks.test.js (route gating + allowlist). Full suite 284 pass.
Swagger unchanged (web-root verification file is #swagger.ignore'd).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
2026-07-20 18:37:17 -05:00
d37c3a46a9 Merge pull request 'feat(auth): native SSO authorization bridge for the Android app (M9 Part 1)' (#80) from feature/mobile-sso-bridge into main
All checks were successful
Build container images / build (push) Successful in 53s
Build container images / deploy (push) Successful in 35s
Reviewed-on: #80
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-20 22:27:22 +00:00
e3dd5358b6 feat(auth): Active Devices — view/revoke mobile sessions
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m27s
PR Checks / client-build (pull_request) Successful in 10m16s
PR Checks / bot-install (pull_request) Successful in 9m17s
Adds the self-service device-session surface the mobile-SSO spec requires, on
top of the existing mobile_refresh_tokens store.

- Schema: device_name + last_used_at columns on mobile_refresh_tokens (nullable,
  additive via the ALTER section; seeded to now on insert). With single-use
  rotation each login/refresh inserts a fresh row, so the active row's timestamp
  is the session's last activity, and the label is carried forward on refresh.
- Model: listActiveForUser (one row per live device, no token hash) +
  revokeByIdForUser (ownership-scoped, idempotent).
- GET /auth/me/sessions + DELETE /auth/me/sessions/:id (role-agnostic, behind
  requireAuth). Named distinctly from /auth/me/devices (push endpoints).
- device_name is an optional field on /auth/mobile/login and
  /auth/mobile/sso/exchange so the app can label a device.
- Client: an "Active Devices" panel on the player account page (list + sign a
  device out), plus the PlayerLogin change to honor the mobile SSO bridge's
  { redirect } deep link on a 2FA completion.
- Swagger DeviceSession schema + regenerated spec; 3 controller tests. Full
  server suite green (274); client builds.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 17:01:47 -05:00
61f4591a6b feat(auth): native SSO authorization bridge for the Android app
Add a Mobile SSO Authorization Bridge so the native app can "Sign in with
Google/Discord" without shipping any OAuth secret. It EXTENDS the existing
/auth/sso/* redirect flow (same PKCE-vs-IdP, link-only + opt-in provisioning,
TOTP gate) and terminates in the existing mobile bearer tokens — not a parallel
auth path.

- Schema: mobile_auth_sessions + mobile_auth_codes (short-lived, self-pruning;
  authorization code stored hash-only, PKCE challenge is a hash by construction).
- GET /auth/mobile/sso/start: validate provider enabled + redirect_uri by EXACT
  allowlist match (never prefix), seed a bridge session, reuse the SSO redirect
  tagged mode:'mobile' (new redirectToIdp helper extracted from beginFlow).
- SSO callback + finishSsoTotp gain a mode:'mobile' branch: mint a single-use,
  hashed, PKCE-bound code and redirect to the fixed app callback (code + echoed
  state, never a token) instead of setting a cookie. 2FA keeps full parity via
  the existing web TOTP form (now carrying the bridge session).
- POST /auth/mobile/sso/exchange: verify Layer-B PKCE (before burning the code),
  single-use consume, then issue the SAME pair as /auth/mobile/login.
- Discovery reuses GET /auth/providers; refresh/logout reuse /auth/mobile/*.
- Rate limits: /start per-IP+provider, /exchange per-IP. Boot-time +
  opportunistic prune of both tables (no cron, mirrors revoked_sessions).
- Redirect allowlist is MOBILE_AUTH_REDIRECT_URIS (default the one fixed
  runicgateway://auth/callback); App Link URIs can be appended per shard later.
- Swagger regenerated; 39 tests (model single-use/gating + full controller
  matrix: bad/expired/reused code, PKCE mismatch, disabled provider, redirect
  allowlist, TOTP-through-bridge). Full suite green (271).

Refs docs/website/BACKEND_DESIGN.md, docs/android/PLAN.md §9 (M9).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 16:55:06 -05:00
31b72859ce Merge pull request 'feat(settings): surface push.ntfyUrl in /public/settings for the app' (#79) from feat/settings-push-ntfy-url into main
All checks were successful
Build container images / build (push) Successful in 1m2s
Build container images / deploy (push) Successful in 45s
Reviewed-on: #79
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-20 20:55:56 +00:00
a789ee3ac9 feat(settings): surface push.ntfyUrl in /public/settings for the app
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m39s
PR Checks / client-build (pull_request) Successful in 9m24s
PR Checks / bot-install (pull_request) Successful in 9m17s
The Android app's embedded push distributor (M7 Part 2) needs the shard's
client-facing ntfy relay URL to build its device topic endpoint, but the M7
Part 1 backend only used the NTFY_* vars server-side and never surfaced them.

Add a `push: { ntfyUrl }` block to settings.getPublic(), sourced from
NTFY_PUBLIC_URL or the first NTFY_ALLOWED_ORIGINS entry (never the possibly
internal NTFY_BASE_URL); null when unconfigured, so the app shows push as
unavailable for that shard. Additive, non-sensitive, forward-compatible.

- Extend the PublicSettings swagger schema; regenerate swagger-output.json.
- publicBrand.test.js: cover null / NTFY_PUBLIC_URL / NTFY_ALLOWED_ORIGINS.
- Document NTFY_PUBLIC_URL in .env.example and (docs PR) BACKEND_DESIGN.md.

Full server suite green (250 pass).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 15:25:40 -05:00
4fa73d3ccf Merge pull request 'feat(push): M7 backend — opt-in push notifications via self-hosted ntfy' (#78) from feat/push-notifications-backend into main
All checks were successful
Build container images / build (push) Successful in 1m15s
Build container images / deploy (push) Successful in 35s
Reviewed-on: #78
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-20 15:27:59 +00:00
416761f8f7 feat(push): M7 backend — opt-in push notifications via self-hosted ntfy
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m37s
PR Checks / client-build (pull_request) Successful in 9m21s
PR Checks / bot-install (pull_request) Successful in 9m17s
Additive, v1-only backend contract for the Android app's opt-in push (Part 1 of
M7; docs/android/PLAN.md §11). The app is a pure consumer — this lands the
endpoints, fan-out, and relay it needs.

- Schema: push_devices (per-device endpoint) + notification_subscriptions
  (per-user opted-in streams), FK→users ON DELETE CASCADE.
- Stream catalog + event→stream mapping (config/notificationStreams.js): public
  streams (news.post, server.status, idoc.warning, champ.start, governor.election)
  drawn ONLY from the SSE PUBLIC_KINDS allowlist; personal owner-keyed streams
  (vendor.sale, house.idoc, account.login). Full-state upserts (champ/city) fire
  only on a real transition via an injectable tracker.
- Fan-out (utils/pushDispatch.js): content-free tickles ({ stream, ref }) POSTed
  to each subscribed device; never throws. Two producers — shardIngest.ingest
  (beside the SSE broadcast) and the create/publish-post path (news.post).
  Personal events resolve to the owner via shardLinks. SSRF guard: endpoints must
  be HTTPS, non-private, and on the NTFY_BASE_URL/NTFY_ALLOWED_ORIGINS allow-set —
  enforced at registration and every publish.
- Routes under the role-agnostic self surface (never /admin): POST|GET
  /auth/me/devices, DELETE /auth/me/devices/:id, GET
  /auth/me/notifications/streams, GET|PUT /auth/me/notifications/subscriptions.
  Swagger regenerated (4 paths, PushDevice/NotificationStreams/etc. schemas).
- ntfy service in docker-compose.yml: pinned image, declarative ./ntfy/server.yml,
  no published host port, anonymous unguessable topics (no accounts) — zero
  interactive setup. No publish token required (content-free design); optional
  NTFY_PUBLISH_TOKEN honored.
- Tests: pushDispatch (mapping, PUBLIC_KINDS gate, owner-keying, SSRF guard,
  content-free payload) + notifications route auth gate. Full suite green (247).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 05:13:48 -05:00
030414f13d Merge pull request 'feat(public): version/health surfacing + typed brand block' (#77) from feat/public-version-and-brand into main
All checks were successful
Build container images / build (push) Successful in 1m2s
Build container images / deploy (push) Successful in 33s
Reviewed-on: #77
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-19 17:38:20 +00:00
c35509e8b3 feat(public): type the brand block so mobile clients get typed theming
All checks were successful
PR Checks / client-build (pull_request) Successful in 9m29s
PR Checks / server-tests (pull_request) Successful in 10m30s
PR Checks / bot-install (pull_request) Successful in 9m21s
Branding is already returned by GET /public/settings (the `brand` block:
name/colors/logo/hero/favicon, per-shard from BRAND_*). §8.6 of the Android
plan asks to confirm it — this makes it a first-class part of the contract so
the app's OpenAPI codegen produces typed branding instead of an untyped map.

- Swagger: add Brand + PublicSettings schemas; /public/settings now references
  PublicSettings (was additionalProperties:true). Brand documents that asset
  fields may be site-relative paths (resolve against the base URL).
- test/publicBrand.test.js locks the brand theming contract the app depends on
  (all fields present; BRAND_* defaults; admin site_title/contact_email
  overrides; accentInt never leaked).

No behavior change to the response — it already carried `brand`; this types and
guards it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
2026-07-19 11:58:01 -05:00
90c8eae20f feat(public): version/health surfacing for the app first-run probe
Expose a small backend identity/version descriptor (§8.4 of the Android plan)
so a client can positively recognize a Runic Gateway backend on first-run and
run a version-mismatch guard, instead of inferring from an incidental shape.

- New config/version.js: { service: 'runic-gateway', api: 'v1', server: <pkg> }.
- GET /public/status now includes a `version` block (the app already calls this
  on first-run, so it gets identity + version in one round trip).
- New GET /public/version: a lightweight, DB-free identity endpoint — the
  canonical target for the version guard and a cheap liveness check.
- Swagger: PublicVersion schema + version on PublicStatus; /version annotated.
- test/publicVersion.test.js covers the config shape and the DB-free 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
2026-07-19 11:47:26 -05:00
93a2c0d55f Merge pull request 'feat(auth): role-agnostic self-service surface under /auth/me' (#76) from feat/auth-me-self-surface into main
All checks were successful
Build container images / build (push) Successful in 1m4s
Build container images / deploy (push) Successful in 34s
Reviewed-on: #76
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-19 16:34:40 +00:00
fc5255da99 feat(auth): role-agnostic self-service surface under /auth/me
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m27s
PR Checks / client-build (pull_request) Successful in 10m23s
PR Checks / bot-install (pull_request) Successful in 9m19s
Add /auth/me/account* — the canonical "me" endpoints for every authenticated
role (Android app §6.4/§8.1). Reuses the existing account.controller handlers
(getAccount, changeUsername, changePassword, TOTP setup/enable/disable, list/
unlink identities) verbatim behind requireAuth (any role) — no logic
duplication. The app gets one self surface and never has to touch /admin; the
old /player/account/* and /admin/account/* routes stay for web back-compat.

New routes (all bearer- or cookie-auth, any active role):
- GET    /auth/me/account
- PATCH  /auth/me/account/username
- PATCH  /auth/me/account/password
- POST   /auth/me/account/totp/setup|enable|disable
- GET    /auth/me/account/identities
- DELETE /auth/me/account/identities/:provider

Mounted as a sub-router; the bare GET /auth/me is unchanged. Swagger
regenerated with #swagger annotations. Adds test/authMe.test.js (the group
gate rejects unauthenticated callers with 401).

Verified end-to-end against MariaDB: a player and an editor both drive the
same surface (role-agnostic), username/password changes work, a password
change revokes the caller's old bearer token, and validation/401 paths behave.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
2026-07-19 04:55:01 -05:00
715eaedd74 Merge pull request 'feat(auth): self-service password reset (backend + web)' (#75) from feat/password-reset into main
Some checks failed
Build container images / build (push) Successful in 1m14s
Build container images / deploy (push) Failing after 10m47s
Reviewed-on: #75
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-19 09:36:35 +00:00
250cb1e2d3 Merge branch 'main' into feat/password-reset
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m59s
PR Checks / client-build (pull_request) Successful in 9m26s
PR Checks / bot-install (pull_request) Successful in 9m31s
2026-07-19 09:04:43 +00:00
10aed49bb6 feat(auth): self-service password reset (backend + web)
All checks were successful
PR Checks / client-build (pull_request) Successful in 9m45s
PR Checks / server-tests (pull_request) Successful in 10m42s
PR Checks / bot-install (pull_request) Successful in 9m21s
Add a full password-reset flow — the prerequisite for the Android app
(docs/android/PLAN.md §8.2), which hands off to the website for reset
rather than shipping a native screen.

Backend:
- password_resets table: stores only the sha256 hash of an opaque 32-byte
  token (mirrors user_invites / mobile_refresh_tokens), single-use, ~1h TTL.
- model/passwordResets + users.getActiveByEmail (email is non-unique, so a
  request can match several accounts, each emailed its own link).
- mailer.sendPasswordReset (fails soft when email is unconfigured).
- Endpoints: POST /auth/password/forgot (always a generic 200 — no account
  enumeration), GET|POST /auth/password/reset/:token. Confirming rotates the
  hash and revokes every session (web cutoff + mobile refresh tokens); it does
  not auto-login, so a 2FA account still passes TOTP next sign-in. Also serves
  SSO-only accounts (null hash) as their set-initial-password path.
- Dedicated request/confirm rate limiters. Swagger regenerated.

Web:
- ForgotPassword + ResetPassword pages, routes /account/forgot and
  /account/reset/:token, and a "Forgot your password?" link on the login page.

Tests: test/passwordResets.test.js (5). All server tests pass; client builds;
end-to-end smoketest against MariaDB passes (no-enumeration, single-use, hash
rotation, session revoke, login with the new password).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
2026-07-19 03:57:13 -05:00
896773b8a5 Merge pull request 'Moderation appeals (Phase 6c) + Discord auto-reversal (Phase 6d)' (#74) from feature/moderation-appeals into main
All checks were successful
Build container images / build (push) Successful in 1m32s
Build container images / deploy (push) Successful in 37s
Reviewed-on: #74
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-19 05:18:18 +00:00
60e6a60842 Merge branch 'main' into feature/moderation-appeals
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m35s
PR Checks / client-build (pull_request) Successful in 9m51s
PR Checks / bot-install (pull_request) Successful in 9m22s
2026-07-19 03:49:39 +00:00
9ac1f35fa0 Merge pull request 'docs: generalize deployment section to any reverse proxy' (#73) from docs/reverse-proxy-generic into main
All checks were successful
Build container images / build (push) Successful in 1m27s
Build container images / deploy (push) Successful in 36s
Reviewed-on: #73
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-19 03:20:01 +00:00
028ba8c5e4 feat(moderation): appeals (6c) + Discord reversal on approve (6d)
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m59s
PR Checks / client-build (pull_request) Successful in 9m32s
PR Checks / bot-install (pull_request) Successful in 9m37s
Players whose linked Discord identity was banned or muted can now submit
an appeal from the portal and track it; staff get a queue in the admin
moderation section to claim and resolve (approve/deny) appeals. Approving
a ban/mute appeal best-effort asks the Discord bot to reverse the action
(unban / clear timeout) via the internal API and posts a mod-log embed; a
down bot never fails the resolution (reversal_status is recorded).

- Schema: new server-owned `appeals` table (no cross-owner FK to
  mod_actions; existence validated in app code).
- Server: model/appeals/* + player appeals controller (submit/mine/
  eligible/withdraw) and admin queue handlers (list/claim/resolve/
  per-user) under the existing admin+moderator gate; one-active-appeal
  enforced app-side; eligibility keyed on the caller's linked Discord id.
- 6d: bot POST /internal/mod-reverse (+ modLog.postReversal) and
  server botInternalClient.reverseModAction, wired into resolve().
- Client: admin Appeals queue + resolve modal, ModerationUser appeals
  tab, player Appeals page (submit/withdraw), nav + routes + api methods.
- Docs: swagger annotations + component schemas, regenerated output.
- Tests: appeals controller + pure suites (server npm test 224 green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmHdsbnLzDMAVQkAoTQSBe
2026-07-18 22:01:06 -05:00
5f09ab1146 docs: generalize deployment section to any reverse proxy
All checks were successful
PR Checks / client-build (pull_request) Successful in 9m25s
PR Checks / server-tests (pull_request) Successful in 10m10s
PR Checks / bot-install (pull_request) Successful in 9m21s
Rework the README's Pangolin-specific deployment section to cover any
reverse proxy (Nginx, Caddy, Traefik, Pangolin). Add TRUST_PROXY /
X-Forwarded-* guidance and Nginx + Caddy config snippets, keeping
Pangolin as one documented example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 22:00:01 -05:00
b0549f5845 Merge pull request 'fix(shard): restrict staff in-game location to admins/moderators' (#72) from fix/staff-location-visibility into main
All checks were successful
Build container images / build (push) Successful in 59s
Build container images / deploy (push) Successful in 38s
PR Checks / client-build (pull_request) Successful in 9m43s
PR Checks / server-tests (pull_request) Successful in 10m35s
PR Checks / bot-install (pull_request) Successful in 9m24s
Reviewed-on: #72
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-19 02:39:22 +00:00
aa2177715e fix(shard): restrict staff in-game location to admins/moderators
All checks were successful
PR Checks / client-build (pull_request) Successful in 9m23s
PR Checks / server-tests (pull_request) Successful in 10m33s
PR Checks / bot-install (pull_request) Successful in 9m18s
The public "Staff online" list on the Shard page exposed each staff
member's in-game location (map + coordinates) to everyone, including
logged-in players and unauthenticated visitors.

Location is now privileged data:
- Server: getOnline inspects the caller's role via getUserFromRequest
  (the same non-rejecting helper siteMode uses on public routes) and
  only includes map/x/y/z for admin/moderator callers. For everyone
  else the fields are omitted from the JSON entirely, so they can't be
  read from the network tab. serial + name (online status) still shown.
- Client: Shard.jsx gates the location span on the viewer's role from
  useAuth() (same pattern as RoleGate); non-privileged viewers see who
  is online but no location field is rendered.

Tests: publicShardOnline.test.js covers admin + moderator (location
included), player + unauthenticated + editor (location omitted).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmHdsbnLzDMAVQkAoTQSBe
2026-07-18 21:18:52 -05:00
3e6ecb959a Merge pull request 'chore: add open-source governance files (GPLv3 + contributing docs)' (#71) from chore/open-source-governance into main
All checks were successful
Build container images / build (push) Successful in 54s
Build container images / deploy (push) Successful in 34s
Reviewed-on: #71
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-19 00:31:03 +00:00
Claude
d441e92029 chore: add open-source governance files (GPLv3 + contributing docs)
All checks were successful
PR Checks / client-build (pull_request) Successful in 9m24s
PR Checks / server-tests (pull_request) Successful in 10m32s
PR Checks / bot-install (pull_request) Successful in 9m20s
Add standard open-source project files:
- LICENSE.md — GNU GPL v3.0 or later (verbatim)
- CONTRIBUTING.md — setup, workflow, and required AI-usage disclosure
- CONTRIBUTORS.md — maintainers, contributors, AI-assistance policy
- CODE_OF_CONDUCT.md — Contributor Covenant 2.1
- SECURITY.md — private vulnerability reporting
- .gitea/ISSUE_TEMPLATE/* + PULL_REQUEST_TEMPLATE.md
- README: License section (Copyright (C) 2026 Runic Gateway)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmHdsbnLzDMAVQkAoTQSBe
2026-07-18 19:10:55 -05:00
5639117936 Merge pull request 'fix(brand): link "Runic Gateway" footer badge to Gitea org' (#70) from fix/branding-footer-link into main
All checks were successful
Build container images / build (push) Successful in 1m11s
Build container images / deploy (push) Successful in 33s
Reviewed-on: #70
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-18 23:36:46 +00:00
50133155d6 fix(brand): link "Runic Gateway" footer badge to Gitea org
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m27s
PR Checks / client-build (pull_request) Successful in 10m16s
PR Checks / bot-install (pull_request) Successful in 9m19s
Wrap the "Powered by Runic Gateway" wordmark in an anchor pointing to
https://gitea.whitlocktech.com/RunicGateway (new tab, noopener). Adds a
subtle accent-color hover on the wordmark.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmHdsbnLzDMAVQkAoTQSBe
2026-07-18 18:16:51 -05:00
bdce23f9b6 Merge pull request 'feat(brand): default emblem — favicon, hero medallion, footer credit' (#69) from feature/branding into main
All checks were successful
Build container images / build (push) Successful in 1m57s
Build container images / deploy (push) Successful in 2m3s
Reviewed-on: #69
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-18 22:20:27 +00:00
a0a70ce2ca docs: rename project-structure root label UOMSITE/ → website/
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m48s
PR Checks / client-build (pull_request) Successful in 10m48s
PR Checks / bot-install (pull_request) Successful in 9m21s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmHdsbnLzDMAVQkAoTQSBe
2026-07-18 17:00:04 -05:00
526160721a feat(brand): emblem behind hero text + Powered By footer with logo
Address review: the SVG footer badge rendered too small, and the default
hero should feature the Runic Gateway emblem rather than the moon.

- Footer: drop powered-by.svg; render the emblem PNG beside a "Powered by
  Runic Gateway" label (left-justified, info text stays centered).
- Default hero: brand.hero (and the client fallbacks) now default to the
  emblem PNG. The untouched default hero centers the square emblem behind
  the text as a medallion with a symmetric legibility overlay (per-layer
  background-size so the overlay stays full-bleed). BRAND_HERO still
  overrides.
- Admin login / player shell / maintenance backgrounds center the emblem
  as a capped medallion instead of a cropped full-bleed cover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmHdsbnLzDMAVQkAoTQSBe
2026-07-18 16:56:22 -05:00
352ae4f256 feat(brand): default favicon emblem + Powered By footer badge
Ship the Runic Gateway emblem as the baked-in default favicon so an
instance renders a tab icon with no BRAND_FAVICON set, and place a
left-justified "Powered By" badge in the site footer.

- brand.favicon now defaults to /assets/img/favicon.ico (was empty).
  BRAND_FAVICON still overrides per-instance.
- Add favicon.ico + powered-by.svg under client/public/assets/img.
- SiteFooter: badge pinned to the left of the centered content column
  (absolute on >=641px, stacked on mobile); info text stays centered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmHdsbnLzDMAVQkAoTQSBe
2026-07-18 16:40:09 -05:00
a16092f13a Merge pull request 'feat(brand): BRAND_* env scheme — instance branding without a rebuild' (#68) from feature/brand-env into main
All checks were successful
Build container images / build (push) Successful in 2m13s
Build container images / deploy (push) Successful in 24s
Reviewed-on: #68
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-18 07:40:04 +00:00
7a08546da6 feat(brand): BRAND_* env scheme — instance branding without a rebuild
All checks were successful
PR Checks / client-build (pull_request) Successful in 9m24s
PR Checks / server-tests (pull_request) Successful in 10m33s
PR Checks / bot-install (pull_request) Successful in 9m20s
Replace baked-in UOM/MysticMoon/UOMysticmoon branding with a BRAND_* env
scheme so one prebuilt image runs as any shard; UOMysticmoon becomes the
first tenant that sets these vars rather than a special case in the code.

Architecture (chosen because the app ships as a prebuilt image):
- server/src/config/brand.js + bot/src/brand.js read BRAND_* once at boot,
  with Runic Gateway defaults.
- Text/colors reach the SPA at RUNTIME through the existing public settings
  API (settings.model.getPublic -> SiteContext), so no client rebuild. The
  admin-editable site title + contact email still override BRAND_NAME/email.
- SiteContext applies BRAND_ACCENT_COLOR to the --accent CSS var at runtime.
- Express templates the built index.html <title>/description/OG/favicon at
  serve time from BRAND_* (renderIndexHtml in app.js).
- Server-side consumers read brand directly: emails, TOTP issuer, API docs,
  boot logs, HTML error page. Bot uses it for embed color + logs.

Assets: logo/hero/favicon delivered from a ./brand:/app/brand bind-mount
(BRAND_LOGO/HERO/FAVICON), with neutral defaults baked in; hero falls back
to a built-in image when unset.

Scope: also genericized package.json names (uomysticmoon-* -> runic-gateway-*)
and the DB_NAME/DB_USER/COOKIE_NAME code defaults (runic_gateway/runic/
rg_token). Production keeps its real values by pinning them in .env — see
.env.uomysticmoon.example, which reproduces the exact UOMysticmoon identity
(proof the substitution works). Changing a deployed COOKIE_NAME invalidates
existing sessions, so UOMysticmoon pins uomm_token.

Verified: 193 server tests pass, client builds, app.js loads + templates the
built index.html, brand transform injects title/description/OG/favicon.
2026-07-18 02:20:04 -05:00
1bb9e3c3c3 Merge pull request 'docs: move design docs to RunicGateway/docs' (#67) from chore/extract-docs into main
Some checks failed
Build container images / build (push) Failing after 28s
Build container images / deploy (push) Has been skipped
Reviewed-on: #67
2026-07-18 05:51:30 +00:00
b8f67fb208 Merge branch 'main' into chore/extract-docs
All checks were successful
PR Checks / client-build (pull_request) Successful in 9m32s
PR Checks / server-tests (pull_request) Successful in 9m55s
PR Checks / bot-install (pull_request) Successful in 9m19s
2026-07-18 05:27:59 +00:00
e0fadbdcc2 Merge pull request 'chore(org): retarget org paths to RunicGateway' (#66) from chore/org-rename-runicgateway into main
All checks were successful
Build container images / build (push) Successful in 1m14s
Build container images / deploy (push) Successful in 26s
Reviewed-on: #66
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-18 05:06:43 +00:00
b3033909d3 docs: move design docs to RunicGateway/docs
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m49s
PR Checks / client-build (pull_request) Successful in 9m28s
PR Checks / bot-install (pull_request) Successful in 9m30s
Extracted BACKEND_DESIGN.md, HERO_EDITOR.md, and WIKI_UPGRADE.md into the
central RunicGateway/docs repo (under docs website/, full history preserved
via git filter-repo). Repoint the README's two BACKEND_DESIGN.md links at
the new location.

Docs repo: https://gitea.whitlocktech.com/RunicGateway/docs
2026-07-18 00:05:51 -05:00
6c967d9a6c chore(org): retarget org paths to RunicGateway
All checks were successful
PR Checks / client-build (pull_request) Successful in 9m30s
PR Checks / server-tests (pull_request) Successful in 10m34s
PR Checks / bot-install (pull_request) Successful in 9m21s
Repo was transferred UOM -> RunicGateway. build-images.yml already
derives its registry owner from github.repository_owner, so it now
publishes to gitea.whitlocktech.com/runicgateway/*, but docker-compose
still pulled from /uom/*. Point the app+bot images at the new owner so
deploys pull the images CI actually publishes. Also update the two
README links to the uo-link repo (UOM/link -> RunicGateway/link).

No branding text touched (that is handled separately).
2026-07-17 23:46:36 -05:00
ee085496ab Merge pull request 'Protocol 2.0/2.1 uo-link integration — boards, cross-links, news gump, account provisioning' (#65) from feature/protocol2-integration into main
All checks were successful
Build container images / build (push) Successful in 1m35s
Build container images / deploy (push) Successful in 35s
Reviewed-on: UOM/website#65
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-18 02:34:25 +00:00
3ef1c8e438 feat(provisioning): admin game-signup mode setting, invite link option, staff self-create
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m37s
PR Checks / client-build (pull_request) Successful in 10m18s
PR Checks / bot-install (pull_request) Successful in 9m22s
Follow-ups from live testing:

- Game-account creation is now an admin Settings control (disabled / website /
  hybrid / game) instead of a hidden on/off flag. The site offers creation for
  website+hybrid; help text notes the shard's SignupMode (Bridge.cfg) has the final
  say. game_account_signup setting widened to a 4-value enum + validated on save.
- Invites: the accept link is ALWAYS returned and shown with a Copy button, and a
  "Email the invitation" toggle lets an admin create a link-only invite (no email)
  or email it. Backend takes sendEmail (default true) and always returns acceptUrl.
- Staff can create a game account from their own /admin/characters page too
  (POST /admin/shard/account → the shared createGameAccount controller), so the
  form is reachable in both the player and admin portals.

Note: the admin Houses view (/admin/houses) already worked; the earlier failure
was a stale Vite HMR state for the new route (needs a hard refresh).

Client build clean; server routes load; swagger regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 21:08:59 -05:00