feat(redis): кэш, rate limit, баны IP и pub/sub через Redis
Добавлен сервис redis:7-alpine (AOF, requirepass, maxmemory + allkeys-lru, healthcheck, том redis-data, порт только на 127.0.0.1) и абстракция redis.js по образцу storage.js. Переведено на Redis: - кэш ответов API и настроек (было Map в памяти), инвалидация по префиксу через SCAN + DEL; - rate limit для api/entry/file — общие счётчики вместо MemoryStore; - баны IP и счётчики неудачных входа — с TTL, вместо опроса БД каждую минуту; - кэш сессий (30 с) с invalidateSessions() на каждой мутации users/sessions/ user_branches, иначе деактивированный пользователь сохранил бы доступ; - pub/sub для SSE-событий и мгновенного пробуждения фоновых воркеров вместо ожидания цикла опроса БД. Отказоустойчивость: при недоступном Redis все операции уходят в in-memory backend с той же семантикой, приложение стартует и работает без Redis и возвращается в Redis автоматически. Первое подключение ограничено по времени (REDIS_CONNECT_TIMEOUT_MS, 5 с) — node-redis не отклоняет connect() при недоступном сервере, а повторяет попытки бесконечно. Добавлены тесты: redis.selftest.js (в т.ч. поведение при недоступном сервере) и api.smoketest.js (сквозная проверка API, включая инвалидацию кэша и мгновенную смерть сессии после logout).
This commit is contained in:
@@ -8,8 +8,8 @@ This document defines how AI agents should work with the WhatIDo codebase. Follo
|
||||
|
||||
**WhatIDo** — Accounting system for an educational center: attendance journal, student project works, group gallery, detached files, and public showcase pages (share links).
|
||||
|
||||
- **Stack**: Node.js 20 + Express, PostgreSQL 16, Docker Compose, S3-совместимое хранилище файлов, Tailscale (Serve/Funnel)
|
||||
- **Architecture**: Single Express server (`server.js`) + storage abstraction (`storage.js`) + static frontend in `public/`
|
||||
- **Stack**: Node.js 20 + Express, PostgreSQL 16, Redis 7, Docker Compose, S3-совместимое хранилище файлов, Tailscale (Serve/Funnel)
|
||||
- **Architecture**: Single Express server (`server.js`) + storage abstraction (`storage.js`) + cache/pub-sub abstraction (`redis.js`) + static frontend in `public/`
|
||||
- **Deployment**: Docker Compose (app + db + s3 + tailscale), bind-mounted uploads, named volumes for Postgres and S3 data
|
||||
- **Auth**: Admin-only via `X-Admin-Token` header (value = `ADMIN_PASSWORD` env var). No user sessions.
|
||||
|
||||
@@ -47,9 +47,24 @@ This document defines how AI agents should work with the WhatIDo codebase. Follo
|
||||
- **Cache**: `.thumbs` (WebP miniatures) and `.cache` (originals localized for sharp/zip) live inside `uploads/` and are pruned hourly (`STORAGE_CACHE_MAX_AGE_HOURS`)
|
||||
- **Never publish the S3 API port**: only `127.0.0.1` on the host, file access stays behind app auth/rate limits
|
||||
|
||||
### 3b. Redis (`redis.js`)
|
||||
- **Единственная точка доступа**: `createRedis({ url, prefix })` — все операции кэша/счётчиков/pub-sub идут через неё
|
||||
- **API**: `get`, `set`, `del`, `dropPrefix`, `dropMatch`, `clear`, `wrap`, `incr`, `publish`, `on`, `rateLimitStore`, `info`, `connect`, `close`
|
||||
- **Graceful fallback — обязательное требование**: при недоступном Redis все операции уходят в in-memory backend с той же семантикой. Приложение обязано стартовать и работать без Redis
|
||||
- **Первое подключение ограничено по времени** (`REDIS_CONNECT_TIMEOUT_MS`, 5 с): node-redis не отклоняет `connect()` при недоступном сервере, а повторяет попытки бесконечно — без таймаута старт приложения зависнет навсегда
|
||||
- **Переподключение**: node-redis переподключается сам; по событию `ready` подписки и subscriber-клиент восстанавливаются (`ensureSubscriber`). Не пересоздавать subscriber через `destroy()` — это гонка с внутренним teardown node-redis
|
||||
- **Ключи**: `get`/`set` сами добавляют namespace (`REDIS_PREFIX`, по умолчанию `whatido`), `dropPrefix`/`dropMatch` тоже. В `rateLimitStore` префикс добавляется один раз в `base` — не применяйте `fullKey` повторно
|
||||
- **`scanDelete`**: курсор `SCAN` в node-redis v5 обязан быть строкой, числовой `0` вызовет `TypeError`. Возвращаемое значение курсора — тоже строка, сравнивайте с `'0'`
|
||||
- **`resetTime` в `rateLimitStore.increment` обязан быть `Date`** — express-rate-limit v8 вызывает `resetTime.getTime()`
|
||||
- **Пабликация всегда отдаёт подписчикам строку** (JSON), независимо от бэкенда — иначе fallback и Redis расходятся по формату
|
||||
- **Инвалидация — по префиксу** (`SCAN` + `DEL`), точечного удаления по ключу избегайте
|
||||
- **Ключевые пространства**: `setting:`, `groups:`, `students:`, `entries:`, `stats:`, `dashboard:`, `share:payload:`, `public-settings`, `system-info`, `session:`, `ban:`, `fail:`, `rl:`
|
||||
- **Сессии**: `loadUserByToken` кэширует пользователя на 30 с. Любая мутация `users` / `sessions` / `user_branches` обязана вызывать `invalidateSessions()` или удалять `session:<token>`, иначе деактивированный пользователь сохранит доступ
|
||||
- **Секреты**: пароль только в `REDIS_URL` / `REDIS_PASSWORD`, порт 6379 публикуется лишь на `127.0.0.1`
|
||||
|
||||
### 4. API Patterns
|
||||
- **Admin routes**: `requireAdmin` middleware (checks `X-Admin-Token`)
|
||||
- **Public routes**: `apiLimiter` (300/15min), `entryLimiter` (10/15min), `fileLimiter` (300/15min)
|
||||
- **Public routes**: `apiLimiter` (300/15min), `entryLimiter` (10/15min), `fileLimiter` (300/15min) — все на `cache.rateLimitStore(...)`, не на `MemoryStore`
|
||||
- **Responses**: JSON, `{ error: 'message' }` on failure, data directly on success
|
||||
- **Pagination**: `limit` / `offset` query params, return `{ items, total }` or `{ entries, total }`
|
||||
- **Filters**: `group_id`, `date_from`, `date_to`, `student_name`, `search`, `deleted`
|
||||
@@ -62,11 +77,13 @@ This document defines how AI agents should work with the WhatIDo codebase. Follo
|
||||
|
||||
### 6. Docker / Compose
|
||||
- **Dockerfile**: Node 20 Alpine, installs deps, generates self-signed TLS cert
|
||||
- **docker-compose.yml**: 3 services (db, app, tailscale)
|
||||
- **docker-compose.yml**: сервисы `db`, `app`, `redis`, `s3` (+ опционально `tailscale`, `cloudflared`, `text-corrector`, `photo-ai`)
|
||||
- `db`: postgres:16-alpine, healthcheck, init.sql mounted
|
||||
- `redis`: redis:7-alpine, `--requirepass`, AOF, `maxmemory` + `allkeys-lru`, healthcheck, том `redis-data`, порт только на `127.0.0.1`
|
||||
- `app`: builds from Dockerfile, exposes 3003/3443, mounts uploads
|
||||
- `tailscale`: host network, NET_ADMIN, runs `start-tailscale.sh` (funnel to 127.0.0.1:3443)
|
||||
- **Env vars** (required): `ADMIN_PASSWORD`, `DB_PASSWORD`
|
||||
- **Env vars** (required): `ADMIN_PASSWORD`, `DB_PASSWORD`, `REDIS_PASSWORD`
|
||||
- **Env vars** (optional): `REDIS_PREFIX` (default `whatido`), `REDIS_MAXMEMORY` (default `256mb`), `REDIS_CONNECT_TIMEOUT_MS` (default `5000`)
|
||||
- **Port 443 on host** must be free (tailscale listens directly)
|
||||
|
||||
### 7. Tailscale Publication
|
||||
@@ -144,8 +161,18 @@ curl -H "X-Admin-Token: $ADMIN_PASSWORD" http://localhost:3003/api/groups
|
||||
docker compose up -d s3
|
||||
docker compose exec -T app node scripts/migrate-to-s3.js --dry-run
|
||||
docker compose exec -T app node scripts/migrate-to-s3.js --verify-only
|
||||
|
||||
# Redis checks
|
||||
node redis.selftest.js # unit + degradation, needs redis on 127.0.0.1:6379
|
||||
node api.smoketest.js # e2e, needs running stack
|
||||
docker compose exec redis redis-cli -a "$REDIS_PASSWORD" --no-auth-warning INFO
|
||||
docker compose stop redis && node api.smoketest.js # app must keep working in-memory
|
||||
docker compose start redis # app reconnects on its own
|
||||
```
|
||||
|
||||
Verify Redis state through `GET /api/system-info` → `cache` (`driver`, `ready`, `hits`, `misses`,
|
||||
`fallbackOps`, `used_memory_human`, `keys`).
|
||||
|
||||
---
|
||||
|
||||
## Security Checklist (before any change)
|
||||
@@ -166,6 +193,9 @@ docker compose exec -T app node scripts/migrate-to-s3.js --verify-only
|
||||
|------|---------|
|
||||
| `server.js` | Entire backend (Express, routes, DB, uploads, backup) |
|
||||
| `storage.js` | Storage abstraction: `local` and `s3` drivers, key normalization, cache/thumb helpers |
|
||||
| `redis.js` | Redis abstraction: cache, counters, rate-limit store, pub/sub, in-memory fallback |
|
||||
| `redis.selftest.js` | Self-tests for `redis.js`, including behaviour with Redis unavailable |
|
||||
| `api.smoketest.js` | End-to-end API smoke test against a running stack |
|
||||
| `worker.js` | Background AI auto-check worker for entry messages + photo enhance worker |
|
||||
| `db/init.sql` | Initial schema (runs on fresh DB) |
|
||||
| `db/migration.sql` | Idempotent migrations for existing DBs |
|
||||
@@ -191,6 +221,11 @@ docker compose exec -T app node scripts/migrate-to-s3.js --verify-only
|
||||
- ❌ Touch `uploads/` with `fs.*` in request/worker code — use `storage.*` (files may live only in S3)
|
||||
- ❌ Run `migrate-to-s3.js --delete-local` before verification and cutover
|
||||
- ❌ Expose the S3 API port publicly (only `127.0.0.1` in compose)
|
||||
- ❌ Expose the Redis port publicly (only `127.0.0.1` in compose)
|
||||
- ❌ Call `fs.*`/`pg` directly for cache, counters or pub/sub — use `redis.js`
|
||||
- ❌ Make Redis a hard dependency: any new Redis-backed path must keep the in-memory fallback
|
||||
- ❌ `await client.connect()` without a timeout — it never rejects while Redis is unreachable
|
||||
- ❌ Cache authorization-relevant data without an invalidation path on the mutation
|
||||
- ❌ Commit `.env`, `certs/`, `uploads/`, `backups/`, `node_modules/`
|
||||
- ❌ Expose DB port (5432) outside docker network
|
||||
- ❌ Use `eval`, `Function` constructor, or dynamic code execution
|
||||
@@ -213,6 +248,10 @@ docker compose logs -f app
|
||||
# DB shell
|
||||
docker compose exec db psql -U app -d whereldo
|
||||
|
||||
# Redis status and cache keys
|
||||
docker compose exec redis redis-cli -a "$REDIS_PASSWORD" --no-auth-warning DBSIZE
|
||||
docker compose exec redis redis-cli -a "$REDIS_PASSWORD" --no-auth-warning KEYS 'whatido:*'
|
||||
|
||||
# S3 storage status and migration verification
|
||||
docker compose up -d s3
|
||||
docker compose exec -T app node scripts/migrate-to-s3.js --verify-only
|
||||
|
||||
Reference in New Issue
Block a user