Repository navigation
Move the email and push gateway reports behind admin auth #433
Description
Summary
GET /api/email/health needs no authentication. It returns the mail provider name, pending and failed job counts, and sent_last_hour. GET /health/push does the same for push, plus vapid_subject. That is operational data an outsider should not see. The only consumer of both is the admin dashboard Notifications page, which needs the counts.
- Severity: Low (information leak)
- Area:
apps/hocuspocus.serveremail and health routes,apps/admin-dashboard - Source: security review of 2026-10-06, finding L13. The push route is the same leak, found while checking this one.
Where
- Email route:
apps/hocuspocus.server/src/api/email.ts:109-117. Data:apps/hocuspocus.server/src/lib/email/service.ts:90-101. - Push route:
apps/hocuspocus.server/src/api/routers/health.router.ts:18→checkPushHealth(apps/hocuspocus.server/src/api/controllers/health.controller.ts:49-54). Data:apps/hocuspocus.server/src/lib/push/service.ts:62-73. - The dashboard calls both from the browser with no
Authorizationheader:checkPushGatewayHealthandcheckEmailGatewayHealth,apps/admin-dashboard/src/services/api.ts:435-509. It showsproviderandpendingJobs(apps/admin-dashboard/src/pages/notifications.tsx:458-459,:584,:625). - Admin routes already pass
adminAuthMiddlewareand the global rate limiter:apps/hocuspocus.server/src/api/routers/admin.router.ts:31. Peers:/email/digest-grouping(:39) and/push/subscriptions(:52). - Public docs:
apps/hocuspocus.server/API.md:35,:110-112,:1151-1153,:1278;docs/api/README.md:103; OpenAPIsrc/modules/openapi/domain/paths/email.ts:100-111,paths/health.ts:91anddocument.ts:26.
Fix plan
- Add two admin routes in
admin.router.ts:GET /api/admin/email/gatewayreturnsemailGateway.getHealth().GET /api/admin/push/gatewayreturnspushGateway.getHealth().- Put the two handlers in
apps/hocuspocus.server/src/api/controllers/admin-stats.controller.ts, besidegetEmailStatsAdmin.admin.controller.tsalready re-exports that file. - Answer
200with the report, or500with{ error }like their peers. The push handler answers200even when push is not configured, because the dashboard reads the flags from the body.
- Delete the public routes:
emailRouter.get('/health', …)inemail.ts, andhealth.get('/push', …)withcheckPushHealth. - Switch the dashboard
checkEmailGatewayHealthandcheckPushGatewayHealthtofetchApi('/api/admin/email/gateway')andfetchApi('/api/admin/push/gateway').fetchApiadds the admin's token. Pass{ signal: AbortSignal.timeout(5000) }to keep today's timeout.fetchApithrows on a non-OK answer, so the existingcatchreturnsstatus: 'down'. Remove the!response.okbranch.- Keep their return shapes, so
notifications.tsxdoes not change.
- Docs: move both entries from the email and health OpenAPI paths to
paths/admin.ts, and updateAPI.md,docs/api/README.mdand the push sentence inopenapi/domain/document.ts:26. Add a line toapps/hocuspocus.server/CHANGELOG.md.
Layer: admin-only data goes through the admin router (apps/hocuspocus.server/CLAUDE.md §Admin API And Dashboard). Do not use requireServiceRole: the browser cannot hold the service-role key, and the admin container no longer has it (#403). Do not register adminAuthMiddleware on a /health/* path; that group skips the rate limiter, so each bad token there would reach Supabase Auth unthrottled.
Ship the server and dashboard changes in one release. Between the two container updates, the Notifications page shows the gateways as down. That gap is harmless and ends when both are live.
Out of scope
GET /api/email/status. It returns only{ operational, timestamp }. Keep it public.- The
/healthdependency checks. See Stop REST /health from leaking error text or taking every /api route down #405.
Acceptance criteria
-
GET /api/email/healthandGET /health/pushreturn404. -
GET /api/admin/email/gatewayandGET /api/admin/push/gatewayreturn401with no token and403for a non-admin. - The admin dashboard Notifications page still shows the provider, pending jobs and gateway status for both channels.
- The OpenAPI document lists the two admin routes and not the two removed ones.
Verify
curl -i http://localhost:4000/api/email/healthandcurl -i http://localhost:4000/health/push→404.curl -i http://localhost:4000/api/admin/email/gateway→401. With an admin access token inAuthorization: Bearer …→200.cd apps/hocuspocus.server && bun test src/modules/openapi/__tests__/unit/specCoverage.test.ts.- Open the admin dashboard Notifications page on the local stack and check both gateway cards.
Related
- Stop REST /health from leaking error text or taking every /api route down #405 (REST
/healthleaks error text) - Keep server secrets out of the webapp and admin-dashboard containers #403 (service-role key removed from the admin container, fixed)
Generated by Claude Code
Activity
[-][Security] GET /api/email/health is public and shows mail volume[/-][+]Move the email and push gateway reports behind admin auth[/+]fix(api): put push gateway status behind admin and confirm unsubscribe
docs(hocuspocus): sync server docs with the 9 October fixes
docs(repo): record the 9 Oct security and email batch in the changelo…
HMarzban commentedon Oct 9, 2026
Fixed and live.
659029c36bbdaae66c(production run 37976785981, green).GET /health/pushanswers 404. Admins read the report onGET /api/admin/push/gateway. The publicGET /api/email/healthreturns{status, queue_connected}only.