task/33: Relay admin panel at /admin/relay (final, all review fixes applied)
## 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 ✓.
This commit is contained in:
@@ -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 ──────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user