mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-05 12:24:20 +00:00
fa1a19c03c
Add .golangci.yml (v2): the standard linters plus bodyclose, errorlint, noctx, misspell, rowserrcheck, sqlclosecheck, unconvert, usestdlibvars, with gofumpt + goimports formatters. Enable the std-error-handling exclusion preset for idiomatic Close/Remove/Setenv ignores; scope-exclude SA1019 (parser.ParseDir in tools/openapigen) and ST1005 (intentional capitalized user-facing error copy that tests assert verbatim). No inline nolint directives were introduced. Resolve all 217 findings behavior-preserving: gofumpt/goimports formatting, explicit blank assignment on intentionally ignored errors, errors.Is/errors.As and %w wrapping, context-aware stdlib calls (CommandContext/QueryContext/NewRequestWithContext/Dialer), staticcheck simplifications, removed redundant conversions, http.StatusOK and http.MethodGet, inlined the go:fix intPtr helper, and deferred sql rows Close. Add a golangci CI job mirroring the existing Go jobs.
168 lines
4.2 KiB
Go
168 lines
4.2 KiB
Go
package panel
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/xlzd/gotp"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
|
|
ldaputil "github.com/mhsanaei/3x-ui/v3/internal/util/ldap"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
|
)
|
|
|
|
// UserService provides business logic for user management and authentication.
|
|
// It handles user creation, login, password management, and 2FA operations.
|
|
type UserService struct {
|
|
settingService service.SettingService
|
|
}
|
|
|
|
// GetFirstUser retrieves the first user from the database.
|
|
// This is typically used for initial setup or when there's only one admin user.
|
|
func (s *UserService) GetFirstUser() (*model.User, error) {
|
|
db := database.GetDB()
|
|
|
|
user := &model.User{}
|
|
err := db.Model(model.User{}).
|
|
First(user).
|
|
Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
func (s *UserService) CheckUser(username string, password string, twoFactorCode string) (*model.User, error) {
|
|
db := database.GetDB()
|
|
|
|
user := &model.User{}
|
|
|
|
err := db.Model(model.User{}).
|
|
Where("username = ?", username).
|
|
First(user).
|
|
Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("invalid credentials")
|
|
} else if err != nil {
|
|
logger.Warning("check user err:", err)
|
|
return nil, err
|
|
}
|
|
|
|
if !crypto.CheckPasswordHash(user.Password, password) {
|
|
ldapEnabled, _ := s.settingService.GetLdapEnable()
|
|
if !ldapEnabled {
|
|
return nil, errors.New("invalid credentials")
|
|
}
|
|
|
|
host, _ := s.settingService.GetLdapHost()
|
|
port, _ := s.settingService.GetLdapPort()
|
|
useTLS, _ := s.settingService.GetLdapUseTLS()
|
|
bindDN, _ := s.settingService.GetLdapBindDN()
|
|
ldapPass, _ := s.settingService.GetLdapPassword()
|
|
baseDN, _ := s.settingService.GetLdapBaseDN()
|
|
userFilter, _ := s.settingService.GetLdapUserFilter()
|
|
userAttr, _ := s.settingService.GetLdapUserAttr()
|
|
|
|
cfg := ldaputil.Config{
|
|
Host: host,
|
|
Port: port,
|
|
UseTLS: useTLS,
|
|
BindDN: bindDN,
|
|
Password: ldapPass,
|
|
BaseDN: baseDN,
|
|
UserFilter: userFilter,
|
|
UserAttr: userAttr,
|
|
}
|
|
ok, err := ldaputil.AuthenticateUser(cfg, username, password)
|
|
if err != nil || !ok {
|
|
return nil, errors.New("invalid credentials")
|
|
}
|
|
}
|
|
|
|
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
|
|
if err != nil {
|
|
logger.Warning("check two factor err:", err)
|
|
return nil, err
|
|
}
|
|
|
|
if twoFactorEnable {
|
|
twoFactorToken, err := s.settingService.GetTwoFactorToken()
|
|
if err != nil {
|
|
logger.Warning("check two factor token err:", err)
|
|
return nil, err
|
|
}
|
|
|
|
if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode {
|
|
return nil, errors.New("invalid 2fa code")
|
|
}
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (s *UserService) BumpLoginEpoch() error {
|
|
db := database.GetDB()
|
|
return db.Model(model.User{}).
|
|
Where("1 = 1").
|
|
Update("login_epoch", gorm.Expr("login_epoch + 1")).
|
|
Error
|
|
}
|
|
|
|
func (s *UserService) UpdateUser(id int, username string, password string) error {
|
|
db := database.GetDB()
|
|
hashedPassword, err := crypto.HashPasswordAsBcrypt(password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if twoFactorEnable {
|
|
_ = s.settingService.SetTwoFactorEnable(false)
|
|
_ = s.settingService.SetTwoFactorToken("")
|
|
}
|
|
|
|
return db.Model(model.User{}).
|
|
Where("id = ?", id).
|
|
Updates(map[string]any{
|
|
"username": username,
|
|
"password": hashedPassword,
|
|
"login_epoch": gorm.Expr("login_epoch + 1"),
|
|
}).
|
|
Error
|
|
}
|
|
|
|
func (s *UserService) UpdateFirstUser(username string, password string) error {
|
|
if username == "" {
|
|
return errors.New("username can not be empty")
|
|
} else if password == "" {
|
|
return errors.New("password can not be empty")
|
|
}
|
|
hashedPassword, er := crypto.HashPasswordAsBcrypt(password)
|
|
|
|
if er != nil {
|
|
return er
|
|
}
|
|
|
|
db := database.GetDB()
|
|
user := &model.User{}
|
|
err := db.Model(model.User{}).First(user).Error
|
|
if database.IsNotFound(err) {
|
|
user.Username = username
|
|
user.Password = hashedPassword
|
|
return db.Model(model.User{}).Create(user).Error
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
user.Username = username
|
|
user.Password = hashedPassword
|
|
user.LoginEpoch++
|
|
return db.Save(user).Error
|
|
}
|