11 KiB
IP Whitelist App — AI Agent Guide
For AI coding agents (Copilot, etc.) to understand and continue development. Last updated: 2026-06-04 | Version: 0.5.22
1. Quick Overview
Purpose: Self-service portal for cloud provider customers to manage trusted IPv4 whitelist entries (CIDRs) that are excluded from DDoS filtering.
Stack: Node.js + Express + EJS + PostgreSQL
Auth: Mock RS256 JWT (dev) / Keycloak OIDC (production)
Deployment: PM2, behind nginx on a VM
2. Architecture
Browser → nginx:443 → Express:3001
├── /export (public, no auth)
├── /api/v1/* (Bearer JWT, REST API)
└── /* (UI layer, SSR EJS, session-based)
Two-layer design:
- API layer (
src/api/routes/): REST endpoints, Bearer JWT auth, direct DB access - UI layer (
ui/routes/): SSR EJS pages, session-based auth, proxies to API via HTTP (ui/api-client.js)
Why two layers?
- API can be used directly by nginx/firewall for whitelist export
- UI provides human-friendly management interface
- Clear separation of concerns
Key flow:
Browser → UI route → api-client.js (HTTP) → API route → PostgreSQL
3. Project Structure
ipwhitelist-app/
├── server.js # Entry point — mounts all routes
├── package.json
├── .env # Config (not in git)
├── .env.example # Config template
├── sql/schema.sql # Database schema
│
├── src/
│ ├── auth.js # Auth: mock JWT + OIDC/Keycloak
│ ├── config.js # MOCK_USERS + helpers
│ ├── db.js # PostgreSQL pool
│ ├── queries.js # All DB queries (CRUD, export, audit)
│ ├── validators.js # CIDR validation + blocked ranges + aggregation
│ │
│ ├── api/
│ │ ├── index.js # API router mount
│ │ └── routes/
│ │ ├── entries.js # GET/POST/PATCH/DELETE /entries + /entries/export
│ │ └── admin.js # GET /companies, PATCH /limit, GET /audit
│ │
│ ├── middleware/
│ │ ├── csrf.js # CSRF protection (NOT ACTIVE — tokens are empty)
│ │ ├── rateLimit.js # Rate limiters (auth, mutation, export)
│ │ └── session.js # express-session with pg store
│ │
│ └── routes/
│ ├── oidc.js # OIDC/Keycloak routes (inactive in mock mode)
│ └── (old monolithic routes deleted in v0.5.11)
│
├── ui/
│ ├── index.js # UI middleware: requireToken + resolveUser + mount routers
│ ├── api-client.js # HTTP client to /api/v1/* (from UI layer)
│ └── routes/
│ ├── auth.js # GET/POST /login, POST /login-token, GET /logout
│ ├── entries.js # GET /, POST /add, POST /edit/:id, POST /delete/:id
│ ├── admin.js # GET /admin, /audit (admin-only)
│ └── export.js # GET /export (proxies to API)
│
├── views/
│ ├── index.ejs # Main page: entries table + add/edit/delete forms
│ ├── ui-login.ejs # Login page (mock + manual clientId entry)
│ ├── admin.ejs # Admin: companies list + limits
│ ├── audit.ejs # Audit log
│ └── error.ejs # 404/500 error page
│
├── tests/
│ ├── api.js # 121 API integration tests (Bearer JWT)
│ ├── integration.js # 68 UI integration tests (session, EJS)
│ ├── tz-compliance.js # 47 ТЗ compliance tests
│ ├── tz-full-compliance.js # 111 comprehensive ТЗ tests (20 companies × 100 CIDRs)
│ ├── run-tests.js # 68 unit/smoke tests (no DB)
│ └── stress.js # 191 concurrency/stress tests
│
└── docs/
├── ТЗ.md # Tech spec (Russian)
├── ТЗ-плюс.md # Client clarifications
├── ТЗ-реализация.md # Implementation notes
├── keycloak-auth-reference.md # Keycloak/OIDC reference
├── devops-deploy.md # DevOps deployment guide
└── deploy-keycloak.md # Keycloak setup guide
4. Auth System
Mock Mode (default, DEV_MODE=true)
- Local RSA keypair generated at startup
POST /login-tokenwithclientIdfield → mock JWT → session- Mock users defined in
src/config.js:admin(WZ01112) — admin@nubes.rualfa(WZ88888) — alfa@example.commulti(WZ88888, WZ77777) — multi@example.combeta(WZ77777) — beta@example.com
OIDC Mode (production, when KC_CLIENT_ID set)
- Authorization Code flow via Keycloak
- Routes in
src/routes/oidc.js(inactive in mock mode) - JWKS certificate verification
UI resolveUser (ui/index.js)
- Decodes JWT from session (no signature check — API validates on each call)
- Extracts
ClientID,allClientIds,activeClientId,companyId,companyName,email - Sets
req.userfor all downstream handlers
API bearerMiddleware (src/auth.js)
- Verifies Bearer JWT signature (RS256 via JWKS or mock keypair)
- Sets
req.userviauserFromPayload()
5. Multi-Company
Users can belong to multiple companies via IAM API profiles:
ClientID: "WZ88888, WZ77777"→ two companies- Active company selector in UI (dropdown,
?switchTo=Xor JS navigation) - API supports
?client_id=<id>parameter to specify active company - Default: first company in list (
allClientIds[0])
IMPORTANT: req.user.activeClientId must be synced BOTH in session.user.activeClientId AND req.user.activeClientId — see ui/routes/entries.js switch handler.
6. Company Isolation
- Each company is a row in
companiestable (unique byclient_id) - Entries are isolated by
company_idforeign key - Admin can view/edit any company via
?company=<db_id> - User sees only their company's entries
- Multi-company user switches via
?client_id=<clientId>
7. Database
Three tables (see sql/schema.sql):
companies— (id, client_id, name, custom_limit, created_at, updated_at)whitelist_entries— (id, company_id FK, value_cidr, comment, created_by, created_at, updated_at, deleted_at, deleted_by)audit_log— (id, user_email, company_id, action, old_value, new_value, entry_id, created_at)
Default limit: 15 entries per company (configurable via DEFAULT_LIMIT env).
8. Key Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /export |
None (public) | Aggregated CIDR list, text/plain |
| GET | /api/v1/entries |
Bearer JWT | List entries for company |
| POST | /api/v1/entries |
Bearer JWT | Create entry |
| PATCH | /api/v1/entries/:id |
Bearer JWT | Update entry |
| DELETE | /api/v1/entries/:id |
Bearer JWT | Soft-delete entry |
| GET | /api/v1/entries/export |
Bearer JWT | Aggregated CIDR export |
| GET | /api/v1/companies |
Bearer JWT (admin) | List companies |
| PATCH | /api/v1/companies/:id/limit |
Bearer JWT (admin) | Set company limit |
| GET | /api/v1/audit |
Bearer JWT (admin) | Audit log |
| GET | /login |
None | Login page (UI) |
| POST | /login |
None | Mock login (form) |
| POST | /login-token |
None | Manual clientId login |
| GET | /logout |
Session | Logout |
Export query params:
?view=1— inline (browser display)?filename=X— custom filename (default: white-list.txt)
9. CIDR Validation Rules
- Only IPv4, no IPv6
- Mask must be /22 to /32 (inclusive)
- Single IP without mask = /32
- 14 blocked ranges (RFC1918, CGNAT, loopback, etc.)
- No duplicates within a company
- No overlaps within a company
- Normalization: host bits zeroed (e.g., 9.9.9.5/24 → 9.9.9.0/24)
- Comment max 255 chars, optional
10. Running Tests
# Unit tests (no DB needed)
node tests/run-tests.js
# API tests (requires DB, DEV_MODE=true)
node tests/api.js
# UI integration tests
node tests/integration.js
# ТЗ compliance
node tests/tz-compliance.js
# Full ТЗ compliance (20 companies × 100 CIDRs)
node tests/tz-full-compliance.js
# Stress/concurrency tests
node tests/stress.js
All tests: 347+ PASS, 0 FAIL
11. Deployment
Deploy the app on a VM with nginx as reverse proxy. See docs/devops-deploy.md for details.
12. Common Pitfalls
🚨 Rate Limiter
authLimiter: 10 requests per 5 minutes. Hits quickly during testing. Reset after 5 min or use different IPs.
🚨 Mock Keypair Mismatch
Each initAuth() generates a NEW RSA keypair. When testing with supertest, the UI api-client makes HTTP requests to localhost:3001 (PM2 instance), which has a DIFFERENT keypair → "invalid signature". Fix: test against the running PM2 instance directly, or stop PM2 and use the supertest port.
🚨 Company Switch Bug (FIXED v0.5.12)
resolveUser runs BEFORE the route handler. When switching companies via ?switchTo=X, the session was updated but req.user.activeClientId still had the old value. Fixed by adding req.user.activeClientId = switchTo after session update.
🚨 CSRF Not Active
csrfToken is always "" in templates. CSRF middleware is not mounted. Forms work, but there's no CSRF protection. To enable: mount doubleCsrfProtection middleware and call generateCsrfToken() in GET handlers.
🚨 .env on VM gets overwritten
Rsync excludes .env now, but double-check. If .env contains DB credentials, they MUST NOT be overwritten.
🚨 Version Must Always Increment
After ANY code change: bump "version" in package.json. PM2 shows version — user expects it to match.
13. OIDC/Keycloak Transition
To enable production auth:
- DevOps creates client in Keycloak with redirect URI
/callback - Set
.env:KC_CLIENT_ID,KC_CLIENT_SECRET,KC_BASE_URL,APP_URL - Set
DEV_MODE=false pm2 restart ipwhitelist --update-env- Routes in
src/routes/oidc.jsactivate automatically whenKC_CLIENT_IDis set
See docs/deploy-keycloak.md for details.
14. Version History
| Version | Date | Key Changes |
|---|---|---|
| 0.5.14 | 2026-06-03 | Token login, JWKS in mock mode, production deploy docs |
| 0.5.13 | 2026-06-03 | OIDC routes prepared, .env.example, devops-deploy guide, repo cleanup |
| 0.5.12 | 2026-06-02 | Fixed company switch bug, aggregation, validation, TZ compliance |
| 0.5.11 | 2026-06-02 | Dead code cleanup, new integration tests, /export public |
| 0.5.10 | 2026-06-02 | UI/API split, mock users fixed, 121 API tests passing |