mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
When the admin changes username/password from one machine, sessions on every other machine kept working until they manually logged out because session storage is a signed client-side cookie — there is no server-side session list to revoke. Add a per-user LoginEpoch counter stamped into the session at login and re-verified on every authenticated request. UpdateUser and UpdateFirstUser bump the epoch (UpdateUser via gorm.Expr so a single update statement is atomic), so any cookie issued before the change no longer matches the user's current epoch and GetLoginUser returns nil — the SPA's 401 interceptor then redirects to the login page. Backward compatible: the column defaults to 0 and missing cookie values are treated as 0, so sessions issued before this change remain valid until the first credential update.
159 lines
3.9 KiB
Go
159 lines
3.9 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/database"
|
|
"github.com/mhsanaei/3x-ui/v3/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/logger"
|
|
"github.com/mhsanaei/3x-ui/v3/util/crypto"
|
|
ldaputil "github.com/mhsanaei/3x-ui/v3/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, error) {
|
|
db := database.GetDB()
|
|
|
|
user := &model.User{}
|
|
|
|
err := db.Model(model.User{}).
|
|
Where("username = ?", username).
|
|
First(user).
|
|
Error
|
|
if 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) 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
|
|
}
|