customer-presentation/CHECKLIST.md
Arlind Ukshini cbfb187d16 /fenjaops: admin-only form to invite non-admin users
- POST /api/fenjaops/invites on server.js (requireAuth+requireAdmin).
  Ignores any is_admin field in the body — always stores 0. Records
  the acting admin's email in invited_by so the audit trail shows
  who added whom (CLI adds still record "cli").
- admin/index.html: new "Invite a new user" form panel at the top
  (email + optional first name).
- admin/admin.js: wires the form submit to the POST, shows inline
  success/error, refreshes the tables on success.
- admin/admin.css: form styling matching the existing paper/ink
  palette; mobile stacks.
- Docs: CLAUDE.md, PROJECT.md, OPERATIONS.md, CHECKLIST.md, README.md
  all updated. New non-negotiable property in PROJECT.md: no web
  endpoint can set is_admin=1 or delete an invite — promotion +
  removal stay on bin/invite.js. New CHECKLIST.md section H2 covers
  the page's gating, the invite form, and an escalation-path audit.

Admin promotion and invite deletion remain CLI-only so a compromised
admin session cannot escalate or evict.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:07:47 +02:00

8.6 KiB

Checklist

Run the relevant section after every change before considering it done. Items are ordered: if an earlier one fails, stop and fix before testing the rest.

Notation: run on the VPS unless marked [local] or [browser].


A. After any code change (minimum viable smoke test)

  • sudo systemctl status fenjaactive (running)
  • sudo journalctl -u fenja -n 20 shows [bifrost] listening on 127.0.0.1:3000, no red errors (there is no [mail] SMTP relay reachable line anymore — the mail stack was removed)
  • [local] curl -I https://project-bifrost.fenja.ai/ → 200, with X-Frame-Options: DENY and Content-Security-Policy headers
  • [local] curl -I https://project-bifrost.fenja.ai/timeline.js → 302, Location: /
  • [browser, private window] Open https://project-bifrost.fenja.ai/ → entrance page renders (not timeline)

If all five pass, the site is up and the gate holds.


B. After changes to auth, sessions, cookies, middleware, or src/

Do section A, then:

  • [browser, private] Enter invited email → session cookie issued immediately, welcome step appears (no 6-digit code flow anymore)
  • [browser] Click "Learn more" on the welcome step → redirected to /timeline showing the timeline page
  • [browser] Hard-refresh (Ctrl+Shift+R) → stays on the timeline (cookie persists)
  • [browser, new private window] Visit / → entrance appears (no cookie leak between sessions)
  • [browser] Click logout → lands on entrance → visiting /timeline redirects to /
  • [browser] Visit /timeline.js or /vendor/d3-array.min.js directly while logged out → redirects to /
  • DevTools → Application → Cookies: fenja_session shows HttpOnly ✓, Secure ✓ (prod only), SameSite=Lax

C. After changes to the entrance form or login endpoint

  • Submit a non-invited address → inline "not invited" message, no session issued
  • Submit a malformed email (foo, foo@, empty) → inline error appears, no request sent
  • Submit > 30 login attempts from the same IP in an hour → rate-limit response (429)

