IP blocking and Security
Catch-all for invalid routes, CQRS-ready repositories (Postgres active), and an admin API under /api/v1/security.
Overview
The module under src/common/security tracks access to unknown API routes and can block offending IPs. Persistence is CQRS-ready: Postgres is the active store via BlockedIpPostgresRepository; a Mongo repository is registered but unused (same pattern as users). Admins manage the list via REST endpoints that require JWT and the super role.
Key ideas
- • CatchAllController registers invalid-route attempts (production/staging only)
- • BlockedIpPostgresRepository is the active store; Mongo repo is CQRS-ready / unused
- • After MAX_ATTEMPTS, the IP is blocked (403 on further hits)
- • Admin API: list, unblock, reset, remove — JWT + Role.SUPER
Structure
src/common/security/ ├── security.module.ts ├── security.controller.ts ├── security.service.ts ├── catchall.controller.ts ├── repositories/ │ ├── postgres.repository.ts │ └── mongo.repository.ts ├── entities/blocked-ip.entity.ts ├── schemas/blocked-ip.schema.ts └── interfaces/blocked-ip.interface.ts
Catch-all (invalid routes)
CatchAllController is registered only when NODE_ENV is production or staging. It handles unmatched routes with @All('*') and is @Public().
Flow
- • Unknown API path hits CatchAllController
- • registerInvalidRouteAttempt(ip, path, userAgent)
- • If attempts ≥ MAX_ATTEMPTS → blocked = true → 403
- • Otherwise → 404 JSON { message: "Route not found" }
Does not treat as suspicious
- • Wiki HTML 404s (browser Accept: text/html)
- • GraphQL path (GRAPHQL_PATH or /graphql) — Fastify Apollo route
- • /favicon.ico (and other favicon assets), /robots.txt and /health
SecurityService
Domain rules live in SecurityService; persistence goes through BlockedIpPostgresRepository. Mongo repository exists for future CQRS but is not called.
Constants
- • MAX_ATTEMPTS = 1 (block after this many invalid-route hits)
- • MAX_PATHS_STORED = 20 (last paths kept on the record)
Main methods
- • registerInvalidRouteAttempt — upsert IP, bump attempts, maybe block
- • getBlockedIps / getSuspiciousIps — admin lists
- • unblockIp — clear block and attempts
- • resetAttempts — clear attempts/block/paths
- • removeIp — delete via Postgres repository
Admin API
All routes under /api/v1/security require Authorization: Bearer and Role.SUPER.
Endpoints
- • GET /api/v1/security/blocked-ips — list blocked IPs
- • GET /api/v1/security/suspicious-ips — list IPs with attempts, not blocked
- • POST /api/v1/security/unblock/:ip — unblock (200 / 404)
- • POST /api/v1/security/reset/:ip — reset attempts (200 / 404)
- • DELETE /api/v1/security/:ip — remove record (200 / 404)
GET /api/v1/security/blocked-ips Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
[
{
"id": "uuid-...",
"ip": "203.0.113.10",
"attempts": 1,
"blocked": true,
"paths": ["/api/v1/unknown"],
"blockedAt": "2026-07-23T12:00:00.000Z"
}
]
POST /api/v1/security/unblock/203.0.113.10 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
{
"message": "IP 203.0.113.10 unblocked successfully",
"data": { "id": "uuid-...", "ip": "203.0.113.10", "blocked": false, "attempts": 0 }
}
Docker / production note
With NODE_ENV=production (as in .env.docker), the catch-all is active. Hitting unknown API paths can block your IP after MAX_ATTEMPTS. Use the admin API (super role) to unblock, or clear the blocked_ips table.