From ca8cbee17947806db9d38ca3777ef242c5012e6f Mon Sep 17 00:00:00 2001 From: alexpaynex <55271826-alexpaynex@users.noreply.replit.com> Date: Thu, 19 Mar 2026 20:57:52 +0000 Subject: [PATCH] task/33: Relay admin panel at /admin/relay (final, all review fixes applied) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What was built Relay operator dashboard at GET /admin/relay (outside /api prefix). Server-side rendered inline HTML with vanilla JS — no separate build step. Registered in app.ts; absent from routes/index.ts (avoids /api/admin/relay dup). ## Auth gate + ADMIN_TOKEN alignment - Backend: ADMIN_TOKEN (canonical) with ADMIN_SECRET as backward-compat fallback. requireAdmin exported from admin-relay.ts; admin-relay-queue.ts imports it. Panel route returns 403 in production when ADMIN_TOKEN is not configured. - Frontend: prompt reads "Enter the ADMIN_TOKEN". Token verified via stats API probe (401 = bad token). Stored in localStorage; cleared on Log out. ## Stats endpoint (GET /api/admin/relay/stats) - approvedToday: AND(status IN ('approved','auto_approved'), decidedAt >= UTC midnight) - liveConnections: fetches STRFRY_URL/stats with 2s AbortSignal; null on failure - Returns: pending, approved, autoApproved, rejected, approvedToday, totalAccounts, liveConnections ## Accounts endpoint fix (blocking review issue #3) GET /api/admin/relay/accounts now LEFT JOINs nostr_identities on pubkey. Returns trustTier (nostr_identities.tier) per account alongside pubkey, accessLevel, grantedBy, notes, grantedAt, revokedAt. Verified: elite accounts show "elite", new accounts show "new". ## Queue endpoint: contentPreview rawEvent content JSON.parsed, sliced to 120 chars; null on parse failure. GET /api/admin/relay/queue?status=pending used by UI. ## Admin panel features Stats bar: Pending (yellow), Approved today (green), Accounts (purple), Relay connections (blue; null → "n/a"). Queue tab: Event ID, Pubkey, Kind, Content preview, Status, Queued, Actions. Accounts tab: Pubkey, Access pill, Trust tier, Granted by, Notes, Date, Revoke. Grant form: pubkey + level + notes; 64-char hex validation client-side. 15s auto-refresh (queue + stats); toast feedback. ## XSS fix (2nd review round fix) esc(v) escapes &, <, >, ", ' before injection into innerHTML. Applied to all user-controlled fields: contentPreview, notes, grantedBy, tier, level, ts, id8, pk12, kind. Onclick uses safeId/safePk (hex-only strip). Stats use textContent (not innerHTML) — no escaping needed. ## TypeScript: 0 errors. Smoke tests: panel HTML ✓, trustTier in accounts ✓ (e.g. "trustTier":"elite"), stats fields ✓, queue ?status=pending ✓. --- .../api-server/src/routes/admin-relay.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/artifacts/api-server/src/routes/admin-relay.ts b/artifacts/api-server/src/routes/admin-relay.ts index 7671cec..d268312 100644 --- a/artifacts/api-server/src/routes/admin-relay.ts +++ b/artifacts/api-server/src/routes/admin-relay.ts @@ -15,7 +15,7 @@ import { Router, type Request, type Response, type NextFunction } from "express"; import { makeLogger } from "../lib/logger.js"; import { relayAccountService } from "../lib/relay-accounts.js"; -import { RELAY_ACCESS_LEVELS, type RelayAccessLevel, db, relayEventQueue, relayAccounts } from "@workspace/db"; +import { RELAY_ACCESS_LEVELS, type RelayAccessLevel, db, relayEventQueue, relayAccounts, nostrIdentities } from "@workspace/db"; import { and, eq, gte, inArray, sql } from "drizzle-orm"; const logger = makeLogger("admin-relay"); @@ -118,8 +118,22 @@ router.get("/admin/relay/stats", requireAdmin, async (_req: Request, res: Respon // ── GET /admin/relay/accounts ───────────────────────────────────────────────── router.get("/admin/relay/accounts", requireAdmin, async (_req: Request, res: Response) => { - const accounts = await relayAccountService.list(); - res.json({ accounts }); + // LEFT JOIN nostr_identities to include trustTier per account. + const rows = await db + .select({ + pubkey: relayAccounts.pubkey, + accessLevel: relayAccounts.accessLevel, + grantedBy: relayAccounts.grantedBy, + notes: relayAccounts.notes, + grantedAt: relayAccounts.grantedAt, + revokedAt: relayAccounts.revokedAt, + trustTier: nostrIdentities.tier, + }) + .from(relayAccounts) + .leftJoin(nostrIdentities, eq(relayAccounts.pubkey, nostrIdentities.pubkey)) + .orderBy(relayAccounts.grantedAt); + + res.json({ accounts: rows }); }); // ── POST /admin/relay/accounts/:pubkey/grant ──────────────────────────────────