- Логин/пароль администратора — `ADMIN_USERNAME` / `ADMIN_PASSWORD` из `.env` (создаётся при первом старте; пароль не короче 8 символов).
-`JWT_SECRET` обязателен и не короче 32 символов (заглушки отклоняются): без него приложение не стартует (изменение 017). `scripts/gen_env.py` генерирует корректные значения.
- Порт приложения публикуется на `${APP_BIND:-0.0.0.0}` (в `.env` можно задать `APP_BIND=127.0.0.1` — только loopback, за reverse-proxy с TLS, см. «Публикация»).
- «Свободные» адреса не хранятся, а вычисляются; в списке адресов они показываются для подсетей до /20. Страница `status=free` считается арифметически
(без перебора адресов, `offset` ≤ 10 000 000), остальные фильтры и постраничная выдача — в SQL (изменение 014).
- **IP уникален в пределах VRF и хранится в самом узком префиксе** (изменение 011): `addresses.vrf_id` + составной FK `(prefix_id, vrf_id)` (обновляется каскадом при переносе VRF),
уникальность `(vrf_id, address)`. Адрес из диапазона вложенного префикса в родителе назначить нельзя (422), дубль в VRF → 409; новый вложенный префикс забирает адреса родителя
из своего диапазона (`moved_addresses` в журнале); перенос префикса в VRF с тем же адресом → 409. Миграция 0007 останавливается при дублях — найти: `scripts/find_duplicate_addresses.py`.
- Адрес сети и broadcast (IPv4, префикс ≤ /30) назначить нельзя (422, изменение 015); уже внесённые найдёт `scripts/find_unusable_addresses.py`; загрузка не превышает 100 %.
Перенос адресов при создании вложенного префикса или смене VRF, из-за которого адрес стал бы сетевым/broadcast в новом месте, тоже отклоняется 422 с перечислением адресов и целевых
префиксов (изменение 024): создание префикса откатывается целиком, смена VRF — без частичных изменений.
- Автовыделение вложенного префикса (изменение 010): `POST /prefixes/{id}/subnets/next`с`length` (например 30) создаёт дочерний префикс в первом свободном выровненном блоке родителя (учитываются вложенные префиксы
и адреса родителя); `GET`с тем же путём и `?length=` — предпросмотр. Нет места → 409, недопустимый размер → 422. В UI — пункт «Добавить вложенный (авто)» в меню «⋯» префикса.
- VRF, тип устройства, организация с зависимыми объектами не удаляются (409). У организации без префиксов, устройств и операторов
служебный VRF `default` удаляется вместе с ней (исправлено в изменении 007: раньше такое удаление давало 409).
- Изменения дерева префиксов и раскладки адресов одного VRF (создание/удаление/перенос префикса, автовыделение, назначение адреса) выполняются по одному —
`pg_advisory_xact_lock` по `vrf_id` берётся до чтения дерева; разные VRF друг друга не блокируют, чтения (`GET`) не блокируются (изменение 029, находка №5).
Адреса префикса: `GET|POST /prefixes/{id}/addresses` (`status`, `q`, `limit`, `offset`), `POST …/addresses/next` — автоназначение из пула
(первый свободный адрес вне вложенных префиксов, без адреса сети/broadcast; префикс блокируется на время выдачи — изменение 020).
Пагинация во всех списках: `limit` ≥ 1 (и не больше предела эндпоинта), `offset` от 0 до 10 000 000; иначе 422 (изменение 013).
`PATCH`: `null` в текстовом поле очищает его (пустая строка), `null` в `status` адреса → 422, `device_id: null` отвязывает устройство (изменение 018).
`PATCH /devices/{id}` проверяет `name` (hostname/FQDN) и `mac` (формат, нормализация к `AA:BB:CC:DD:EE:FF`) так же, как создание — раньше PATCH пропускал некорректные значения (изменение 024).
Поиск (`q`) экранирует `%` и `_`. Ответы содержат заголовки `Content-Security-Policy` (кроме `/docs`), `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy` (изменение 023).
Ошибки: `{code, message, fields}` (для блокировок/лимитов — дополнительные поля `attempts_left`, `retry_after_seconds`). Каждое изменение пишется в `audit_log`.
-`POST /users/me/password {current_password, new_password}` — смена своего пароля, доступна любой роли; неверный текущий пароль → 403. Ответ содержит новый `access_token`:
прежние токены пользователя после смены пароля (самим или администратором) недействительны (изменение 023, claim `pv`).
-`PATCH /users/{id}`с`password` для **своей же** учётной записи → 422: свой пароль меняется только через `/users/me/password` (там обязательно подтверждение текущего пароля; изменение 024).
- Отключение действует немедленно (токен проверяется по `users.is_active` на каждом запросе). Роль `viewer` видит реестр и журнал, но любые изменения получает с 403.
- Роль по умолчанию при создании — `viewer` (изменение 016). Логин уникален без учёта регистра и в БД (изменение 021); снятие прав администратора и удаление пользователей идут под advisory-lock.
- Отказ в удалении по бизнес-правилу (409: организация, VRF, тип устройства, префикс с адресами, пользователь) фиксируется событием `<сущность>.delete_blocked` (изменение 008);
в «Данных» записи — `reason` и `blocked_by` со списками мешающих объектов (`total` и до 20 названий: префиксы, устройства, операторы, адреса).
-`GET /audit` — поиск и фильтры: `q` (сообщение, метка объекта, начало ID записи), `event_type` (`prefix.created`), `entity_type`, `actor` (`ui:admin`, `system`, `anonymous`), `date_from`/`date_to` (UTC), `limit`/`offset`; `GET /audit/summary`, `/audit/facets`, `/audit/{uid}`.
-`GET|PUT /journal/settings` — ротация: `retention_days` (по умолчанию 90) и `max_entries` (100 000), `0` — без ограничения. Ротация идёт раз в час и сразу при сохранении настроек
(advisory-lock защищает от параллельного запуска); каждая ротация с удалениями фиксируется записью `journal.rotated`. Запись — только admin.
-`POST /journal/clear {password}` — очистка с подтверждением пароля текущего пользователя (admin); 5 неверных попыток за 10 минут → блокировка на 10 минут (429). В журнале остаётся запись `journal.cleared`.
- **IP и метаданные запроса:** каждая запись, созданная в рамках HTTP-запроса, хранит `client_ip` и `meta` (`user_agent`, `method`, `path`, `request_id`; ответ содержит `X-Request-ID`);
системные события (ротация) — без IP. Фильтр `GET /audit?client_ip=` принимает IP или подсеть (`192.168.5.0/24`), текстовый поиск `q` ищет и по началу IP.
IP берётся из адреса сокета. `X-Forwarded-For` учитывается только от прокси из `TRUSTED_PROXIES` (CIDR через запятую в `.env`, по умолчанию пусто) — иначе IP можно подделать.
Запросы с самой машины через `127.0.0.1` Docker показывает адресом шлюза сети (`172.x.0.1`); с LAN-адреса и удалённых хостов виден реальный источник.
- В журнал пишутся также входы (`session.login`, `session.failed` — актор `anonymous`, только первая неудача в окне **по этому логину**; `session.locked` — блокировка, `diff.distinct_logins` —
число разных логинов с этого IP в окне, если сработал лимит по IP) и служебные события (`journal.*`, актор `system`; неверный пароль очистки — `journal.clear_failed` / `journal.clear_locked`).
Удаление префикса с `force=true` фиксирует `addresses_deleted`.
- **Вход:** три области лимита за 10 минут (блокировка — окно после последней неудачи): **логин + IP** — 5 неудач блокирует эту пару; **IP** — 20 неудач блокирует
любые логины с этого IP (как раньше); **логин** — 50 неудач со всех IP блокирует логин, но **не для «известных» IP** — тех, с которых этот пользователь уже успешно
входил за последние 30 дней (`known_logins`, обновляется при каждом успешном входе; устаревшие записи удаляет ротация). Так анонимный клиент, знающий логин
(например, `admin`), не может держать пользователя заблокированным с его обычного рабочего места — только с незнакомых IP (изменение 026, находка №2 ревью). 429 содержит
`Retry-After`/`retry_after_seconds`; для несуществующего логина время ответа выравнивается, пароль ≤ 128 символов.
Попытки одного логина сериализованы (`pg_advisory_xact_lock`, изменение 024), затем — попытки одного IP по всем логинам (изменение 027, находка №7: без этого перебор разных
логинов с одного IP проходит проверку лимита по IP параллельно и превышает его на степень параллелизма); порядок всегда «логин, затем IP», взаимная блокировка исключена.
Цена — попытки с одного IP (в том числе за NAT) обрабатываются по одной, время ответа при массовом переборе растёт на время проверки пароля.
При ротации по количеству первыми удаляются `session.failed` и `session.locked`. За reverse-proxy без `TRUSTED_PROXIES` все клиенты делят один IP — лимит по IP заденет всех.
- Контейнер приложения работает от непривилегированного пользователя (uid 10001), у сервиса есть healthcheck (`/healthz`), сервисы перезапускаются (`restart: unless-stopped`).
- Миграции при старте выполняются под advisory-lock — параллельные реплики не гоняют их одновременно. Зависимости зафиксированы в `requirements.lock`
- TLS: приложение отдаёт HTTP; для эксплуатации поставьте reverse-proxy (пример для Caddy) и задайте `APP_BIND=127.0.0.1`, `TRUSTED_PROXIES=<адрес прокси/сеть Docker>`:
Администратор может выбирать строки чекбоксами (в шапке — «выбрать все») на экранах «Организации», «Операторы», «Устройства», «Префиксы» (листовые), «Адреса» (кроме «Свободен») и «Пользователи» (кроме себя);
в «Журнале» выбора нет. Панель над таблицей: «Удалить» везде, «Сменить тип» (устройства), «Статус» (префиксы, адреса), «Разрешить/Отключить доступ» (пользователи). Операции идут по одному запросу
на объект (изменение 009): итог «выполнено N из M», отказы (зависимые объекты, свой аккаунт, последний администратор) показаны списком с причиной, попадают в журнал как `*.delete_blocked`,
и остаются выбранными. Префиксы удаляются без `force`: префикс с адресами удаляется из одиночного меню строки.
Строка реестра кликабельна целиком (как в журнале): «Префиксы» — лист открывает адреса подсети, родитель сворачивает/разворачивает ветку; «Организации» — префиксы организации;
«Операторы», «Устройства» и «Адреса» — окно редактирования (свободный адрес — «Назначить адрес» с этим IP). Ссылки, шеврон, меню «⋯» работают как раньше и не запускают действие строки;
Ctrl/Shift+клик и выделение текста тоже игнорируются.
Экран «Пользователи» доступен только администратору: создание, редактирование роли и доступа, удаление на месте, а свой пароль меняется кнопкой «Сменить пароль» в шапке.
Экраны «Префиксы» и «Адреса» догружают все страницы `/prefixes` организации (не только первые 1000), но не больше `PREFIX_UI_CAP` (20 000) префиксов; заголовок,
вкладка «Все» и подвал показывают `total`, а при срабатывании предела над таблицей появляется строка «Загружено X из N…» (изменение 028, находка №3).