package service import ( "errors" "github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/crypto" ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap" "github.com/xlzd/gotp" "gorm.io/gorm" ) // UserService provides business logic for user management and authentication. // It handles user creation, login, password management, and 2FA operations. type UserService struct { settingService 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 { db := database.GetDB() user := &model.User{} err := db.Model(model.User{}). Where("username = ?", username). First(user). Error if err == gorm.ErrRecordNotFound { return nil } else if err != nil { logger.Warning("check user err:", err) return nil } // If LDAP enabled and local password check fails, attempt LDAP auth if !crypto.CheckPasswordHash(user.Password, password) { ldapEnabled, _ := s.settingService.GetLdapEnable() if !ldapEnabled { return nil } 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 } // On successful LDAP auth, continue 2FA checks below } twoFactorEnable, err := s.settingService.GetTwoFactorEnable() if err != nil { logger.Warning("check two factor err:", err) return nil } if twoFactorEnable { twoFactorToken, err := s.settingService.GetTwoFactorToken() if err != nil { logger.Warning("check two factor token err:", err) return nil } if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode { return nil } } return user } 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}). 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 return db.Save(user).Error }