Files
HY2XS_flamy/apps/model/dto/auth.go
T
founder a8407cf16b fix(admin): вход в панель падал на теге правила, пережившего переименование
RC2 на чистом Debian 13 завершался INSTALL EXIT CODE: 0 при полностью
недоступной панели. На LoginDto.Username стоял тег `validateStr` — правило с
таким именем не регистрировалось: при переименовании в `credentialStr` правка
не доехала до одного файла, оставив мёртвую регистрацию и живую ссылку на
несуществующее имя. go-playground/validator на неизвестный тег ПАНИКУЕТ при
разборе структуры, то есть до всякой проверки логина и пароля, а gin.Recovery
превращал панику в HTTP 500 на каждый POST /api/auth/login.

Дефект пережил 311 Go-тестов, и это главное, что здесь чинится. Проверялся сам
регексп, в обход валидатора, а обработчика входа не касался ни один тест.
Очевидная замена не помогла бы: цепочка правил поля обрывается на первом
несработавшем, поэтому нулевое DTO отказывает по `required` и до испорченного
тега не доходит. Теперь TestEveryValidationTagIsRegistered обходит исходники
apps/model/**, вытаскивает каждый тег `validate:"…"` и предъявляет его
валидатору отдельно — незарегистрированное правило паникует так же, как в бою,
но на сборке. Барьер проверен возвратом исходного тега.

Установка тоже не отвечала на вопрос, ради которого проверялась. Smoke считал
панель работающей по трём признакам — юнит активен, порт в LISTEN, /healthz
отвечает ok, — и все три были истинны. Теперь smoke выполняет настоящий вход
bootstrap-учётными данными и требует конверт успеха с непустым токеном: по коду
HTTP это неотличимо, админка отвечает 200 OK и на отказ. Отрицательная проба
идёт в любом режиме операции и от актуальности пароля не зависит.

Рядом лежали три расхождения того же класса, найденные при разборе.

Оркестратор не знал контракта, который сам порождает: HY2XS_ADMIN_USER по
умолчанию был `admin` — пять символов при минимуме панели в шесть, — и такая
установка проходила целиком, создавая учётную запись, под которой невозможно
войти. Про одно имя существовало три расходящихся умолчания. Оба значения
теперь проверяются при разборе окружения — той стороной, которая их порождает:
отказ, пришедший установщику, чинится строкой в hy2xs.env, а неработающий вход
на готовом сервере — переустановкой.

Панель была строже сервера. Форма входа ограничивала пароль 32 символами при
серверном пределе в 64, а форма смены пароля назначала до 64: пароль,
назначенный штатной операцией, после этого не вводился. Набор символов на
пароле отвергал значение, которое сервер принял бы, — сервер его не
ограничивает нигде. Контракт учётных данных объявлен один раз в
service/admin_credentials.go, копии в панели и оркестраторе сверяются с ним
тестами, читающими Go-исходник.

Класс символов логина был записан диапазоном по опечатке: неэкранированный
дефис превращал `+-=` в диапазон, впускающий `, - . / 0-9 : ; < =`. С серверным
набором это совпадало только потому, что обе стороны несли одну опечатку. Набор
записан явно и НЕ сужен — он уже действует на установленных серверах.

Визуально: красная рамка отказа обводила не то, что видит оператор. Element Plus
рисует состояние ошибки на el-input__wrapper селектором из четырёх классов, а
форма входа рисует видимую рамку поля на el-form-item — внутрь поля кладутся
иконка, ввод и переключатель видимости — и гасила чужую тень селектором из трёх,
проигрывая по специфичности. Рамка ложилась вокруг одного лишь ввода: у логина
начиналась после иконки, у пароля обрывалась перед «глазом». Индикация
перенесена на элемент, который оператор и видит полем; чужая тень гасится
селектором, повторяющим её собственный и добавляющим атрибут scoped-стиля, —
конкретностью, а не !important. Остальные формы панели проверены: собственная
рамка на el-form-item есть только на форме входа.

Заодно: `last_login_at` объявлен в схеме и в entity, а писать его было некому —
UpdateAdminLastLoginAt не вызывался ниоткуда. Отметка ставится в service.Login
сразу после успешной проверки пароля; отказ записи вход не отменяет, но
попадает в журнал. Обработчик входа переехал из controller/peer.go в
controller/auth.go: стек в journal указывал на управление пирами.

Требование теперь называется, а не сообщается фактом нарушения. «Неверный
формат логина» и «Некорректное значение» не давали оператору способа узнать,
что от него хотят: набор символов приходит из hy2xs.env и в панели нигде не
показан. Фразы форм и серверная причина credential_format перечисляют границы
и набор.

Гейт сборки run_admin_login_acceptance удерживает барьеры от тихого удаления —
по той же причине, что и гейт детектора гонок. Каждое из его утверждений
проверено мутационной пробой на реальный отказ; две первые редакции оказались
вакуумными и переписаны.

Прогнано: go vet + go test ./... , bun test оркестратора (427) и контрактов
панели (66), vue-tsc --noEmit, production-сборка frontend, гейт приёмки
целиком. `go test -race` не прогонялся — на машине нет C-компилятора, это
релизный гейт сборщика.

Прогон задокументирован в
docs/acceptance/2026-09-04-v1.0.0-rc2-runtime-findings.md.
2026-09-04 02:32:50 +05:00

53 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dto
// Контракт формы входа.
//
// Раньше на Username стояло `required,min=6,max=32,validateStr`, и правила
// `validateStr` не существовало: оно пережило переименование в `credentialStr`
// и осталось ссылкой на незарегистрированную функцию. go-playground/validator в
// таком случае ПАНИКУЕТ, поэтому каждый POST /api/auth/login заканчивался
// паникой, которую gin.Recovery превращал в HTTP 500 — панель после чистой
// установки была недоступна целиком. Ни один тест этого не видел: проверялся
// сам регексп, а не LoginDto через production-валидатор, и цепочка правил на
// нулевом DTO обрывается на `required` раньше, чем доходит до испорченного
// тега.
//
// Границы длины логина живут ВНУТРИ правила `credentialStr`, а не рядом с ним.
// Два правила на одном поле уже приводили к отказу, который невозможно
// объяснить: имя пира с `min=1,max=32,validateStr` принималось одним правилом
// и отвергалось другим, а оператор видел «invalid». Здесь та же конструкция
// была бы той же ошибкой.
//
// У пароля набора символов НЕТ, и это осознанно: сервер нигде его не
// ограничивает — ни при установке, ни при смене пароля. Ограничение на входе
// не защищало бы ничего (значение всё равно сравнивается с хешем) и умело
// только запирать оператора с паролем, который сервер принял бы.
//
// Числа в тегах обязаны совпадать с service.AdminPassword*Length; за этим
// следит TestLoginPasswordBoundsMatchContract — в теге структуры константу не
// подставить.
type LoginDto struct {
Username *string `json:"username" form:"username" validate:"required,credentialStr"`
Pass *string `json:"pass" form:"pass" validate:"required,min=6,max=64"`
}
// Normalize: незаполненное поле обязано выглядеть незаполненным.
//
// У поля-указателя `required` считает значение заданным по ненулевому
// указателю, поэтому `{"username":"","pass":""}` проходило `required` целиком и
// получало отказ по набору символов и по длине — то есть оператор, отправивший
// пустую форму, читал про недопустимые символы вместо «поле обязательно».
//
// Логин при этом ещё и триммится: скопированное из bootstrap-admin.secret
// значение регулярно приезжает с пробелом или переводом строки на конце, и
// раньше это давало «неверный логин или пароль» — отказ, который невозможно
// связать с причиной.
//
// Пароль НЕ триммится. Набор его символов не ограничен, пробел по краям —
// законная часть значения, и молчаливое его удаление означало бы, что панель
// отправляет не то, что ввёл оператор.
func (d *LoginDto) Normalize() {
blankToNil(&d.Username)
emptyToNil(&d.Pass)
}