После предыдущего фикса 5/мин per-username — per-IP 30/мин всё равно
ломал stage e2e (multi-tenant специ делают 4 signup+token подряд →
накапливается за минуту). Поднял до 60/мин token, 600/час; per-username
5/мин остаётся как анти-bruteforce.
Также: playwright.config.ts добавлен locale: 'ru-RU' — без этого
Chromium шлёт en-US, i18next отдаёт английский sidebar, а тесты ищут
русские лейблы (2.2 'Главная', 6.1 'Поставщик/Склад/Дата').
Verify-spec'и V-14 (POS Sync) и V-15 (Stock race) — починены payload'ы
под актуальную схему API (/api/catalog/stores не /api/inventory/stores,
quantity не qty, unitCost не costPrice, polnyy retail-sale body с
retailPointId/currencyId/payment/isReturn). Проверено:
- V-14: 1-й POS-батч 200 (accepted=1), 2-й replayedFromCache=true с тем
же serverSaleId; detail GET показывает notes=pos:<csid-N> ✓
- V-15: 5 параллельных Post на остаток=3 → ровно 3 успешных (204), 2
конфликта (409 'Недостаточно остатка'). Stock=0 после dust settles. ✓
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Регрессия после ba54155: rate-limit 5/мин per-IP сваливал stage e2e
(75 тестов с одного IP, каждый по /connect/token при apiSignup → 22+
из них падали на 429 после 5-й попытки). Per-IP лимит был
неправильной осью защиты.
Новая стратегия в AuthRateLimiterExtensions:
- Per-username (только /connect/token): 5/мин, 20/час. Защищает от
перебора пароля к конкретному account независимо от IP атакующего.
Username вытаскивается form-body peek-middleware'ом перед UseRateLimiter
(EnableBuffering + ручной парс x-www-form-urlencoded, тело ≤4KB).
- Per-IP (token+signup): 30/мин, 200/час. Защищает от спам-регистрации
и от 1-IP-перебирает-тысячи-аккаунтов сценария.
- Back-compat: legacy RateLimiting:PerMinute/PerHour мапятся в IP-лимит.
Проверено через https://test.admin.food-market.kz:
- 6 неверных попыток на ОДНУ учётку → 6-я → 429 ✓
- 8 неверных попыток на РАЗНЫЕ учётки с того же IP → все 400 (IP-лимит 30/мин не достигнут) ✓
Также добавлены verify-spec'и stage-ui-verify-pos-sync (п.14) и
stage-ui-verify-stock-race (п.15).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- roles.steps.ts step08: было «задокументированный gap», стало реальная
проверка — кастомная роль без ProductsEdit → 403 на PUT товара, GET → 200.
Сценарий roles зелёный 8/8.
- RateLimiting:* конфиг (Enabled/PerMinute/PerHour): тесты с общим loopback-IP
поднимают/выключают лимит, чтобы повторные логины не упирались в 429.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sliding window на IP: 5/мин + 20/час (оба окна chained, оба должны
пропустить). Отдельные бакеты на эндпоинт — регистрация не съедает лимит
логинов. Глобальный лимитер с no-op для не-auth путей: двойное окно
per-endpoint policy выразить не может. 429 + JSON-телом, X-Forwarded-For
учитывается за прокси. Проверено curl'ом: 6-я попытка/мин → 429.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>