feat(admin): secure /admin/stats + /admin/usage with industrial API key#19
Merged
simonabler merged 5 commits intomasterfrom Mar 11, 2026
Merged
feat(admin): secure /admin/stats + /admin/usage with industrial API key#19simonabler merged 5 commits intomasterfrom
simonabler merged 5 commits intomasterfrom
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backend:
Frontend: