@@ -138,6 +153,69 @@ Do H1–H7, commit after each. Target ≥70% coverage on services.
---
## Agent A2: Progressive Login Lockout
**Зависимости:** Agent A (redis, rateLimit), Agent C (auth routes). Уже есть Auth и rateLimit.
**Ветка:**`feat/progressive-login-lockout`
**Контекст:** Сейчас `POST /auth/login` использует фиксированный лимит через `@fastify/rate-limit` (N попыток на 15 мин). По [security.md](samreshu_docs/principles/security.md) нужен **прогрессивный lockout** — считаются только **неудачные** попытки входа, с нарастающим временем блокировки:
| Неудачных попыток | Блокировка |
|-------------------|------------|
| 5 за 15 мин | 15 минут |
| 10 за 1 час | 1 час |
| 20 за 24 часа | 24 часа |
Счётчики в Redis по ключу `lockout:<ip>`. При успешном логине — сброс счётчиков.
**Задача A2-1:**
1.**Создать `src/utils/loginLockout.ts`** — утилита для работы с Redis:
-`checkBlocked(redis, ip: string)` → `{ blocked: boolean, retryAfter?: number }` — проверяет `lockout:blocked:<ip>`; если ключ есть и TTL > 0, возвращает `{ blocked: true, retryAfter }`.
-`recordFailedAttempt(redis, ip: string)` — INCR по ключам `lockout:15m:<ip>`, `lockout:1h:<ip>`, `lockout:24h:<ip>`с TTL 15m/1h/24h; при достижении порогов (5/10/20) устанавливает `lockout:blocked:<ip>`с соответствующим TTL.
-`clearOnSuccess(redis, ip: string)` — DEL всех ключей `lockout:*:<ip>` и `lockout:blocked:<ip>`.
2.**Обновить `src/plugins/rateLimit.ts`** — убрать `login` из `rateLimitOptions` (больше не используется для login). Остальные endpoints без изменений.
3.**Обновить `src/routes/auth.ts`** — для `POST /login`:
-В handler: обернуть `authService.login(...)` в try/catch; при успехе — `clearOnSuccess(app.redis, ip)`; при throw (например `unauthorized`) — `recordFailedAttempt(app.redis, ip)`, затем rethrow.
4.**Опционально** — вынести пороги (5, 10, 20) и окна в `env.ts` или оставить константами в `loginLockout.ts` (документация security.md указывает фиксированные значения).
**Коммит:**`feat: replace fixed login rate limit with progressive lockout`
**Итого:** 1 коммит. После — PR в `dev`.
---
## Agent A2-2: Fix Login Lockout Tests (после A2)
**Зависимости:** Agent A2 выполнен.
**Ветка:**`fix/login-lockout-test-mock`
**Проблема:** Auth routes используют `app.redis` для `checkBlocked`, `recordFailedAttempt`, `clearOnSuccess`. В`buildAuthTestApp` Redis не подключён — тесты `POST /auth/login` падают с 500.
**Задача A2-2:**
1.**Добавить mock Redis** в `tests/helpers/build-test-app.ts`:
- Создать объект, реализующий минимальный интерфейс ioredis для loginLockout: `ttl`, `setex`, `del`, `eval`.
-`ttl(key)` → -2 (ключ не существует) или -1/положительное значение для тестов.
-`setex`, `del` → no-op или простая in-memory имитация.
-`eval(script, keysCount, ...keys, ...args)` → вернуть `[0, 0, 0]` (счётчики не достигли порога).
-`app.decorate('redis', mockRedis)` перед регистрацией auth routes.
2.**Обновить `rateLimitOptions`** в buildAuthTestApp — убрать `login` (его больше нет в типе из rateLimit.ts).
**Коммит:**`test: add mock Redis for auth integration tests with login lockout`
**Итого:** 1 коммит. Проверить: `npm run test -- tests/integration/auth.routes.test.ts` проходит.
---
## Agent B: Data Model & Drizzle Schema
**Зависимости:** Agent A (нужен database plugin). Может стартовать после A4.
@@ -31,8 +51,8 @@ export async function buildAuthTestApp(mockDb?: MockDb): Promise<FastifyInstance
});
app.decorate('db',db);
app.decorate('redis',mockRedis);
app.decorate('rateLimitOptions',{
login:{max: 100,timeWindow:'1 minute'},
register:{max: 100,timeWindow:'1 hour'},
forgotPassword:{max: 100,timeWindow:'1 hour'},
verifyEmail:{max: 100,timeWindow:'15 minutes'},
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.