Security

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

Treesrc/common/security
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)
HTTP RequestGET /api/v1/security/blocked-ips
GET /api/v1/security/blocked-ips
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
HTTP Response200 OK
[
  {
    "id": "uuid-...",
    "ip": "203.0.113.10",
    "attempts": 1,
    "blocked": true,
    "paths": ["/api/v1/unknown"],
    "blockedAt": "2026-07-23T12:00:00.000Z"
  }
]
HTTP RequestPOST /api/v1/security/unblock/:ip
POST /api/v1/security/unblock/203.0.113.10
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
HTTP Response200 OK
{
  "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.