140 lines
3.4 KiB
Go
140 lines
3.4 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"
|
|
"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) {
|
|
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
|
|
}
|