2023-02-09 19:18:06 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-02 17:38:31 +00:00
|
|
|
"encoding/json"
|
2023-02-09 19:18:06 +00:00
|
|
|
"errors"
|
2026-04-02 15:49:30 +00:00
|
|
|
"strings"
|
2024-03-10 21:31:24 +00:00
|
|
|
|
2026-04-02 17:38:31 +00:00
|
|
|
"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"
|
2025-05-08 14:20:58 +00:00
|
|
|
"github.com/xlzd/gotp"
|
2023-02-09 19:18:06 +00:00
|
|
|
"gorm.io/gorm"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-02 18:02:25 +00:00
|
|
|
// 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"`
|
|
|
|
|
}
|
2026-04-02 18:02:25 +00:00
|
|
|
|
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.
|
2025-05-08 14:20:58 +00:00
|
|
|
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",
|
|
|
|
|
}
|
2026-04-25 17:08:22 +00:00
|
|
|
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
|
|
|
|
|
}
|
2026-04-25 17:08:22 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 19:52:41 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 17:26:53 +00:00
|
|
|
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{}
|
2025-05-03 09:27:53 +00:00
|
|
|
|
2023-02-09 19:18:06 +00:00
|
|
|
err := db.Model(model.User{}).
|
2025-05-08 14:20:58 +00:00
|
|
|
Where("username = ?", username).
|
2023-02-09 19:18:06 +00:00
|
|
|
First(user).
|
|
|
|
|
Error
|
|
|
|
|
if err == gorm.ErrRecordNotFound {
|
2026-03-04 17:26:53 +00:00
|
|
|
return nil, errors.New("invalid credentials")
|
2023-02-09 19:18:06 +00:00
|
|
|
} else if err != nil {
|
|
|
|
|
logger.Warning("check user err:", err)
|
2026-03-04 17:26:53 +00:00
|
|
|
return nil, err
|
2023-02-09 19:18:06 +00:00
|
|
|
}
|
2025-05-03 09:27:53 +00:00
|
|
|
|
2025-10-21 11:02:55 +00:00
|
|
|
if !crypto.CheckPasswordHash(user.Password, password) {
|
|
|
|
|
ldapEnabled, _ := s.settingService.GetLdapEnable()
|
|
|
|
|
if !ldapEnabled {
|
2026-03-04 17:26:53 +00:00
|
|
|
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 {
|
2026-03-04 17:26:53 +00:00
|
|
|
return nil, errors.New("invalid credentials")
|
2025-10-21 11:02:55 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-05-03 09:27:53 +00:00
|
|
|
|
2025-05-08 14:20:58 +00:00
|
|
|
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
|
2025-05-03 09:27:53 +00:00
|
|
|
if err != nil {
|
2025-05-08 14:20:58 +00:00
|
|
|
logger.Warning("check two factor err:", err)
|
2026-03-04 17:26:53 +00:00
|
|
|
return nil, err
|
2025-05-03 09:27:53 +00:00
|
|
|
}
|
|
|
|
|
|
2025-05-08 14:20:58 +00:00
|
|
|
if twoFactorEnable {
|
|
|
|
|
twoFactorToken, err := s.settingService.GetTwoFactorToken()
|
2023-02-09 19:18:06 +00:00
|
|
|
|
2025-05-08 14:20:58 +00:00
|
|
|
if err != nil {
|
|
|
|
|
logger.Warning("check two factor token err:", err)
|
2026-03-04 17:26:53 +00:00
|
|
|
return nil, err
|
2025-05-08 14:20:58 +00:00
|
|
|
}
|
2023-04-21 15:30:14 +00:00
|
|
|
|
2025-05-08 14:20:58 +00:00
|
|
|
if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode {
|
2026-03-17 21:30:05 +00:00
|
|
|
return nil, errors.New("invalid 2fa code")
|
2025-05-08 14:20:58 +00:00
|
|
|
}
|
2023-04-21 15:30:14 +00:00
|
|
|
}
|
2025-05-08 14:20:58 +00:00
|
|
|
|
2026-03-04 17:26:53 +00:00
|
|
|
return user, nil
|
2023-04-21 15:30:14 +00:00
|
|
|
}
|
|
|
|
|
|
2025-05-08 14:20:58 +00:00
|
|
|
func (s *UserService) UpdateUser(id int, username string, password string) error {
|
2024-03-12 17:15:44 +00:00
|
|
|
db := database.GetDB()
|
2025-05-08 14:20:58 +00:00
|
|
|
hashedPassword, err := crypto.HashPasswordAsBcrypt(password)
|
2024-03-12 17:15:44 +00:00
|
|
|
|
|
|
|
|
if err != nil {
|
2025-05-08 14:20:58 +00:00
|
|
|
return err
|
2024-03-12 17:15:44 +00:00
|
|
|
}
|
|
|
|
|
|
2025-07-02 09:25:25 +00:00
|
|
|
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if twoFactorEnable {
|
|
|
|
|
s.settingService.SetTwoFactorEnable(false)
|
|
|
|
|
s.settingService.SetTwoFactorToken("")
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-08 14:20:58 +00:00
|
|
|
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
|
2026-04-02 15:49:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hashedPassword, err := crypto.HashPasswordAsBcrypt(password)
|
|
|
|
|
if err != nil {
|
2026-04-04 06:59:40 +00:00
|
|
|
return nil, err
|
2026-04-02 15:49:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db := database.GetDB()
|
2026-04-04 06:59:40 +00:00
|
|
|
user := &model.User{
|
|
|
|
|
Username: username,
|
|
|
|
|
Password: hashedPassword,
|
|
|
|
|
Role: role,
|
|
|
|
|
}
|
2026-04-02 17:38:31 +00:00
|
|
|
|
2026-04-04 06:59:40 +00:00
|
|
|
err = db.Transaction(func(tx *gorm.DB) error {
|
2026-04-02 17:38:31 +00:00
|
|
|
if err := tx.Create(user).Error; err != nil {
|
2026-04-04 06:59:40 +00:00
|
|
|
if isUniqueConstraintError(err) {
|
2026-04-02 18:02:25 +00:00
|
|
|
return ErrUsernameAlreadyExists
|
2026-04-02 17:38:31 +00:00
|
|
|
}
|
|
|
|
|
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-02 17:38:31 +00:00
|
|
|
}
|
2026-04-04 06:59:40 +00:00
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return sanitizeUser(user), nil
|
|
|
|
|
}
|
2026-04-02 17:38:31 +00:00
|
|
|
|
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-02 17:38:31 +00:00
|
|
|
|
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-02 17:38:31 +00:00
|
|
|
|
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 {
|
2026-04-02 17:38:31 +00:00
|
|
|
return err
|
|
|
|
|
}
|
2026-04-04 06:59:40 +00:00
|
|
|
if adminCount <= 1 {
|
|
|
|
|
return ErrLastAdminRequired
|
2026-04-02 17:38:31 +00:00
|
|
|
}
|
2026-04-04 06:59:40 +00:00
|
|
|
}
|
2026-04-02 17:38:31 +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)
|
2026-04-02 17:38:31 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-04-04 06:59:40 +00:00
|
|
|
updates["password"] = hashedPassword
|
|
|
|
|
}
|
2026-04-02 17:38:31 +00:00
|
|
|
|
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-02 17:38:31 +00:00
|
|
|
}
|
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.
|
2026-04-04 19:52:41 +00:00
|
|
|
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-02 17:38:31 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 19:40:32 +00:00
|
|
|
if user.Role == "admin" {
|
|
|
|
|
adminCount, err := s.countAdmins(db)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2026-04-02 17:38:31 +00:00
|
|
|
}
|
2026-04-04 19:40:32 +00:00
|
|
|
if adminCount <= 1 {
|
|
|
|
|
return ErrLastAdminRequired
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 19:52:41 +00:00
|
|
|
if inboundService == nil {
|
|
|
|
|
inboundService = &InboundService{}
|
|
|
|
|
}
|
2026-04-04 19:40:32 +00:00
|
|
|
inbounds, err := inboundService.GetInbounds(id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
for _, inbound := range inbounds {
|
|
|
|
|
if _, err := inboundService.DelInbound(inbound.Id); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-04 19:52:41 +00:00
|
|
|
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-02 17:38:31 +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)
|
2026-04-02 17:38:31 +00:00
|
|
|
})
|
2026-04-02 15:49:30 +00:00
|
|
|
}
|
|
|
|
|
|
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")
|
|
|
|
|
}
|
2025-05-03 09:27:53 +00:00
|
|
|
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
|
2025-05-03 09:27:53 +00:00
|
|
|
user.Password = hashedPassword
|
2026-04-03 00:13:27 +00:00
|
|
|
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
|
2025-05-03 09:27:53 +00:00
|
|
|
user.Password = hashedPassword
|
2026-04-03 00:13:27 +00:00
|
|
|
user.Role = "admin"
|
2023-02-09 19:18:06 +00:00
|
|
|
return db.Save(user).Error
|
|
|
|
|
}
|