D. After changes to the timeline / protected pages

  • [browser] Timeline loads fully: globe visible, 12 event cards, dot-nav at bottom, fonts render as true italic (not system oblique)
  • [browser] Scroll works smoothly, card reveal animations fire
  • [browser] Dot-nav switches between Timeline / Overview / Archive views
  • DevTools console: no CSP violations, no 404s for fonts/vendor files
  • DevTools network tab: all /vendor/* and /fenja/fonts/* requests return 200

E. After changes to CSP, security headers, or Nginx

Do section A with extra attention to:

  • Response headers on / include all six: X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, Content-Security-Policy, Strict-Transport-Security
  • CSP contains at minimum: default-src 'self', script-src 'self' (no unsafe-inline on scripts), frame-ancestors 'none'
  • sudo nginx -t → "syntax is ok" and "test is successful"
  • [local] Open timeline in a browser → no red CSP violations in DevTools console
  • curl.exe -X POST https://project-bifrost.fenja.ai/auth/login -H 'Content-Type: application/json' -d '{"email":"nobody@example.com"}' returns quickly (rate-limit zone is functioning; expect 403 not_invited on a real email not on the list)

F. After dependency or Node.js upgrades

  • sudo -u fenja npm ci --omit=dev completes without errors
  • sudo -u fenja npm audit reports no high/critical vulnerabilities in production deps
  • Run section A, then section B (full auth flow)
  • Check node --version on the VPS is still 20+

G. After Nginx config changes specifically

  • sudo nginx -t before reloading (catches 95% of errors)
  • ls /etc/nginx/sites-enabled/ contains only project-bifrost (no shadow configs)
  • After reload, curl -I https://project-bifrost.fenja.ai/ includes X-Powered-By: Express (proves Nginx is still proxying to Node, not serving static files)
  • Rate-limit zone still exists: grep -r "limit_req_zone" /etc/nginx/

H1. After changes to the Join-CTA tracking (bifrost_joins)

  • [browser, logged in] Click the final "Join Project Bifrost" CTA → confirmation panel renders (staggered checkmarks appear)
  • DevTools network tab: POST /api/bifrost-join returns 200 with {clicked_at: <ms>}
  • sudo -u fenja node /opt/fenja/bin/joins.js list shows a new row with your email and a current timestamp
  • Click the CTA a second time (refresh the page first): a second row appears — the log is per-click, not per-user
  • sudo -u fenja node /opt/fenja/bin/joins.js summary groups by email with correct click_count
  • sudo -u fenja node /opt/fenja/bin/joins.js stats totals match what you see in list
  • Logged out: curl -X POST https://project-bifrost.fenja.ai/api/bifrost-join → 401 (auth gate holds)

H2. After changes to the hidden admin page (/fenjaops)

Covers admin/, the /api/fenjaops/* endpoints, and requireAdmin.

Access / gating:

  • [browser, logged out] GET /fenjaops → 302 → / (not a leaky 404-that-sets-no-cookie)
  • [browser, logged in as non-admin] GET /fenjaops → plain 404 page (not a redirect, not a blank 403 — the existence of the URL must not be leaked)
  • [browser, logged in as admin] GET /fenjaops → page renders with stats, invite list (Admin badges present), join summary, raw click log, and the "Invite a new user" form at the top

Invite form — happy path:

  • Fill email with a brand-new address, leave first name blank → click "Send invite" → green "Invited …" confirmation; form clears
  • The invite list below refreshes and shows the new row with no Admin badge and invited_by = your admin email (not "cli")
  • sudo -u fenja node /opt/fenja/bin/invite.js list on the VPS shows the same row

Invite form — edge cases:

  • Submit an email already on the list → red "already on the invite list" message; no duplicate row added
  • Submit a malformed email (foo, foo@, empty) → red "not valid" message; no row added
  • Submit a first name >64 chars → red "too long" message; no row added
  • DevTools network: the request is POST /api/fenjaops/invites, Content-Type: application/json, 201 on success / 400 / 409 on failure

Escalation-path audit (do this after any change that touches server.js around /fenjaops or src/middleware.js):

  • curl -i -X POST https://project-bifrost.fenja.ai/api/fenjaops/invites -H 'Content-Type: application/json' -d '{"email":"foo@example.com","is_admin":1}' while logged in as admin → 201, then confirm on the VPS that the new row has is_admin = 0 (the body field must be ignored server-side)
  • Same POST while logged out → 401 or redirect; no row created
  • Same POST while logged in as non-admin → 404 from requireAdmin; no row created
  • There is no endpoint on the server that accepts is_admin, setInviteAdmin, or deleteInvite from HTTP — grep server.js to confirm the only write is the invite-creation POST

H. After data/DB changes (schema, migrations)

  • Fresh boot creates schema cleanly: stop service, mv data/fenja.sqlite data/fenja.sqlite.bak, restart, verify it comes up healthy
  • Restore the backup (mv it back) before inviting further users
  • Take a manual backup before deploying: sudo -u fenja sqlite3 /opt/fenja/data/fenja.sqlite ".backup /opt/fenja/data/pre-change-$(date +%F).sqlite"

I. Deploy readiness (before any push to production)

  • Ran locally: npm install && npm run dev, walked full flow end-to-end
  • No uncommitted .env file in the rsync source
  • No data/*.sqlite in the rsync source
  • On the VPS after deploy: section A passes
  • On the VPS after deploy: section B passes
  • Tailed journalctl -u fenja -f for 30s while refreshing the site → no errors

Red flags that always mean stop

  • curl -I / returns 200 without X-Powered-By: Express → Nginx is serving static, not proxying to Node. Site is broken.
  • curl -I /timeline.js returns 200 → the auth gate is down. Do not invite anyone.
  • Any Set-Cookie: fenja_session=... header that lacks HttpOnly or Secure (in prod) → cookie hardening regression.
  • journalctl -u fenja showing repeated crashes → something's wrong, systemctl restart fenja won't fix it.
  • The entrance page submits the form as a GET with ?email= in the URL → JS isn't running (usually CSP blocking a newly-added inline script).

If any of these happen, revert to the last known-good commit and investigate offline.