package service import ( "errors" "time" "github.com/gin-gonic/gin" "hy2xs-admin/dao" "hy2xs-admin/model/bo" "hy2xs-admin/model/constant" "hy2xs-admin/model/entity" "hy2xs-admin/model/vo" "hy2xs-admin/util" ) func adminClaimsFromContext(c *gin.Context) (bo.AccountBo, bool) { if c == nil { return bo.AccountBo{}, false } v, ok := c.Get("adminClaims") if !ok { return bo.AccountBo{}, false } claims, castOK := v.(bo.AccountBo) return claims, castOK } // ErrInvalidCredentials — логин или пароль не подошли. // // ОДНО значение на оба случая, и это не упрощение. «Такого администратора // нет» и «пароль не тот» обязаны быть неразличимы снаружи: иначе форма входа // превращается в способ проверять существование имён администраторов, а // панель слушает только localhost именно потому, что вход — самая ценная // дверь продукта. // // Отказ хранилища при этом сюда НЕ сворачивается: слой данных уже умеет // отличать «записи нет» от «база не ответила» (dao.IsNotFound), и недоступная // SQLite обязана выглядеть как системная ошибка, а не как неверный пароль. var ErrInvalidCredentials = errors.New(constant.WrongPassword) func Login(username string, plainPassword string) (string, bool, error) { admin, err := dao.GetAdminUser("username = ? and status = 1", username) if err != nil { if dao.IsNotFound(err) { return "", false, ErrInvalidCredentials } return "", false, err } if !util.VerifyPassword(plainPassword, *admin.PasswordHash) { return "", false, ErrInvalidCredentials } tokenVersion := int64(1) if admin.TokenVersion != nil && *admin.TokenVersion > 0 { tokenVersion = *admin.TokenVersion } accountBo := bo.AccountBo{Id: *admin.Id, Username: *admin.Username, Roles: []string{"admin"}, Deleted: 0, TokenVersion: tokenVersion} token, tokenErr := GenToken(accountBo) if tokenErr != nil { return "", false, tokenErr } // Признак «пароль в старом формате, заставить сменить» отсюда убран вместе // с поддержкой самого формата: единственный источник этого требования — // флаг force_password_change, который ставит установка. requirePasswordChange := false if admin.ForcePasswordChange != nil { requirePasswordChange = *admin.ForcePasswordChange != 0 } return token, requirePasswordChange, nil } // GetAdminInfo читает учётные данные ТОЛЬКО из контекста запроса. // // Резервной ветки «если в контексте пусто — разобрать токен самостоятельно» // здесь больше нет. Она была недостижима: все маршруты, ведущие сюда, стоят за // middleware.JWTHandler, который разбирает токен, сверяет token_version и // кладёт claims в контекст. При этом ветка несла собственный разбор заголовка // Authorization через service.GetToken, где strings.SplitN(...)[1] паниковал // на заголовке без пробела. // // Хуже недостижимости было бы её оживление: этот путь не проверял ни статус // учётной записи, ни версию токена, то есть отозванный токен прошёл бы здесь, // но не прошёл бы через middleware. Второй способ аутентификации, слабее // первого, — не запасной вариант, а обход. func GetAdminInfo(c *gin.Context) (vo.AdminInfoVo, error) { claims, ok := adminClaimsFromContext(c) if !ok { return vo.AdminInfoVo{}, errors.New(constant.UnauthorizedError) } admin, err := dao.GetAdminUser("id = ?", claims.Id) if err != nil { return vo.AdminInfoVo{}, err } if admin.Status != nil && *admin.Status == 0 { return vo.AdminInfoVo{}, errors.New("this account has been disabled") } force := admin.ForcePasswordChange != nil && *admin.ForcePasswordChange != 0 return vo.AdminInfoVo{Id: claims.Id, Username: claims.Username, Roles: claims.Roles, ForcePasswordChange: force}, nil } func UpdateAdminLastLoginAt(id int64, loginAt int64) error { return dao.UpdateAdminUser([]int64{id}, map[string]interface{}{"last_login_at": loginAt}) } func ChangeAdminPassword(c *gin.Context, oldPassword string, newPassword string) error { info, err := GetAdminInfo(c) if err != nil { return err } admin, err := dao.GetAdminUser("id = ?", info.Id) if err != nil { return err } if admin.PasswordHash == nil { return errors.New(constant.SysError) } if !util.VerifyPassword(oldPassword, *admin.PasswordHash) { return errors.New(constant.WrongPassword) } hash, hashErr := util.HashPassword(newPassword) if hashErr != nil { return hashErr } nowMs := time.Now().UnixMilli() currentTokenVersion := int64(1) if admin.TokenVersion != nil && *admin.TokenVersion > 0 { currentTokenVersion = *admin.TokenVersion } return dao.UpdateAdminUser([]int64{info.Id}, map[string]interface{}{ "password_hash": hash, "force_password_change": 0, "password_changed_at": nowMs, "token_version": currentTokenVersion + 1, }) } func GetAdminForTokenValidation(id int64) (entity.AdminUser, error) { return dao.GetAdminUser("id = ?", id) }