Самокритика v0.4 (рабочая админка с RBAC)

Дата: 2026-05-13 Кто проверял: Claude (сам автор) — поэтому проверка не объективная, но конкретная. Что проверял: https://obmenka.felix.nohumaninside.me с пятью разными ролями.

Что работает (короткое подтверждение)

  • ✅ Логин / логаут / редирект анонимов на /login
  • ✅ 5 ролей: admin / manager / accountant / cashier / viewer — каждая со своими паролями в pass
  • ✅ Access matrix: PATH admin manager accnt cashier viewer / 200 200 200 200 200 /operations 200 200 200 200 200 /reconciliation 200 200 200 403 403 /branches 200 200 200 403 403 /wallets 200 200 200 403 403 /managers 200 200 403 403 403 /handbook 200 200 200 200 200 /audit 200 403 403 403 403
  • ✅ Cashier видит только свою кассу (Москва · Арбат) в операциях
  • ✅ Cashier видит только свой раздел handbook (role=cashier), admin — все 5
  • ✅ 143 тестовые операции (10 за сегодня), 10 disputed, 11 unmatched, 3 TG-флага
  • ✅ Дашборд: оборот 11.78 млн ₽ сегодня, график за 7 дней с разбивкой по отделениям

Что плохо — критика по убыванию важности

Бизнес-логика

1. ⚠ Балансы наличных уходят в минус. Сейчас остаток = sum(cash_in) − sum(cash_out) без начального капитала. По seed-данным три из трёх физических отделений в минусе по EUR (−18k / −9.5k / −32.5k). Это не баг seed'а — это дефект модели. В реальной кассе всегда есть открытие смены (начальный остаток) и пересчёт (фактическая денежка). Без этого балансы не имеют смысла. - Чинить: добавить таблицу cash_count (snapshot пересчёта) и/или операцию shift_open. Балансы считать от последнего пересчёта плюс delta операций.

2. ⚠ Маржа считается как ровно 2% от оборота. Это плейсхолдер, не настоящая маржа. Настоящая = (курс_клиенту − рыночный_курс) × объём. У операций сейчас нет поля applied_fx_rate. - Чинить: добавить applied_fx_rate в operations, подключить источник рыночного курса (Binance API), пересчитать.

3. ⚠ Кнопки «связать вручную» и «Добавить операцию» не функциональны. В сверке и в operations они отрисованы, но не реагируют. Это вводит в заблуждение. - Чинить: на этом этапе либо disabled с tooltip «появится на E1+», либо вообще убрать. Не показывать «функционал», которого нет.

4. ⚠ Сверка показывает две колонки, но связь между парой визуально не сделана. В operations виден matched_with_id, в reconciliation — только надпись «связано с op #N», без подсветки/линии/hover. На 30 записях с двух сторон искать пару — глаза устают. - Чинить: при hover на cash подсвечивать парную crypto, или явные пары рядом.

5. ⚠ Сессии не invalidate при logout. Логаут просто удаляет cookie у клиента (это signed JWT-подобный токен, не серверная сессия). Если кто-то перехватил cookie — он валиден 12 часов даже после нажатия «Выйти». - Чинить: server-side sessions (таблица sessions + revocation) или хотя бы короткий TTL.

Безопасность

6. ⚠ Cookie без Secure флага. set-cookie: ...; HttpOnly; SameSite=lax — нет Secure. На HTTPS обязателен. - Чинить: одна строка в main.py: secure=True.

7. ⚠ Нет rate-limit на /login. 5 неправильных попыток за 1.5 сек ответили без задержки. Brute-force открыт. - Чинить: Caddy rate_limit matcher или slowapi на роуте.

8. ⚠ Нет CSRF tokens на POST формах. /login и /logout уязвимы к CSRF (правда, SameSite=lax многое закрывает, но не всё). - Чинить: добавить CSRF middleware или fastapi-csrf-protect.

9. ⚠ Cashier через дашборд узнаёт сколько флагов у менеджеров (KPI-карточка). Хотя /managers ему 403. Маленькая информационная утечка. - Чинить: скрывать KPI-карту «Флаги менеджеров» если can_see(managers) = False.

Технические

10. @app.on_event("startup") deprecated в FastAPI ≥0.100. Должен быть lifespan handler.

11. N+1 в шаблонах. o.branch.name, o.matched_with_id тянутся лениво. 200 операций → потенциально 200+ запросов. Сейчас отвечает за 50ms — терпимо, на росте — joinedload.

12. init_db() запускается при каждом старте сервиса. Это create_all(), идемпотентно, но без миграций — любое изменение схемы потребует ручного DROP. Нужен Alembic.

13. Audit log не пишется автоматически. Я наполнил его seed-данными, но реальные действия (логин, матчинг) в него не пишутся. Нет middleware.

14. Пагинации нет. Operations лимит 200 — дальше обрезается. На росте сломается.

UI/UX

15. Нет селектора периода на дашборде. В preview была кнопка «7д/30д/90д», в рабочей версии — нет. Хардкод «сегодня + 7 дней».

16. Сайдбар не сжимается на мобильном. grid-cols-[240px_1fr] ломает layout на узких экранах.

17. Колонка «расхождение onchain vs db» в wallets всегда показывает «✓ совпадает». Потому что seed заполнил оба поля одинаково. Без E2 эта колонка бесполезна. Лучше пометить «нет данных» до подключения сетей.

18. Handbook рендерит markdown полу-ручным regexp в шаблоне (в admin-preview HTML-моке) — это плохо. В рабочей версии используется python-markdown, всё ок, но на фронте <style> инлайн в шаблоне. Лучше вынести в app.css.

19. Виджет «Последние операции» на дашборде показывает 6 строк. На загруженном дне их 20+. Кнопки «показать все» нет (есть ссылка «все →», ок).

20. Cashier на дашборде видит общий оборот по отделению, но не видит свой персональный. Может, надо разделить «оборот моей смены» vs «оборот моей кассы».

Что я бы починил первым, если продолжать

  1. Открытие смены + пересчёт кассы (фикс #1) — без этого балансы врут.
  2. Secure cookie + rate-limit на login (фиксы #6, #7) — 30 минут работы, важная гигиена.
  3. Реальная маржа с applied_fx_rate (фикс #2) — нужен ответ от руководителя про источник курса.
  4. Запись логина и матчинга в audit log (фикс #13) — критично для контроля «леваков», который и есть цель проекта.
  5. Скрытие KPI «флаги» от cashier (фикс #9) — одна строка.

Что я НЕ проверял и почему

  • Производительность под нагрузкойab/k6 не гонял. На 143 строках это бессмысленно.
  • XSS в шаблонах — Jinja2 экранирует по умолчанию, рискованные | safe есть только в handbook (markdown). Если в handbook попадёт пользовательский ввод — будет XSS. Сейчас контент захардкожен — ок.
  • Поведение при недоступной БД — connection-loss recovery. Пока БД жива.
  • Reset password / smena email — этого функционала нет вообще.
  • Audit-trail для самого логина — не пишу, не проверял.