Compare commits
3 Commits
feat/progr
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a4b179d89 | |||
|
|
99a2686532 | ||
|
|
77e4735cdc |
@@ -69,6 +69,21 @@ Implement Agent H tasks from AGENT_TASKS.md. Work in branch feat/testing.
|
|||||||
Do H1–H7, commit after each. Target ≥70% coverage on services.
|
Do H1–H7, commit after each. Target ≥70% coverage on services.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Agent A2 (Progressive Login Lockout):**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Implement Agent A2 task from AGENT_TASKS.md. Work in branch feat/progressive-login-lockout.
|
||||||
|
Branch from dev. Do task A2-1. Commit with the message from the table.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Agent A2-2 (Fix Login Lockout Tests):**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Implement Agent A2-2 task from AGENT_TASKS.md. Work in branch fix/login-lockout-test-mock.
|
||||||
|
Branch from dev. Do task A2-2. Commit with the message from the table.
|
||||||
|
Ensure tests/integration/auth.routes.test.ts passes.
|
||||||
|
```
|
||||||
|
|
||||||
## Текущее состояние репозитория
|
## Текущее состояние репозитория
|
||||||
|
|
||||||
Часть работы уже выполнена одним агентом:
|
Часть работы уже выполнена одним агентом:
|
||||||
@@ -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`:
|
||||||
|
- Убрать `config: { rateLimit: rateLimitOptions.login }`.
|
||||||
|
- Добавить `preValidation`: вызвать `checkBlocked(app.redis, req.ip)`; если `blocked` — `reply.status(429).send({ error: { code: 'RATE_LIMIT_EXCEEDED', message: '...', retryAfter } })`.
|
||||||
|
- В 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 B: Data Model & Drizzle Schema
|
||||||
|
|
||||||
**Зависимости:** Agent A (нужен database plugin). Может стартовать после A4.
|
**Зависимости:** Agent A (нужен database plugin). Может стартовать после A4.
|
||||||
|
|||||||
@@ -4,9 +4,29 @@ import { authRoutes } from '../../src/routes/auth.js';
|
|||||||
import type { MockDb } from '../test-utils.js';
|
import type { MockDb } from '../test-utils.js';
|
||||||
import { createMockDb } from '../test-utils.js';
|
import { createMockDb } from '../test-utils.js';
|
||||||
|
|
||||||
|
/** Mock Redis for login lockout in auth tests. Implements ttl, setex, del, eval. */
|
||||||
|
const mockRedis = {
|
||||||
|
async ttl(_key: string): Promise<number> {
|
||||||
|
return -2; // key does not exist -> not blocked
|
||||||
|
},
|
||||||
|
async setex(_key: string, _ttl: number, _value: string): Promise<'OK'> {
|
||||||
|
return 'OK';
|
||||||
|
},
|
||||||
|
async del(..._keys: string[]): Promise<number> {
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
async eval(
|
||||||
|
_script: string,
|
||||||
|
_keysCount: number,
|
||||||
|
..._keysAndArgs: string[]
|
||||||
|
): Promise<number[]> {
|
||||||
|
return [0, 0, 0]; // counters below threshold
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a minimal Fastify app for auth route integration tests.
|
* Build a minimal Fastify app for auth route integration tests.
|
||||||
* Uses mock db and rate limit options (no actual rate limiting).
|
* Uses mock db, mock Redis for login lockout, and rate limit options (no actual rate limiting).
|
||||||
*/
|
*/
|
||||||
export async function buildAuthTestApp(mockDb?: MockDb): Promise<FastifyInstance> {
|
export async function buildAuthTestApp(mockDb?: MockDb): Promise<FastifyInstance> {
|
||||||
const db = mockDb ?? createMockDb();
|
const db = mockDb ?? createMockDb();
|
||||||
@@ -31,8 +51,8 @@ export async function buildAuthTestApp(mockDb?: MockDb): Promise<FastifyInstance
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.decorate('db', db);
|
app.decorate('db', db);
|
||||||
|
app.decorate('redis', mockRedis);
|
||||||
app.decorate('rateLimitOptions', {
|
app.decorate('rateLimitOptions', {
|
||||||
login: { max: 100, timeWindow: '1 minute' },
|
|
||||||
register: { max: 100, timeWindow: '1 hour' },
|
register: { max: 100, timeWindow: '1 hour' },
|
||||||
forgotPassword: { max: 100, timeWindow: '1 hour' },
|
forgotPassword: { max: 100, timeWindow: '1 hour' },
|
||||||
verifyEmail: { max: 100, timeWindow: '15 minutes' },
|
verifyEmail: { max: 100, timeWindow: '15 minutes' },
|
||||||
|
|||||||
Reference in New Issue
Block a user