# Итог: последний вход пользователя — дата и IP (изменение 035) План: `PLAN.md`. В разделе «Пользователи» появилась колонка «Последний вход»: дата (UTC) и IP последнего успешного входа. ## Что сделано | Файл | Изменение | |---|---| | `app/models.py` | `User.last_login_at` (`timestamptz`), `User.last_login_ip` (`INET`), nullable | | `alembic/versions/0014_users_last_login.py` | Колонки и перенос данных из журнала: последнее событие `session.login` на логин. Если журнал ротирован или очищен — `NULL` | | `app/api/v1/auth.py` | Успешный вход записывает `last_login_at = now()` и `last_login_ip` в той же транзакции, что и `session.login`. IP — тот же, что в журнале (реальный клиент за Caddy, изменение 034). Неудачные попытки поля не трогают | | `app/schemas.py` | `UserOut.last_login_at`, `UserOut.last_login_ip`. INET приводится к строке через `field_validator` с существующей `ip_text()`. Поля видны в `/users` (только `superadmin`) и в `/auth/me` (свои) | | `web/app.js` | Колонка «Последний вход» после «Статус»: дата и IP моноширинным шрифтом, «—», если входа не было | | `tests/test_users.py` | В `test_users_management`: после входа созданного пользователя поля заполнены в `GET /users` | | `README.md` | «Интерфейс», миграции `0001`–`0014`, строка 035 в истории | ## Проверки - Стенд пересобран: `alembic current` — `0014`, `alembic check` — чисто, откат до 0013 и повторный upgrade — без ошибок. - Перенос данных: у `tstark` дата и IP (`192.168.5.253`) совпадают с последним `session.login` в журнале; у `sbas-sa` входов не было — `NULL`. - Временный `viewer` (создан и удалён через SQL): вход через Caddy с LAN-адреса хоста дал `last_login_ip = 192.168.5.9` и дату входа, в JSON — строки. Неудачная попытка поля не изменила. - `node --check web/app.js`, `import app.main` — чисто. - `pytest -q` — 15 passed. Перед прогоном учётная запись `admin` из `.env` (удалена через UI в 08:54, фикстура `client` без неё не работает) создана заново через SQL как `superadmin` с паролем из `.env`; попытки входа под удалённым `admin` (блокировка «логин + IP») очищены. Создание через SQL в журнал не попало. ## Не проверено - Колонка в браузере — не просматривалась.