3x-ui/web/service/user.go

541 lines
14 KiB
Go
Raw Normal View History

2023-02-09 19:18:06 +00:00
package service
import (
"encoding/json"
2023-02-09 19:18:06 +00:00
"errors"
"strings"
"github.com/google/uuid"
2025-09-19 08:05:43 +00:00
"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"
2025-10-21 11:02:55 +00:00
ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap"
"github.com/xlzd/gotp"
2023-02-09 19:18:06 +00:00
"gorm.io/gorm"
)
// ErrUsernameAlreadyExists is returned when a user tries to register with a taken username.
var ErrUsernameAlreadyExists = errors.New("username already exists")
2026-04-04 06:59:40 +00:00
var ErrInvalidUserRole = errors.New("role must be admin or user")
var ErrUserNotFound = errors.New("user not found")
var ErrCannotDeleteSelf = errors.New("cannot delete current user")
var ErrLastAdminRequired = errors.New("at least one admin user must remain")
var ErrCannotDemoteSelf = errors.New("cannot change your own role to non-admin")
// UserInfo is the sanitized user payload returned to the frontend.
type UserInfo struct {
Id int `json:"id"`
Username string `json:"username"`
Role string `json:"role"`
}
2025-09-20 07:35:50 +00:00
// UserService provides business logic for user management and authentication.
// It handles user creation, login, password management, and 2FA operations.
type UserService struct {
settingService SettingService
}
2023-02-09 19:18:06 +00:00
2026-04-04 06:59:40 +00:00
func normalizeManagedUserInput(username string, password string, role string, passwordRequired bool) (string, string, string, error) {
username = strings.TrimSpace(username)
password = strings.TrimSpace(password)
role = strings.ToLower(strings.TrimSpace(role))
if role == "" {
role = "user"
}
if username == "" {
return "", "", "", errors.New("username can not be empty")
}
if len(username) < 3 || len(username) > 64 {
return "", "", "", errors.New("username must be 3-64 characters")
}
if role != "admin" && role != "user" {
return "", "", "", ErrInvalidUserRole
}
if passwordRequired && password == "" {
return "", "", "", errors.New("password can not be empty")
}
if password != "" && (len(password) < 8 || len(password) > 128) {
return "", "", "", errors.New("password must be 8-128 characters")
}
return username, password, role, nil
}
func sanitizeUser(user *model.User) *UserInfo {
if user == nil {
return nil
}
return &UserInfo{
Id: user.Id,
Username: user.Username,
Role: user.Role,
}
}
func isUniqueConstraintError(err error) bool {
if err == nil {
return false
}
errMsg := err.Error()
return strings.Contains(errMsg, "UNIQUE constraint failed") || strings.Contains(errMsg, "Duplicate")
}
func (s *UserService) countAdmins(tx *gorm.DB) (int64, error) {
var count int64
err := tx.Model(&model.User{}).Where("role = ?", "admin").Count(&count).Error
return count, err
}
func (s *UserService) addUserClientsToAllInbounds(tx *gorm.DB, username string, inboundService *InboundService) error {
inbounds, err := inboundService.GetAllInbounds()
if err != nil {
return err
}
for _, inbound := range inbounds {
clientID := uuid.New().String()
client := model.Client{
ID: clientID,
Email: username,
Enable: false,
SubID: uuid.New().String()[:8],
Comment: "auto-added on registration",
}
if shouldAutoFillVisionFlow(inbound.Protocol, inbound.StreamSettings) {
client.Flow = "xtls-rprx-vision"
}
2026-04-04 06:59:40 +00:00
clientEntry := map[string]any{
"email": client.Email,
"enable": client.Enable,
"totalGB": 0,
"expiryTime": 0,
"limitIp": 0,
"subId": client.SubID,
"comment": client.Comment,
"created_at": 0,
"updated_at": 0,
}
switch inbound.Protocol {
case "trojan":
clientEntry["password"] = clientID
case "shadowsocks":
clientEntry["password"] = clientID
default:
clientEntry["id"] = clientID
}
if client.Flow != "" {
clientEntry["flow"] = client.Flow
}
2026-04-04 06:59:40 +00:00
var settings map[string]any
if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
return err
}
clientsRaw, ok := settings["clients"].([]any)
if !ok {
clientsRaw = []any{}
}
clientsRaw = append(clientsRaw, clientEntry)
settings["clients"] = clientsRaw
newSettings, err := json.Marshal(settings)
if err != nil {
return err
}
inbound.Settings = string(newSettings)
if err := tx.Model(&model.Inbound{}).Where("id = ?", inbound.Id).Update("settings", inbound.Settings).Error; err != nil {
return err
}
if err := inboundService.AddClientStat(tx, inbound.Id, &client); err != nil {
return err
}
}
return nil
}
func (s *UserService) removeUserClientsFromAllInbounds(tx *gorm.DB, username string, inboundService *InboundService) error {
inbounds, err := inboundService.GetAllInbounds()
if err != nil {
return err
}
for _, inbound := range inbounds {
var settings map[string]any
if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
return err
}
clientsRaw, ok := settings["clients"].([]any)
if !ok {
continue
}
newClients := make([]any, 0, len(clientsRaw))
removedEmails := make(map[string]struct{})
for _, clientRaw := range clientsRaw {
clientMap, ok := clientRaw.(map[string]any)
if !ok {
newClients = append(newClients, clientRaw)
continue
}
email, _ := clientMap["email"].(string)
if strings.EqualFold(email, username) {
if email != "" {
removedEmails[email] = struct{}{}
}
continue
}
newClients = append(newClients, clientRaw)
}
if len(removedEmails) == 0 {
continue
}
settings["clients"] = newClients
newSettings, err := json.Marshal(settings)
if err != nil {
return err
}
if err := tx.Model(&model.Inbound{}).Where("id = ?", inbound.Id).Update("settings", string(newSettings)).Error; err != nil {
return err
}
for email := range removedEmails {
if err := inboundService.DelClientStat(tx, inbound.Id, email); err != nil {
return err
}
if err := inboundService.DelClientIPs(tx, email); err != nil {
return err
}
}
}
return nil
}
2025-09-20 07:35:50 +00:00
// GetFirstUser retrieves the first user from the database.
// This is typically used for initial setup or when there's only one admin user.
2023-02-09 19:18:06 +00:00
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) {
2023-02-09 19:18:06 +00:00
db := database.GetDB()
user := &model.User{}
2023-02-09 19:18:06 +00:00
err := db.Model(model.User{}).
Where("username = ?", username).
2023-02-09 19:18:06 +00:00
First(user).
Error
if err == gorm.ErrRecordNotFound {
return nil, errors.New("invalid credentials")
2023-02-09 19:18:06 +00:00
} else if err != nil {
logger.Warning("check user err:", err)
return nil, err
2023-02-09 19:18:06 +00:00
}
2025-10-21 11:02:55 +00:00
if !crypto.CheckPasswordHash(user.Password, password) {
ldapEnabled, _ := s.settingService.GetLdapEnable()
if !ldapEnabled {
return nil, errors.New("invalid credentials")
2025-10-21 11:02:55 +00:00
}
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")
2025-10-21 11:02:55 +00:00
}
}
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()
2023-02-09 19:18:06 +00:00
if err != nil {
logger.Warning("check two factor token err:", err)
return nil, err
}
if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode {
2026-03-17 21:30:05 +00:00
return nil, errors.New("invalid 2fa code")
}
}
return user, nil
}
func (s *UserService) UpdateUser(id int, username string, password string) error {
2024-03-12 17:15:44 +00:00
db := database.GetDB()
hashedPassword, err := crypto.HashPasswordAsBcrypt(password)
2024-03-12 17:15:44 +00:00
if err != nil {
return err
2024-03-12 17:15:44 +00:00
}
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
2024-03-12 17:15:44 +00:00
}
2026-04-04 06:59:40 +00:00
// GetUsers returns all panel users without sensitive fields.
func (s *UserService) GetUsers() ([]UserInfo, error) {
db := database.GetDB()
users := make([]UserInfo, 0)
err := db.Model(&model.User{}).
Select("id", "username", "role").
Order("id asc").
Find(&users).
Error
return users, err
}
// CreateUser creates a new managed user.
func (s *UserService) CreateUser(username string, password string, role string, inboundService *InboundService) (*UserInfo, error) {
username, password, role, err := normalizeManagedUserInput(username, password, role, true)
if err != nil {
return nil, err
}
hashedPassword, err := crypto.HashPasswordAsBcrypt(password)
if err != nil {
2026-04-04 06:59:40 +00:00
return nil, err
}
db := database.GetDB()
2026-04-04 06:59:40 +00:00
user := &model.User{
Username: username,
Password: hashedPassword,
Role: role,
}
2026-04-04 06:59:40 +00:00
err = db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(user).Error; err != nil {
2026-04-04 06:59:40 +00:00
if isUniqueConstraintError(err) {
return ErrUsernameAlreadyExists
}
return err
}
2026-04-04 06:59:40 +00:00
if role == "user" {
if err := s.addUserClientsToAllInbounds(tx, username, inboundService); err != nil {
return err
}
}
2026-04-04 06:59:40 +00:00
return nil
})
if err != nil {
return nil, err
}
return sanitizeUser(user), nil
}
2026-04-04 06:59:40 +00:00
// UpdateManagedUser updates username, password, and role for a managed user.
func (s *UserService) UpdateManagedUser(id int, username string, password string, role string, currentUserId int) (*UserInfo, error) {
username, password, role, err := normalizeManagedUserInput(username, password, role, false)
if err != nil {
return nil, err
}
2026-04-04 06:59:40 +00:00
db := database.GetDB()
user := &model.User{}
if err := db.Model(&model.User{}).Where("id = ?", id).First(user).Error; err != nil {
if database.IsNotFound(err) {
return nil, ErrUserNotFound
}
return nil, err
}
2026-04-04 06:59:40 +00:00
if currentUserId == id && role != "admin" {
return nil, ErrCannotDemoteSelf
}
err = db.Transaction(func(tx *gorm.DB) error {
if user.Role == "admin" && role != "admin" {
adminCount, err := s.countAdmins(tx)
if err != nil {
return err
}
2026-04-04 06:59:40 +00:00
if adminCount <= 1 {
return ErrLastAdminRequired
}
2026-04-04 06:59:40 +00:00
}
2026-04-04 06:59:40 +00:00
updates := map[string]any{
"username": username,
"role": role,
}
if password != "" {
hashedPassword, err := crypto.HashPasswordAsBcrypt(password)
if err != nil {
return err
}
2026-04-04 06:59:40 +00:00
updates["password"] = hashedPassword
}
2026-04-04 06:59:40 +00:00
if err := tx.Model(&model.User{}).Where("id = ?", id).Updates(updates).Error; err != nil {
if isUniqueConstraintError(err) {
return ErrUsernameAlreadyExists
}
2026-04-04 06:59:40 +00:00
return err
}
return tx.Model(&model.User{}).Where("id = ?", id).First(user).Error
})
if err != nil {
return nil, err
}
return sanitizeUser(user), nil
}
// DeleteUser deletes a managed user.
func (s *UserService) DeleteUser(id int, currentUserId int, inboundService *InboundService) error {
2026-04-04 06:59:40 +00:00
if id == currentUserId {
return ErrCannotDeleteSelf
}
2026-04-04 06:59:40 +00:00
db := database.GetDB()
user := &model.User{}
if err := db.Model(&model.User{}).Where("id = ?", id).First(user).Error; err != nil {
if database.IsNotFound(err) {
return ErrUserNotFound
}
return err
}
if user.Role == "admin" {
adminCount, err := s.countAdmins(db)
if err != nil {
return err
}
if adminCount <= 1 {
return ErrLastAdminRequired
}
}
if inboundService == nil {
inboundService = &InboundService{}
}
inbounds, err := inboundService.GetInbounds(id)
if err != nil {
return err
}
for _, inbound := range inbounds {
if _, err := inboundService.DelInbound(inbound.Id); err != nil {
return err
}
}
return db.Transaction(func(tx *gorm.DB) error {
if err := s.removeUserClientsFromAllInbounds(tx, user.Username, inboundService); err != nil {
return err
}
return tx.Delete(&model.User{}, id).Error
})
2026-04-04 06:59:40 +00:00
}
2026-04-04 06:59:40 +00:00
func (s *UserService) RegisterUser(username string, password string, inboundService *InboundService) error {
username, password, _, err := normalizeManagedUserInput(username, password, "user", true)
if err != nil {
return err
}
hashedPassword, err := crypto.HashPasswordAsBcrypt(password)
if err != nil {
return err
}
db := database.GetDB()
// Create user and add as client to all inbounds in a single transaction
return db.Transaction(func(tx *gorm.DB) error {
user := &model.User{
Username: username,
Password: hashedPassword,
Role: "user",
}
if err := tx.Create(user).Error; err != nil {
if isUniqueConstraintError(err) {
return ErrUsernameAlreadyExists
}
return err
}
return s.addUserClientsToAllInbounds(tx, username, inboundService)
})
}
2023-02-09 19:18:06 +00:00
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
}
2023-02-09 19:18:06 +00:00
db := database.GetDB()
user := &model.User{}
err := db.Model(model.User{}).First(user).Error
if database.IsNotFound(err) {
user.Username = username
user.Password = hashedPassword
user.Role = "admin"
2023-02-09 19:18:06 +00:00
return db.Model(model.User{}).Create(user).Error
} else if err != nil {
return err
}
user.Username = username
user.Password = hashedPassword
user.Role = "admin"
2023-02-09 19:18:06 +00:00
return db.Save(user).Error
}