mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +03:00
2e14a2a51a
A failed login keeps its username in the in-memory log buffer, the limiter table and the notifier queue, and the route accepted any body the global cap allowed. The login route now takes at most 16 KiB and answers a username over 256 bytes with the generic failure before any of those see it; the user setters refuse such a name too, so no stored username can be one the login route turns away.
194 lines
5.3 KiB
Go
194 lines
5.3 KiB
Go
package panel
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"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/util/totp"
|
|
"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
|
|
}
|
|
|
|
// HasDefaultCredential reports that nobody has chosen the login yet: a database
|
|
// holds no user before the first start, and an older one may still hold admin/admin.
|
|
func (s *UserService) HasDefaultCredential() (bool, error) {
|
|
user, err := s.GetFirstUser()
|
|
if database.IsNotFound(err) {
|
|
return true, nil
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return user.Username == "admin" && crypto.CheckPasswordHash(user.Password, "admin"), 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()
|
|
skipVerify, _ := s.settingService.GetLdapInsecureSkipVerify()
|
|
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,
|
|
InsecureSkipVerify: skipVerify,
|
|
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 !totp.VerifyWithSkew(twoFactorToken, twoFactorCode, time.Now()) {
|
|
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 {
|
|
if len(username) > MaxUsernameLength {
|
|
return fmt.Errorf("username can not be longer than %d bytes", MaxUsernameLength)
|
|
}
|
|
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 len(username) > MaxUsernameLength {
|
|
return fmt.Errorf("username can not be longer than %d bytes", MaxUsernameLength)
|
|
} 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
|
|
}
|
|
|
|
// MaxUsernameLength bounds a panel username: the login route refuses longer names before
|
|
// logging or recording them, so no stored name may exceed it.
|
|
const MaxUsernameLength = 256
|