Files
ipam_control/docs/changes/035-user-last-login/PLAN.md
T
ayurishchevandClaude Opus 5.5 8384c2c311 Задачи 034-035: публикация через Caddy, последний вход пользователя
034 Публикация стенда как https://rxipam.rxmsk.ru через общий Caddy хоста (Caddyfile вне репозитория):
    UI и /api/v1 доступны из интернета, /docs, /redoc, /openapi.json — только из частных сетей
    (RFC 1918/4193, loopback). Caddy проксирует на опубликованный порт приложения — общая Docker-сеть
    отклонена из-за коллизии имён app/db с Nextcloud. TRUSTED_PROXIES=172.16.0.0/12: реальный IP
    клиента из X-Forwarded-For. Интеграционный тест журнала больше не проверяет подделку XFF с хоста
    (хост за Caddy доверенный), логику покрывает unit-тест.
035 Дата и IP последнего входа в разделе «Пользователи»: users.last_login_at / last_login_ip
    (миграция 0014 с заполнением из журнала), запись при успешном входе, UserOut, колонка в UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:45:30 +03:00

5.5 KiB
Raw Blame History

Последний вход пользователя: дата и IP на странице «Пользователи» (изменение 035)

Context

Суперадминистратору нужно видеть в разделе «Пользователи», когда каждый пользователь последний раз входил в систему и с какого IP. Сейчас это можно найти только поиском по журналу. known_logins для этого не подходит: там IP за 30 дней, ротация удаляет старые записи, и он хранит набор IP, а не последний вход.

Решение: два поля в users — время и IP последнего успешного входа. Поля обновляются в POST /auth/login, отдаются в UserOut и выводятся колонкой в таблице пользователей. IP берётся тот же, что уже вычисляет request_context для журнала: реальный адрес клиента за Caddy (TRUSTED_PROXIES, изменение 034).

Изменения

БД и модель

  • app/models.py::User:
    • last_login_at: Mapped[datetime | None] (DateTime(timezone=True));
    • last_login_ip: Mapped[str | None] (INET);
    • комментарий «изменение 035».
  • Миграция alembic/versions/0014_users_last_login.py (down_revision 0013):
    • две nullable-колонки;
    • backfill из журнала: для каждого пользователя берётся последнее событие session.login (DISTINCT ON (username) … ORDER BY username, ts DESC) по audit_log.username = users.username, из него — ts и client_ip;
    • если журнал ротирован или очищен, значения остаются NULL («—» в UI).
    • downgrade — удалить колонки.

API

  • app/api/v1/auth.py::login, ветка успешного входа (рядом с _remember_login): user.last_login_at = func.now(), user.last_login_ip = ip, в той же транзакции, что и запись session.login. Неудачные попытки поля не трогают.
  • app/schemas.py::UserOut: last_login_at: datetime | None = None, last_login_ip: str | None = None. INET приводить к строке тем же способом, что уже используется для client_ip в AuditOut и адресов (ip_text), чтобы в JSON был "192.168.5.9", а не объект. UserOut собирается через model_validate из ORM, поэтому нужен @field_validator("last_login_ip", mode="before") с вызовом ip_text(). UserOut отдают и /users (только superadmin), и /auth/me (свои данные) — утечки чужих IP нет.

UI (web/app.js::screens.users)

  • Новая колонка «Последний вход» после «Статус»:
    • дата — fmtDate(u.last_login_at) (как в журнале, UTC);
    • под ней или через разделитель — IP моноширинным шрифтом, muted;
    • «—», если входа не было.
  • Расширить cols (selCols(...)) и заголовок .th на одну колонку. Ширину подобрать так, чтобы таблица не переполнялась, по образцу соседних колонок (≈170–190px).

Тесты (минимально)

  • tests/test_users.py::test_users_management: после входа созданного пользователя (_client(name, ...)) GET /users?q=<name> → у записи заполнены last_login_at и last_login_ip. Новых тестов не добавлять.

Документация (правила проекта)

  • docs/changes/035-user-last-login/PLAN.md — этот план; по завершении — SUMMARY.md.
  • README.md:
    • раздел «Интерфейс»: в «Пользователях» видны дата и IP последнего входа;
    • миграции 0001–0014;
    • строка 035 в истории изменений.

Исполнение

По принятой схеме: код и тест пишет агент на Sonnet (без запуска тестов и стенда), затем ревью, пересборка стенда, проверки и SUMMARY — моя часть.

Проверка

  • docker compose -p ipam_control_006 up -d --build app → alembic current = 0014, alembic check — чисто; откат до 0013 и повторный upgrade — без ошибок.
  • Backfill: у суперадминистратора из .env, который входил, last_login_at/last_login_ip совпадают с последним session.login в журнале.
  • Вход через Caddy с LAN-адреса хоста (--resolve rxipam.rxmsk.ru:443:192.168.5.9) → last_login_ip = 192.168.5.9; неудачная попытка не меняет поля.
  • GET /users — поля в JSON строками; pytest -q — все зелёные.
  • UI: колонка «Последний вход» с датой и IP, «—» у пользователей без входа. В браузере проверяет пользователь, если нужно.