From 2d06f8e513f0b60a5b62e7109510c3015b162aad Mon Sep 17 00:00:00 2001 From: Arlind Ukshini Date: Fri, 24 Apr 2026 11:23:12 +0200 Subject: [PATCH] /fenjaops: add Remove button for non-admin invites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New endpoint: DELETE /api/fenjaops/invites/:email, behind requireAuth + requireAdmin. Guardrails: * refuses if the target email is an admin (demote first via bin/invite.js admin remove) — preserves the invariant that a compromised admin session can't lock everyone out; * refuses if the target email equals the caller's own — prevents self-inflicted lockouts from the UI; * deletes active sessions for the target email so the user is kicked out immediately instead of holding their 30-day cookie. - Admin page: Invites table gains an "Action" column. Non-admin, non-self rows show a Remove button (quiet ink outline; crimson on hover to cue destructive intent). Admin and self rows show an em-dash. Click → browser confirm() → DELETE → load() to refresh counts + tables. - admin.js fetches /auth/me alongside the other payloads so render can compare each row's email against the viewer's. - PROJECT.md and CLAUDE.md updated: the "no web deletion" invariant is narrowed to "no web deletion of admins or self" to reflect the new capability. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 2 +- PROJECT.md | 2 +- admin/admin.css | 30 ++++++++++++++++++++ admin/admin.js | 71 +++++++++++++++++++++++++++++++++++++++++++++++- admin/index.html | 2 +- server.js | 27 ++++++++++++++++++ 6 files changed, 130 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0ed9cce..9ba8051 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,7 +43,7 @@ Single-process Express app bound to `127.0.0.1:3000`. Nginx is the only public i `bifrost_joins` logs every click of the final "Join Project Bifrost" CTA — one row per click (auto-increment `id`, `email`, `clicked_at`, `session_id`). Writes come from `POST /api/bifrost-join` (behind `requireAuth`); reads come from `bin/joins.js`. See OPERATIONS.md for admin usage. -**Hidden admin page** at `/fenjaops` (deliberately obscure URL, not `/admin`) — gated by `requireAuth` + `requireAdmin` (`is_admin` column on `invites`). Non-admins get a plain 404 so the URL's existence isn't leaked. Files live in `admin/` at the repo root (outside `public/` and `protected/` so only the explicit route reaches them). Admins can create **non-admin** invites from the page (`POST /api/fenjaops/invites` → stores `is_admin=0`, audit trail records the acting admin's email in `invited_by`). Promotion to admin and removal of invites stay **CLI-only** via `bin/invite.js admin add|remove|list` — a web session compromise cannot escalate the invite list. Internal code keeps the word "admin" (middleware, files, CLI); only the public URL is obscured. +**Hidden admin page** at `/fenjaops` (deliberately obscure URL, not `/admin`) — gated by `requireAuth` + `requireAdmin` (`is_admin` column on `invites`). Non-admins get a plain 404 so the URL's existence isn't leaked. Files live in `admin/` at the repo root (outside `public/` and `protected/` so only the explicit route reaches them). Admins can **create** non-admin invites (`POST /api/fenjaops/invites`, stores `is_admin=0`, audit trail records the acting admin in `invited_by`) and **remove** non-admin invites from the page (`DELETE /api/fenjaops/invites/:email`; rejects removing admins or oneself, also kills any active sessions for the deleted email). Admin promotion / demotion stays CLI-only (`bin/invite.js admin add|remove|list`) so a web session compromise cannot escalate or lock everyone out. Internal code keeps the word "admin" (middleware, files, CLI); only the public URL is obscured. **Rate limiting** (`src/middleware.js`) is a SQLite-backed sliding window keyed per-IP — 5 code requests/hour, 20 verify attempts/hour. Nginx adds another layer via a `limit_req_zone` declared in `/etc/nginx/nginx.conf`. diff --git a/PROJECT.md b/PROJECT.md index 7a9481d..bba7038 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -103,7 +103,7 @@ These things define the security model. Breaking any of them is a regression eve - **No inline `