Самокритика 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) — без этого балансы врут.
Securecookie + rate-limit на login (фиксы #6, #7) — 30 минут работы, важная гигиена.- Реальная маржа с
applied_fx_rate(фикс #2) — нужен ответ от руководителя про источник курса. - Запись логина и матчинга в audit log (фикс #13) — критично для контроля «леваков», который и есть цель проекта.
- Скрытие KPI «флаги» от cashier (фикс #9) — одна строка.
Что я НЕ проверял и почему
- Производительность под нагрузкой —
ab/k6не гонял. На 143 строках это бессмысленно. - XSS в шаблонах — Jinja2 экранирует по умолчанию, рискованные
| safeесть только в handbook (markdown). Если в handbook попадёт пользовательский ввод — будет XSS. Сейчас контент захардкожен — ок. - Поведение при недоступной БД — connection-loss recovery. Пока БД жива.
- Reset password / smena email — этого функционала нет вообще.
- Audit-trail для самого логина — не пишу, не проверял.