# IP Whitelist App — AI Agent Guide > For AI coding agents (Copilot, etc.) to understand and continue development. > Last updated: 2026-06-03 | Version: 0.5.13 ## 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 on VM (`5.172.178.213`), HTTP only via `italo.kube5s.ru` ## 2. Architecture ``` Browser → nginx:80 → 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-token` with `clientId` field → mock JWT → session - Mock users defined in `src/config.js`: - `admin` (WZ01112) — admin@nubes.ru - `alfa` (WZ88888) — alfa@example.com - `multi` (WZ88888, WZ77777) — multi@example.com - `beta` (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.user` for all downstream handlers ### API bearerMiddleware (src/auth.js) - Verifies Bearer JWT signature (RS256 via JWKS or mock keypair) - Sets `req.user` via `userFromPayload()` ## 5. Multi-Company Users can belong to multiple companies via comma-separated `clientId`: - `ClientID: "WZ88888, WZ77777"` → two companies - Active company selector in UI (dropdown, `?switchTo=X` or JS navigation) - API supports `?client_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 `companies` table (unique by `client_id`) - Entries are isolated by `company_id` foreign key - Admin can view/edit any company via `?company=` - User sees only their company's entries - Multi-company user switches via `?client_id=` ## 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 ```bash # 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 (v0.5.13) ## 11. Deployment (VM) ```bash # From local workspace: rsync -az -e "ssh -i ~/.ssh/naeel_vm_id_ed25519 ..." /home/naeel/ipwhitelist-app/ naeel@5.172.178.213:~/ipwhitelist/ --exclude node_modules --exclude .git --exclude .env # On VM: cd ~/ipwhitelist && pm2 restart ipwhitelist --update-env ``` ## 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: 1. DevOps creates client in Keycloak with redirect URI `/callback` 2. Set `.env`: `KC_CLIENT_ID`, `KC_CLIENT_SECRET`, `KC_BASE_URL`, `APP_URL` 3. Set `DEV_MODE=false` 4. `pm2 restart ipwhitelist --update-env` 5. Routes in `src/routes/oidc.js` activate automatically when `KC_CLIENT_ID` is set See `docs/deploy-keycloak.md` for details. ## 14. Version History | Version | Date | Key Changes | |---------|------|-------------| | 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 |