Skip to content

feat(admin): secure /admin/stats + /admin/usage with industrial API key#19

Merged
simonabler merged 5 commits intomasterfrom
19-feat-admin-stats-api-key-auth
Mar 11, 2026
Merged

feat(admin): secure /admin/stats + /admin/usage with industrial API key#19
simonabler merged 5 commits intomasterfrom
19-feat-admin-stats-api-key-auth

Conversation

@simonabler
Copy link
Copy Markdown
Owner

Backend:

  • MetricsController: prefix _stats → admin/stats, all 4 endpoints get @RequiresTier('industrial'). ApiKeyModule imported into MetricsModule.
  • UsageController: prefix _usage → admin/usage, x-admin-token header auth replaced with @RequiresTier('industrial'). ApiKeyModule imported into UsageModule dynamic module.
  • UsageInterceptor: /admin/* paths are skipped entirely — admin tooling must never be blocked by its own rate-limit counters.
  • app.module.ts: adminToken option removed (no longer referenced).

Frontend:

  • StatsService: URLs updated to /api/admin/stats. apiKey signal persisted in sessionStorage (key: admin_api_key). All requests send x-api-key header from the signal value. setApiKey() updates signal + sessionStorage.
  • StatsDashboardComponent: statsService exposed as public readonly. onApiKeyChange() wired to new password input in template.
  • stats-dashboard.component.html: API key input row added at top — shows green 'Key set' badge when a key is present, amber warning when not. Input calls onApiKeyChange and triggers immediate refresh.

Simon Abler added 5 commits March 11, 2026 09:07
Backend:
- MetricsController: prefix _stats → admin/stats, all 4 endpoints get
  @RequiresTier('industrial'). ApiKeyModule imported into MetricsModule.
- UsageController: prefix _usage → admin/usage, x-admin-token header
  auth replaced with @RequiresTier('industrial'). ApiKeyModule imported
  into UsageModule dynamic module.
- UsageInterceptor: /admin/* paths are skipped entirely — admin tooling
  must never be blocked by its own rate-limit counters.
- app.module.ts: adminToken option removed (no longer referenced).

Frontend:
- StatsService: URLs updated to /api/admin/stats. apiKey signal persisted
  in sessionStorage (key: admin_api_key). All requests send x-api-key
  header from the signal value. setApiKey() updates signal + sessionStorage.
- StatsDashboardComponent: statsService exposed as public readonly.
  onApiKeyChange() wired to new password input in template.
- stats-dashboard.component.html: API key input row added at top —
  shows green 'Key set' badge when a key is present, amber warning when
  not. Input calls onApiKeyChange and triggers immediate refresh.
Replaces the RequiresTier('industrial') approach with a dedicated static
admin key that lives exclusively in the environment — no DB lookup, no
tier hierarchy, no way for an API customer to accidentally gain access.

api-key.decorator.ts:
  REQUIRES_ADMIN_KEY metadata key + RequiresAdminKey() decorator added.

api-key.guard.ts:
  First check in canActivate() handles @RequiresAdminKey() before any
  tier logic. Uses constant-time Buffer comparison (prevents timing
  attacks). Throws 403 if ADMIN_KEY env var is not set (server
  misconfiguration), 401 if key is wrong or missing.

metrics.controller.ts / usage.controller.ts:
  @RequiresTier('industrial') → @RequiresAdminKey() on all endpoints.

metrics.module.ts / usage.module.ts:
  ApiKeyModule import removed — admin check is pure env, no ORM needed.

.env.example:
  ADMIN_KEY documented with generation hint (openssl rand -hex 32).
Backend:
- api-key-admin.controller.ts: prefix _admin/api-keys → admin/api-keys,
  x-admin-token header auth replaced with @RequiresAdminKey() on all
  three endpoints (GET list, POST create, DELETE revoke).

Frontend:
- api-keys.service.ts (new): list(), create(), revoke() — all requests
  include x-api-key from shared StatsService.apiKey() signal.
- api-keys.component.ts (new): signals-based component — loads all keys
  on init, create form (label, tier select, optional expiry date), inline
  revoke with two-step confirmation, raw-key banner after creation with
  copy button. Shares the admin key input with StatsService.
- api-keys.component.html (new): Bootstrap card layout — create form row,
  table with label/prefix/tier/status/expiry/created columns, revoke
  button with inline confirm (Yes/No). Tier badges color-coded
  (free=grey, pro=blue, industrial=amber).
- app.routes.ts: /admin/api-keys route lazy-loads ApiKeysComponent.
- stats-dashboard + api-keys templates: shared tab nav (📊 Stats /
  🔑 API Keys) with routerLinkActive styling added to both pages.
- stats-dashboard.component.ts: RouterLink + RouterLinkActive imported.
AnomalyGuard runs as APP_GUARD before every request. Without
@SkipAnomalyGuard() the admin dashboard auto-refresh (5s interval)
and repeated GET /admin/stats/security calls could trigger the
burst_per_minute threshold, banning the admin's own IP — which would
then prevent viewing the blocklist they just ended up on.

Added @SkipAnomalyGuard() at class level to:
- MetricsController  (/admin/stats/*)
- UsageController    (/admin/usage/*)
- ApiKeyAdminController (/admin/api-keys/*)

The guard already supports class-level skipping via
reflector.get(SKIP_SECURITY, context.getClass()).
…ick Start

Quick Start (3 cards) and API Catalog (5 cards) were redundant and both
incomplete. Merged into a single unified catalog section:

- 7 catalog cards: QR, Barcodes, GS1, Crypto, Watermark, Signpack, Dev Utils
- Each card gets an optional Try tab showing a curl snippet + copy button
- Info tab shows a short description (new field on CatalogCard)
- Routes list always visible regardless of active tab
- Quick Start section removed entirely
- ServiceCard type removed; catalog data is single source of truth
- setTab() now operates on catalog[] instead of services[]
- catalog-tabs, catalog-desc, catalog-try styles added to SCSS
- GS1 ctaSecondary uses external:true href (mailto) — no routerLink
@simonabler simonabler merged commit 3e29900 into master Mar 11, 2026
1 of 2 checks passed
@simonabler simonabler deleted the 19-feat-admin-stats-api-key-auth branch March 11, 2026 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant