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». Здесь та же конструкция // была бы той же ошибкой. // // У пароля границы длины живут внутри правила `adminPassword` по той же // причине. Здесь стояло `min=6,max=64`, и эта запись была не только вторым // правилом на одном поле, но и НЕПОЛНОЙ: `max` считает символы Unicode, а // bcrypt — байты, поэтому 64 кириллические буквы (128 байт) проходили проверку // и падали в хешировании. Границу в байтах тегом валидатора не выразить. // // Набора символов у пароля НЕТ, и это осознанно: сервер нигде его не // ограничивает — ни при установке, ни при смене пароля. Ограничение на входе // не защищало бы ничего (значение всё равно сравнивается с хешем) и умело // только запирать оператора с паролем, который сервер принял бы. Единственное // исключение — управляющие символы, и оно принадлежит контракту, а не форме: // см. credential.IsValidAdminPassword. type LoginDto struct { Username *string `json:"username" form:"username" validate:"required,credentialStr"` Pass *string `json:"pass" form:"pass" validate:"required,adminPassword"` } // Normalize: незаполненное поле обязано выглядеть незаполненным. // // У поля-указателя `required` считает значение заданным по ненулевому // указателю, поэтому `{"username":"","pass":""}` проходило `required` целиком и // получало отказ по набору символов и по длине — то есть оператор, отправивший // пустую форму, читал про недопустимые символы вместо «поле обязательно». // // Логин при этом ещё и триммится: скопированное из bootstrap-admin.secret // значение регулярно приезжает с пробелом или переводом строки на конце, и // раньше это давало «неверный логин или пароль» — отказ, который невозможно // связать с причиной. // // Пароль НЕ триммится. Набор его символов не ограничен, пробел по краям — // законная часть значения, и молчаливое его удаление означало бы, что панель // отправляет не то, что ввёл оператор. func (d *LoginDto) Normalize() { blankToNil(&d.Username) emptyToNil(&d.Pass) }