mirror of
				https://github.com/MHSanaei/3x-ui.git
				synced 2025-10-29 19:32:51 +00:00 
			
		
		
		
	Merge branch 'main' into bot_develop
This commit is contained in:
		
						commit
						00dfdce79d
					
				
					 30 changed files with 305 additions and 65 deletions
				
			
		|  | @ -33,7 +33,7 @@ | ||||||
| 
 | 
 | ||||||
| لتثبيت المشروع أو تحديثه، نفذ الأمر ده: | لتثبيت المشروع أو تحديثه، نفذ الأمر ده: | ||||||
| ```bash | ```bash | ||||||
| bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) | bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/refs/tags/v2.5.8/install.sh) | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## تثبيت النسخة القديمة (مش موصى بيها) | ## تثبيت النسخة القديمة (مش موصى بيها) | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ | ||||||
| ## Instalar y Actualizar | ## Instalar y Actualizar | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) | bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/refs/tags/v2.5.8/install.sh) | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## Instalar versión antigua (no recomendamos) | ## Instalar versión antigua (no recomendamos) | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ | ||||||
| ## نصب و ارتقا | ## نصب و ارتقا | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) | bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/refs/tags/v2.5.8/install.sh) | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## نصب نسخههای قدیمی (توصیه نمیشود) | ## نصب نسخههای قدیمی (توصیه نمیشود) | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ | ||||||
| ## Install & Upgrade | ## Install & Upgrade | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) | bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/refs/tags/v2.5.8/install.sh) | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## Install legacy Version (we don't recommend) | ## Install legacy Version (we don't recommend) | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ | ||||||
| ## Установка и обновление | ## Установка и обновление | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) | bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/refs/tags/v2.5.8/install.sh) | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## Установить старую версию (мы не рекомендуем) | ## Установить старую версию (мы не рекомендуем) | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ | ||||||
| ## 安装 & 升级 | ## 安装 & 升级 | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) | bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/refs/tags/v2.5.8/install.sh) | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## 安装旧版本 (我们不建议) | ## 安装旧版本 (我们不建议) | ||||||
|  |  | ||||||
|  | @ -7,9 +7,11 @@ import ( | ||||||
| 	"log" | 	"log" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
|  | 	"slices" | ||||||
| 
 | 
 | ||||||
| 	"x-ui/config" | 	"x-ui/config" | ||||||
| 	"x-ui/database/model" | 	"x-ui/database/model" | ||||||
|  | 	"x-ui/util/crypto" | ||||||
| 	"x-ui/xray" | 	"x-ui/xray" | ||||||
| 
 | 
 | ||||||
| 	"gorm.io/driver/sqlite" | 	"gorm.io/driver/sqlite" | ||||||
|  | @ -33,6 +35,7 @@ func initModels() error { | ||||||
| 		&model.Setting{}, | 		&model.Setting{}, | ||||||
| 		&model.InboundClientIps{}, | 		&model.InboundClientIps{}, | ||||||
| 		&xray.ClientTraffic{}, | 		&xray.ClientTraffic{}, | ||||||
|  | 		&model.HistoryOfSeeders{}, | ||||||
| 	} | 	} | ||||||
| 	for _, model := range models { | 	for _, model := range models { | ||||||
| 		if err := db.AutoMigrate(model); err != nil { | 		if err := db.AutoMigrate(model); err != nil { | ||||||
|  | @ -50,9 +53,16 @@ func initUser() error { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if empty { | 	if empty { | ||||||
|  | 		hashedPassword, err := crypto.HashPasswordAsBcrypt(defaultPassword) | ||||||
|  | 
 | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Printf("Error hashing default password: %v", err) | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		user := &model.User{ | 		user := &model.User{ | ||||||
| 			Username:    defaultUsername, | 			Username:    defaultUsername, | ||||||
| 			Password:    defaultPassword, | 			Password:    hashedPassword, | ||||||
| 			LoginSecret: defaultSecret, | 			LoginSecret: defaultSecret, | ||||||
| 		} | 		} | ||||||
| 		return db.Create(user).Error | 		return db.Create(user).Error | ||||||
|  | @ -60,6 +70,45 @@ func initUser() error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func runSeeders(isUsersEmpty bool) error { | ||||||
|  | 	empty, err := isTableEmpty("history_of_seeders") | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Printf("Error checking if users table is empty: %v", err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if empty && isUsersEmpty { | ||||||
|  | 		hashSeeder := &model.HistoryOfSeeders{ | ||||||
|  | 			SeederName: "UserPasswordHash", | ||||||
|  | 		} | ||||||
|  | 		return db.Create(hashSeeder).Error | ||||||
|  | 	} else { | ||||||
|  | 		var seedersHistory []string | ||||||
|  | 		db.Model(&model.HistoryOfSeeders{}).Pluck("seeder_name", &seedersHistory) | ||||||
|  | 
 | ||||||
|  | 		if !slices.Contains(seedersHistory, "UserPasswordHash") && !isUsersEmpty { | ||||||
|  | 			var users []model.User | ||||||
|  | 			db.Find(&users) | ||||||
|  | 
 | ||||||
|  | 			for _, user := range users { | ||||||
|  | 				hashedPassword, err := crypto.HashPasswordAsBcrypt(user.Password) | ||||||
|  | 				if err != nil { | ||||||
|  | 					log.Printf("Error hashing password for user '%s': %v", user.Username, err) | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 				db.Model(&user).Update("password", hashedPassword) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			hashSeeder := &model.HistoryOfSeeders{ | ||||||
|  | 				SeederName: "UserPasswordHash", | ||||||
|  | 			} | ||||||
|  | 			return db.Create(hashSeeder).Error | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func isTableEmpty(tableName string) (bool, error) { | func isTableEmpty(tableName string) (bool, error) { | ||||||
| 	var count int64 | 	var count int64 | ||||||
| 	err := db.Table(tableName).Count(&count).Error | 	err := db.Table(tableName).Count(&count).Error | ||||||
|  | @ -92,11 +141,13 @@ func InitDB(dbPath string) error { | ||||||
| 	if err := initModels(); err != nil { | 	if err := initModels(); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	isUsersEmpty, err := isTableEmpty("users") | ||||||
|  | 
 | ||||||
| 	if err := initUser(); err != nil { | 	if err := initUser(); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 	return runSeeders(isUsersEmpty) | ||||||
| 	return nil |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func CloseDB() error { | func CloseDB() error { | ||||||
|  |  | ||||||
|  | @ -63,6 +63,11 @@ type InboundClientIps struct { | ||||||
| 	Ips         string `json:"ips" form:"ips"` | 	Ips         string `json:"ips" form:"ips"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | type HistoryOfSeeders struct { | ||||||
|  | 	Id         int    `json:"id" gorm:"primaryKey;autoIncrement"` | ||||||
|  | 	SeederName string `json:"seederName"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { | func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { | ||||||
| 	listen := i.Listen | 	listen := i.Listen | ||||||
| 	if listen != "" { | 	if listen != "" { | ||||||
|  |  | ||||||
|  | @ -82,14 +82,13 @@ gen_random_string() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| config_after_install() { | config_after_install() { | ||||||
|     local existing_username=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'username: .+' | awk '{print $2}') |     local existing_hasDefaultCredential=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}') | ||||||
|     local existing_password=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'password: .+' | awk '{print $2}') |  | ||||||
|     local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') |     local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') | ||||||
|     local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') |     local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') | ||||||
|     local server_ip=$(curl -s https://api.ipify.org) |     local server_ip=$(curl -s https://api.ipify.org) | ||||||
| 
 | 
 | ||||||
|     if [[ ${#existing_webBasePath} -lt 4 ]]; then |     if [[ ${#existing_webBasePath} -lt 4 ]]; then | ||||||
|         if [[ "$existing_username" == "admin" && "$existing_password" == "admin" ]]; then |         if [[ "$existing_hasDefaultCredential" == "true" ]]; then | ||||||
|             local config_webBasePath=$(gen_random_string 15) |             local config_webBasePath=$(gen_random_string 15) | ||||||
|             local config_username=$(gen_random_string 10) |             local config_username=$(gen_random_string 10) | ||||||
|             local config_password=$(gen_random_string 10) |             local config_password=$(gen_random_string 10) | ||||||
|  | @ -112,7 +111,6 @@ config_after_install() { | ||||||
|             echo -e "${green}WebBasePath: ${config_webBasePath}${plain}" |             echo -e "${green}WebBasePath: ${config_webBasePath}${plain}" | ||||||
|             echo -e "${green}Access URL: http://${server_ip}:${config_port}/${config_webBasePath}${plain}" |             echo -e "${green}Access URL: http://${server_ip}:${config_port}/${config_webBasePath}${plain}" | ||||||
|             echo -e "###############################################" |             echo -e "###############################################" | ||||||
|             echo -e "${yellow}If you forgot your login info, you can type 'x-ui settings' to check${plain}" |  | ||||||
|         else |         else | ||||||
|             local config_webBasePath=$(gen_random_string 15) |             local config_webBasePath=$(gen_random_string 15) | ||||||
|             echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}" |             echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}" | ||||||
|  | @ -121,7 +119,7 @@ config_after_install() { | ||||||
|             echo -e "${green}Access URL: http://${server_ip}:${existing_port}/${config_webBasePath}${plain}" |             echo -e "${green}Access URL: http://${server_ip}:${existing_port}/${config_webBasePath}${plain}" | ||||||
|         fi |         fi | ||||||
|     else |     else | ||||||
|         if [[ "$existing_username" == "admin" && "$existing_password" == "admin" ]]; then |         if [[ "$existing_hasDefaultCredential" == "true" ]]; then | ||||||
|             local config_username=$(gen_random_string 10) |             local config_username=$(gen_random_string 10) | ||||||
|             local config_password=$(gen_random_string 10) |             local config_password=$(gen_random_string 10) | ||||||
| 
 | 
 | ||||||
|  | @ -132,7 +130,6 @@ config_after_install() { | ||||||
|             echo -e "${green}Username: ${config_username}${plain}" |             echo -e "${green}Username: ${config_username}${plain}" | ||||||
|             echo -e "${green}Password: ${config_password}${plain}" |             echo -e "${green}Password: ${config_password}${plain}" | ||||||
|             echo -e "###############################################" |             echo -e "###############################################" | ||||||
|             echo -e "${yellow}If you forgot your login info, you can type 'x-ui settings' to check${plain}" |  | ||||||
|         else |         else | ||||||
|             echo -e "${green}Username, Password, and WebBasePath are properly set. Exiting...${plain}" |             echo -e "${green}Username, Password, and WebBasePath are properly set. Exiting...${plain}" | ||||||
|         fi |         fi | ||||||
|  |  | ||||||
							
								
								
									
										13
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								main.go
									
									
									
									
									
								
							|  | @ -16,6 +16,7 @@ import ( | ||||||
| 	"x-ui/web" | 	"x-ui/web" | ||||||
| 	"x-ui/web/global" | 	"x-ui/web/global" | ||||||
| 	"x-ui/web/service" | 	"x-ui/web/service" | ||||||
|  | 	"x-ui/util/crypto" | ||||||
| 
 | 
 | ||||||
| 	"github.com/op/go-logging" | 	"github.com/op/go-logging" | ||||||
| ) | ) | ||||||
|  | @ -151,9 +152,7 @@ func showSetting(show bool) { | ||||||
| 			fmt.Println("get current user info failed, error info:", err) | 			fmt.Println("get current user info failed, error info:", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		username := userModel.Username | 		if userModel.Username == "" || userModel.Password == "" { | ||||||
| 		userpasswd := userModel.Password |  | ||||||
| 		if username == "" || userpasswd == "" { |  | ||||||
| 			fmt.Println("current username or password is empty") | 			fmt.Println("current username or password is empty") | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -163,8 +162,12 @@ func showSetting(show bool) { | ||||||
| 		} else { | 		} else { | ||||||
| 			fmt.Println("Panel is secure with SSL") | 			fmt.Println("Panel is secure with SSL") | ||||||
| 		} | 		} | ||||||
| 		fmt.Println("username:", username) | 
 | ||||||
| 		fmt.Println("password:", userpasswd) | 		hasDefaultCredential := func() bool { | ||||||
|  | 			return userModel.Username == "admin" && crypto.CheckPasswordHash(userModel.Password, "admin") | ||||||
|  | 		}() | ||||||
|  | 
 | ||||||
|  | 		fmt.Println("hasDefaultCredential:", hasDefaultCredential) | ||||||
| 		fmt.Println("port:", port) | 		fmt.Println("port:", port) | ||||||
| 		fmt.Println("webBasePath:", webBasePath) | 		fmt.Println("webBasePath:", webBasePath) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
							
								
								
									
										15
									
								
								util/crypto/crypto.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								util/crypto/crypto.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | ||||||
|  | package crypto | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"golang.org/x/crypto/bcrypt" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func HashPasswordAsBcrypt(password string) (string, error) { | ||||||
|  | 	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) | ||||||
|  | 	return string(hash), err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func CheckPasswordHash(hash, password string) bool { | ||||||
|  | 	err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) | ||||||
|  | 	return err == nil | ||||||
|  | } | ||||||
|  | @ -44,6 +44,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) { | ||||||
| 	g.POST("/stopXrayService", a.stopXrayService) | 	g.POST("/stopXrayService", a.stopXrayService) | ||||||
| 	g.POST("/restartXrayService", a.restartXrayService) | 	g.POST("/restartXrayService", a.restartXrayService) | ||||||
| 	g.POST("/installXray/:version", a.installXray) | 	g.POST("/installXray/:version", a.installXray) | ||||||
|  | 	g.POST("/updateGeofile/:fileName", a.updateGeofile) | ||||||
| 	g.POST("/logs/:count", a.getLogs) | 	g.POST("/logs/:count", a.getLogs) | ||||||
| 	g.POST("/getConfigJson", a.getConfigJson) | 	g.POST("/getConfigJson", a.getConfigJson) | ||||||
| 	g.GET("/getDb", a.getDb) | 	g.GET("/getDb", a.getDb) | ||||||
|  | @ -95,7 +96,13 @@ func (a *ServerController) getXrayVersion(c *gin.Context) { | ||||||
| func (a *ServerController) installXray(c *gin.Context) { | func (a *ServerController) installXray(c *gin.Context) { | ||||||
| 	version := c.Param("version") | 	version := c.Param("version") | ||||||
| 	err := a.serverService.UpdateXray(version) | 	err := a.serverService.UpdateXray(version) | ||||||
| 	jsonMsg(c, I18nWeb(c, "install")+" xray", err) | 	jsonMsg(c, I18nWeb(c, "pages.index.xraySwitchVersionPopover"), err) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *ServerController) updateGeofile(c *gin.Context) { | ||||||
|  | 	fileName := c.Param("fileName") | ||||||
|  | 	err := a.serverService.UpdateGeofile(fileName) | ||||||
|  | 	jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), err) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *ServerController) stopXrayService(c *gin.Context) { | func (a *ServerController) stopXrayService(c *gin.Context) { | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | 	"x-ui/util/crypto" | ||||||
| 	"x-ui/web/entity" | 	"x-ui/web/entity" | ||||||
| 	"x-ui/web/service" | 	"x-ui/web/service" | ||||||
| 	"x-ui/web/session" | 	"x-ui/web/session" | ||||||
|  | @ -84,7 +85,7 @@ func (a *SettingController) updateUser(c *gin.Context) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	user := session.GetLoginUser(c) | 	user := session.GetLoginUser(c) | ||||||
| 	if user.Username != form.OldUsername || user.Password != form.OldPassword { | 	if user.Username != form.OldUsername || !crypto.CheckPasswordHash(user.Password, form.OldPassword) { | ||||||
| 		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect"))) | 		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect"))) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | @ -95,7 +96,7 @@ func (a *SettingController) updateUser(c *gin.Context) { | ||||||
| 	err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword) | 	err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		user.Username = form.NewUsername | 		user.Username = form.NewUsername | ||||||
| 		user.Password = form.NewPassword | 		user.Password, _ = crypto.HashPasswordAsBcrypt(form.NewPassword) | ||||||
| 		session.SetLoginUser(c, user) | 		session.SetLoginUser(c, user) | ||||||
| 	} | 	} | ||||||
| 	jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err) | 	jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err) | ||||||
|  |  | ||||||
|  | @ -22,11 +22,14 @@ | ||||||
|   .ant-backup-list-item { |   .ant-backup-list-item { | ||||||
|     gap: 10px; |     gap: 10px; | ||||||
|   } |   } | ||||||
|   .ant-xray-version-list-item { |   .ant-version-list-item { | ||||||
|     --padding: 12px; |     --padding: 12px; | ||||||
|     padding: var(--padding) !important; |     padding: var(--padding) !important; | ||||||
|     gap: var(--padding); |     gap: var(--padding); | ||||||
|   } |   } | ||||||
|  |   .dark .ant-version-list-item svg{ | ||||||
|  |     color: var(--dark-color-text-primary); | ||||||
|  |   } | ||||||
|   .dark .ant-backup-list-item svg, |   .dark .ant-backup-list-item svg, | ||||||
|   .dark .ant-badge-status-text, |   .dark .ant-badge-status-text, | ||||||
|   .dark .ant-card-extra { |   .dark .ant-card-extra { | ||||||
|  | @ -43,7 +46,7 @@ | ||||||
|     border-color: var(--color-primary-100); |     border-color: var(--color-primary-100); | ||||||
|   } |   } | ||||||
|   .dark .ant-backup-list,  |   .dark .ant-backup-list,  | ||||||
|   .dark .ant-xray-version-list, |   .dark .ant-version-list, | ||||||
|   .dark .ant-card-actions, |   .dark .ant-card-actions, | ||||||
|   .dark .ant-card-actions>li:not(:last-child) { |   .dark .ant-card-actions>li:not(:last-child) { | ||||||
|     border-color: var(--dark-color-stroke); |     border-color: var(--dark-color-stroke); | ||||||
|  | @ -353,14 +356,25 @@ | ||||||
|     </a-layout> |     </a-layout> | ||||||
|     <a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' :closable="true" |     <a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' :closable="true" | ||||||
|         @ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer=""> |         @ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer=""> | ||||||
|       <a-alert type="warning" :style="{ marginBottom: '12px', width: '100%' }" |       <a-collapse default-active-key="1"> | ||||||
|         message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert> |         <a-collapse-panel key="1" header='Xray'> | ||||||
|       <a-list class="ant-xray-version-list" bordered :style="{ width: '100%' }"> |           <a-alert type="warning" :style="{ marginBottom: '12px', width: '100%' }" message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert> | ||||||
|         <a-list-item class="ant-xray-version-list-item" v-for="version, index in versionModal.versions"> |           <a-list class="ant-version-list" bordered :style="{ width: '100%' }"> | ||||||
|           <a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ version ]]</a-tag> |             <a-list-item class="ant-version-list-item" v-for="version, index in versionModal.versions"> | ||||||
|           <a-radio :class="themeSwitcher.currentTheme" :checked="version === `v${status.xray.version}`" @click="switchV2rayVersion(version)"></a-radio> |               <a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ version ]]</a-tag> | ||||||
|         </a-list-item> |               <a-radio :class="themeSwitcher.currentTheme" :checked="version === `v${status.xray.version}`" @click="switchV2rayVersion(version)"></a-radio> | ||||||
|       </a-list> |             </a-list-item> | ||||||
|  |           </a-list> | ||||||
|  |         </a-collapse-panel> | ||||||
|  |         <a-collapse-panel key="2" header='Geofiles'> | ||||||
|  |           <a-list class="ant-version-list" bordered :style="{ width: '100%' }"> | ||||||
|  |             <a-list-item class="ant-version-list-item" v-for="file, index in ['geosite.dat', 'geoip.dat', 'geosite_IR.dat', 'geoip_IR.dat', 'geosite_RU.dat', 'geoip_RU.dat']"> | ||||||
|  |               <a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ file ]]</a-tag> | ||||||
|  |               <a-icon type="reload" @click="updateGeofile(file)" :style="{ marginRight: '8px' }"/> | ||||||
|  |             </a-list-item> | ||||||
|  |           </a-list> | ||||||
|  |         </a-collapse-panel> | ||||||
|  |       </a-collapse> | ||||||
|     </a-modal> |     </a-modal> | ||||||
|     <a-modal id="log-modal" v-model="logModal.visible" |     <a-modal id="log-modal" v-model="logModal.visible" | ||||||
|         :closable="true" @cancel="() => logModal.visible = false" |         :closable="true" @cancel="() => logModal.visible = false" | ||||||
|  | @ -645,7 +659,7 @@ | ||||||
|             switchV2rayVersion(version) { |             switchV2rayVersion(version) { | ||||||
|                 this.$confirm({ |                 this.$confirm({ | ||||||
|                     title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}', |                     title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}', | ||||||
|                     content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`, |                     content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}'.replace('#version#', version), | ||||||
|                     okText: '{{ i18n "confirm"}}', |                     okText: '{{ i18n "confirm"}}', | ||||||
|                     class: themeSwitcher.currentTheme, |                     class: themeSwitcher.currentTheme, | ||||||
|                     cancelText: '{{ i18n "cancel"}}', |                     cancelText: '{{ i18n "cancel"}}', | ||||||
|  | @ -657,6 +671,21 @@ | ||||||
|                     }, |                     }, | ||||||
|                 }); |                 }); | ||||||
|             }, |             }, | ||||||
|  |             updateGeofile(fileName) { | ||||||
|  |                 this.$confirm({ | ||||||
|  |                     title: '{{ i18n "pages.index.geofileUpdateDialog" }}', | ||||||
|  |                     content: '{{ i18n "pages.index.geofileUpdateDialogDesc" }}'.replace("#filename#", fileName), | ||||||
|  |                     okText: '{{ i18n "confirm"}}', | ||||||
|  |                     class: themeSwitcher.currentTheme, | ||||||
|  |                     cancelText: '{{ i18n "cancel"}}', | ||||||
|  |                     onOk: async () => { | ||||||
|  |                         versionModal.hide(); | ||||||
|  |                         this.loading(true, '{{ i18n "pages.index.dontRefresh"}}'); | ||||||
|  |                         await HttpUtil.post(`/server/updateGeofile/${fileName}`); | ||||||
|  |                         this.loading(false); | ||||||
|  |                     }, | ||||||
|  |                 }); | ||||||
|  |             }, | ||||||
|             async stopXrayService() { |             async stopXrayService() { | ||||||
|                 this.loading(true); |                 this.loading(true); | ||||||
|                 const msg = await HttpUtil.post('server/stopXrayService'); |                 const msg = await HttpUtil.post('server/stopXrayService'); | ||||||
|  |  | ||||||
|  | @ -287,6 +287,7 @@ | ||||||
|                     { label: 'Malware 🇮🇷', value: 'ext:geosite_IR.dat:malware' }, |                     { label: 'Malware 🇮🇷', value: 'ext:geosite_IR.dat:malware' }, | ||||||
|                     { label: 'Phishing 🇮🇷', value: 'ext:geosite_IR.dat:phishing' }, |                     { label: 'Phishing 🇮🇷', value: 'ext:geosite_IR.dat:phishing' }, | ||||||
|                     { label: 'Cryptominers 🇮🇷', value: 'ext:geosite_IR.dat:cryptominers' }, |                     { label: 'Cryptominers 🇮🇷', value: 'ext:geosite_IR.dat:cryptominers' }, | ||||||
|  |                     { label: 'Adult +18', value: 'geosite:category-porn' }, | ||||||
|                     { label: '🇮🇷 Iran', value: 'ext:geosite_IR.dat:ir' }, |                     { label: '🇮🇷 Iran', value: 'ext:geosite_IR.dat:ir' }, | ||||||
|                     { label: '🇮🇷 .ir', value: 'regexp:.*\\.ir$' }, |                     { label: '🇮🇷 .ir', value: 'regexp:.*\\.ir$' }, | ||||||
|                     { label: '🇮🇷 .ایران', value: 'regexp:.*\\.xn--mgba3a4f16a$' }, |                     { label: '🇮🇷 .ایران', value: 'regexp:.*\\.xn--mgba3a4f16a$' }, | ||||||
|  |  | ||||||
|  | @ -591,6 +591,66 @@ func (s *ServerService) ImportDB(file multipart.File) error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (s *ServerService) UpdateGeofile(fileName string) error { | ||||||
|  |     files := []struct { | ||||||
|  |         URL      string | ||||||
|  |         FileName string | ||||||
|  |     }{ | ||||||
|  |         {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip.dat"}, | ||||||
|  |         {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite.dat"}, | ||||||
|  |         {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat", "geoip_IR.dat"}, | ||||||
|  |         {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat", "geosite_IR.dat"}, | ||||||
|  |         {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip_RU.dat"}, | ||||||
|  |         {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite_RU.dat"}, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     downloadFile := func(url, destPath string) error { | ||||||
|  |         resp, err := http.Get(url) | ||||||
|  |         if err != nil { | ||||||
|  |             return common.NewErrorf("Failed to download Geofile from %s: %v", url, err) | ||||||
|  |         } | ||||||
|  |         defer resp.Body.Close() | ||||||
|  | 
 | ||||||
|  |         file, err := os.Create(destPath) | ||||||
|  |         if err != nil { | ||||||
|  |             return common.NewErrorf("Failed to create Geofile %s: %v", destPath, err) | ||||||
|  |         } | ||||||
|  |         defer file.Close() | ||||||
|  | 
 | ||||||
|  |         _, err = io.Copy(file, resp.Body) | ||||||
|  |         if err != nil { | ||||||
|  |             return common.NewErrorf("Failed to save Geofile %s: %v", destPath, err) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return nil | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var fileURL string | ||||||
|  |     for _, file := range files { | ||||||
|  |         if file.FileName == fileName { | ||||||
|  |             fileURL = file.URL | ||||||
|  |             break | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if fileURL == "" { | ||||||
|  |         return common.NewErrorf("File '%s' not found in the list of Geofiles", fileName) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     destPath := fmt.Sprintf("%s/%s", config.GetBinFolderPath(), fileName) | ||||||
|  | 
 | ||||||
|  |     if err := downloadFile(fileURL, destPath); err != nil { | ||||||
|  |         return common.NewErrorf("Error downloading Geofile '%s': %v", fileName, err) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     err := s.RestartXrayService() | ||||||
|  |     if err != nil { | ||||||
|  |         return common.NewErrorf("Updated Geofile '%s' but Failed to start Xray: %v", fileName, err) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (s *ServerService) GetNewX25519Cert() (any, error) { | func (s *ServerService) GetNewX25519Cert() (any, error) { | ||||||
| 	// Run the command
 | 	// Run the command
 | ||||||
| 	cmd := exec.Command(xray.GetBinaryPath(), "x25519") | 	cmd := exec.Command(xray.GetBinaryPath(), "x25519") | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import ( | ||||||
| 	"x-ui/database" | 	"x-ui/database" | ||||||
| 	"x-ui/database/model" | 	"x-ui/database/model" | ||||||
| 	"x-ui/logger" | 	"x-ui/logger" | ||||||
|  | 	"x-ui/util/crypto" | ||||||
| 
 | 
 | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
|  | @ -29,8 +30,9 @@ func (s *UserService) CheckUser(username string, password string, secret string) | ||||||
| 	db := database.GetDB() | 	db := database.GetDB() | ||||||
| 
 | 
 | ||||||
| 	user := &model.User{} | 	user := &model.User{} | ||||||
|  | 
 | ||||||
| 	err := db.Model(model.User{}). | 	err := db.Model(model.User{}). | ||||||
| 		Where("username = ? and password = ? and login_secret = ?", username, password, secret). | 		Where("username = ? and login_secret = ?", username, secret). | ||||||
| 		First(user). | 		First(user). | ||||||
| 		Error | 		Error | ||||||
| 	if err == gorm.ErrRecordNotFound { | 	if err == gorm.ErrRecordNotFound { | ||||||
|  | @ -39,14 +41,25 @@ func (s *UserService) CheckUser(username string, password string, secret string) | ||||||
| 		logger.Warning("check user err:", err) | 		logger.Warning("check user err:", err) | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 	return user | 
 | ||||||
|  | 	if crypto.CheckPasswordHash(user.Password, password) { | ||||||
|  | 		return user | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *UserService) UpdateUser(id int, username string, password string) error { | func (s *UserService) UpdateUser(id int, username string, password string) error { | ||||||
| 	db := database.GetDB() | 	db := database.GetDB() | ||||||
|  | 	hashedPassword, err := crypto.HashPasswordAsBcrypt(password) | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return db.Model(model.User{}). | 	return db.Model(model.User{}). | ||||||
| 		Where("id = ?", id). | 		Where("id = ?", id). | ||||||
| 		Updates(map[string]any{"username": username, "password": password}). | 		Updates(map[string]any{"username": username, "password": hashedPassword}). | ||||||
| 		Error | 		Error | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -100,17 +113,23 @@ func (s *UserService) UpdateFirstUser(username string, password string) error { | ||||||
| 	} else if password == "" { | 	} else if password == "" { | ||||||
| 		return errors.New("password can not be empty") | 		return errors.New("password can not be empty") | ||||||
| 	} | 	} | ||||||
|  | 	hashedPassword, er := crypto.HashPasswordAsBcrypt(password) | ||||||
|  | 
 | ||||||
|  | 	if er != nil { | ||||||
|  | 		return er | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	db := database.GetDB() | 	db := database.GetDB() | ||||||
| 	user := &model.User{} | 	user := &model.User{} | ||||||
| 	err := db.Model(model.User{}).First(user).Error | 	err := db.Model(model.User{}).First(user).Error | ||||||
| 	if database.IsNotFound(err) { | 	if database.IsNotFound(err) { | ||||||
| 		user.Username = username | 		user.Username = username | ||||||
| 		user.Password = password | 		user.Password = hashedPassword | ||||||
| 		return db.Model(model.User{}).Create(user).Error | 		return db.Model(model.User{}).Create(user).Error | ||||||
| 	} else if err != nil { | 	} else if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	user.Username = username | 	user.Username = username | ||||||
| 	user.Password = password | 	user.Password = hashedPassword | ||||||
| 	return db.Save(user).Error | 	return db.Save(user).Error | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -122,8 +122,12 @@ | ||||||
| "totalData" = "إجمالي البيانات" | "totalData" = "إجمالي البيانات" | ||||||
| "sent" = "مرسل" | "sent" = "مرسل" | ||||||
| "received" = "مستقبل" | "received" = "مستقبل" | ||||||
| "xraySwitchVersionDialog" = "تغيير نسخة Xray" | "xraySwitchVersionDialog" = "هل تريد حقًا تغيير إصدار Xray؟" | ||||||
| "xraySwitchVersionDialogDesc" = "متأكد إنك عايز تغير نسخة Xray لـ" | "xraySwitchVersionDialogDesc" = "سيؤدي هذا إلى تغيير إصدار Xray إلى #version#." | ||||||
|  | "xraySwitchVersionPopover" = "تم تحديث Xray بنجاح" | ||||||
|  | "geofileUpdateDialog" = "هل تريد حقًا تحديث ملف الجغرافيا؟" | ||||||
|  | "geofileUpdateDialogDesc" = "سيؤدي هذا إلى تحديث ملف #filename#." | ||||||
|  | "geofileUpdatePopover" = "تم تحديث ملف الجغرافيا بنجاح" | ||||||
| "dontRefresh" = "التثبيت شغال، متعملش Refresh للصفحة" | "dontRefresh" = "التثبيت شغال، متعملش Refresh للصفحة" | ||||||
| "logs" = "السجلات" | "logs" = "السجلات" | ||||||
| "config" = "الإعدادات" | "config" = "الإعدادات" | ||||||
|  |  | ||||||
|  | @ -122,8 +122,12 @@ | ||||||
| "totalData" = "Total Data" | "totalData" = "Total Data" | ||||||
| "sent" = "Sent" | "sent" = "Sent" | ||||||
| "received" = "Received" | "received" = "Received" | ||||||
| "xraySwitchVersionDialog" = "Change Xray Version" | "xraySwitchVersionDialog" = "Do you really want to change the Xray version?" | ||||||
| "xraySwitchVersionDialogDesc" = "Are you sure you want to change the Xray version to" | "xraySwitchVersionDialogDesc" = "This will change the Xray version to #version#." | ||||||
|  | "xraySwitchVersionPopover" = "Xray updated successfully" | ||||||
|  | "geofileUpdateDialog" = "Do you really want to update the geofile?" | ||||||
|  | "geofileUpdateDialogDesc" = "This will update the #filename# file." | ||||||
|  | "geofileUpdatePopover" = "Geofile updated successfully" | ||||||
| "dontRefresh" = "Installation is in progress, please do not refresh this page" | "dontRefresh" = "Installation is in progress, please do not refresh this page" | ||||||
| "logs" = "Logs" | "logs" = "Logs" | ||||||
| "config" = "Config" | "config" = "Config" | ||||||
|  |  | ||||||
|  | @ -124,8 +124,12 @@ | ||||||
| "totalData" = "Datos totales" | "totalData" = "Datos totales" | ||||||
| "sent" = "Enviado" | "sent" = "Enviado" | ||||||
| "received" = "Recibido" | "received" = "Recibido" | ||||||
| "xraySwitchVersionDialog" = "Cambiar Versión de Xray" | "xraySwitchVersionDialog" = "¿Realmente deseas cambiar la versión de Xray?" | ||||||
| "xraySwitchVersionDialogDesc" = "¿Estás seguro de que deseas cambiar la versión de Xray a" | "xraySwitchVersionDialogDesc" = "Esto cambiará la versión de Xray a #version#." | ||||||
|  | "xraySwitchVersionPopover" = "Xray se actualizó correctamente" | ||||||
|  | "geofileUpdateDialog" = "¿Realmente deseas actualizar el geofichero?" | ||||||
|  | "geofileUpdateDialogDesc" = "Esto actualizará el archivo #filename#." | ||||||
|  | "geofileUpdatePopover" = "Geofichero actualizado correctamente" | ||||||
| "dontRefresh" = "La instalación está en progreso, por favor no actualices esta página." | "dontRefresh" = "La instalación está en progreso, por favor no actualices esta página." | ||||||
| "logs" = "Registros" | "logs" = "Registros" | ||||||
| "config" = "Configuración" | "config" = "Configuración" | ||||||
|  |  | ||||||
|  | @ -124,8 +124,12 @@ | ||||||
| "totalData" = "دادههای کل" | "totalData" = "دادههای کل" | ||||||
| "sent" = "ارسال شده" | "sent" = "ارسال شده" | ||||||
| "received" = "دریافت شده" | "received" = "دریافت شده" | ||||||
| "xraySwitchVersionDialog" = "تغییر نسخه ایکسری" | "xraySwitchVersionDialog" = "آیا واقعاً میخواهید نسخه Xray را تغییر دهید؟" | ||||||
| "xraySwitchVersionDialogDesc" = "آیا از تغییر نسخه مطمئن هستید؟" | "xraySwitchVersionDialogDesc" = "این کار نسخه Xray را به #version# تغییر میدهد." | ||||||
|  | "xraySwitchVersionPopover" = "Xray با موفقیت بهروز شد" | ||||||
|  | "geofileUpdateDialog" = "آیا واقعاً میخواهید فایل جغرافیایی را بهروز کنید؟" | ||||||
|  | "geofileUpdateDialogDesc" = "این عمل فایل #filename# را بهروز میکند." | ||||||
|  | "geofileUpdatePopover" = "فایل جغرافیایی با موفقیت بهروز شد" | ||||||
| "dontRefresh" = "در حال نصب، لطفا صفحه را رفرش نکنید" | "dontRefresh" = "در حال نصب، لطفا صفحه را رفرش نکنید" | ||||||
| "logs" = "گزارشها" | "logs" = "گزارشها" | ||||||
| "config" = "پیکربندی" | "config" = "پیکربندی" | ||||||
|  |  | ||||||
|  | @ -124,8 +124,12 @@ | ||||||
| "totalData" = "Total data" | "totalData" = "Total data" | ||||||
| "sent" = "Dikirim" | "sent" = "Dikirim" | ||||||
| "received" = "Diterima" | "received" = "Diterima" | ||||||
| "xraySwitchVersionDialog" = "Ganti Versi Xray" | "xraySwitchVersionDialog" = "Apakah Anda yakin ingin mengubah versi Xray?" | ||||||
| "xraySwitchVersionDialogDesc" = "Apakah Anda yakin ingin mengubah versi Xray menjadi" | "xraySwitchVersionDialogDesc" = "Ini akan mengubah versi Xray ke #version#." | ||||||
|  | "xraySwitchVersionPopover" = "Xray berhasil diperbarui" | ||||||
|  | "geofileUpdateDialog" = "Apakah Anda yakin ingin memperbarui geofile?" | ||||||
|  | "geofileUpdateDialogDesc" = "Ini akan memperbarui file #filename#." | ||||||
|  | "geofileUpdatePopover" = "Geofile berhasil diperbarui" | ||||||
| "dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini" | "dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini" | ||||||
| "logs" = "Log" | "logs" = "Log" | ||||||
| "config" = "Konfigurasi" | "config" = "Konfigurasi" | ||||||
|  |  | ||||||
|  | @ -124,8 +124,12 @@ | ||||||
| "totalData" = "総データ量" | "totalData" = "総データ量" | ||||||
| "sent" = "送信" | "sent" = "送信" | ||||||
| "received" = "受信" | "received" = "受信" | ||||||
| "xraySwitchVersionDialog" = "Xrayバージョン切り替え" | "xraySwitchVersionDialog" = "Xrayのバージョンを本当に変更しますか?" | ||||||
| "xraySwitchVersionDialogDesc" = "Xrayのバージョンを切り替えますか?" | "xraySwitchVersionDialogDesc" = "Xrayのバージョンが#version#に変更されます。" | ||||||
|  | "xraySwitchVersionPopover" = "Xrayの更新が成功しました" | ||||||
|  | "geofileUpdateDialog" = "ジオファイルを本当に更新しますか?" | ||||||
|  | "geofileUpdateDialogDesc" = "これにより#filename#ファイルが更新されます。" | ||||||
|  | "geofileUpdatePopover" = "ジオファイルの更新が成功しました" | ||||||
| "dontRefresh" = "インストール中、このページをリロードしないでください" | "dontRefresh" = "インストール中、このページをリロードしないでください" | ||||||
| "logs" = "ログ" | "logs" = "ログ" | ||||||
| "config" = "設定" | "config" = "設定" | ||||||
|  |  | ||||||
|  | @ -124,8 +124,12 @@ | ||||||
| "totalData" = "Dados totais" | "totalData" = "Dados totais" | ||||||
| "sent" = "Enviado" | "sent" = "Enviado" | ||||||
| "received" = "Recebido" | "received" = "Recebido" | ||||||
| "xraySwitchVersionDialog" = "Alterar Versão do Xray" | "xraySwitchVersionDialog" = "Você realmente deseja alterar a versão do Xray?" | ||||||
| "xraySwitchVersionDialogDesc" = "Tem certeza de que deseja alterar a versão do Xray para" | "xraySwitchVersionDialogDesc" = "Isso mudará a versão do Xray para #version#." | ||||||
|  | "xraySwitchVersionPopover" = "Xray atualizado com sucesso" | ||||||
|  | "geofileUpdateDialog" = "Você realmente deseja atualizar o geofile?" | ||||||
|  | "geofileUpdateDialogDesc" = "Isso atualizará o arquivo #filename#." | ||||||
|  | "geofileUpdatePopover" = "Geofile atualizado com sucesso" | ||||||
| "dontRefresh" = "Instalação em andamento, por favor não atualize a página" | "dontRefresh" = "Instalação em andamento, por favor não atualize a página" | ||||||
| "logs" = "Logs" | "logs" = "Logs" | ||||||
| "config" = "Configuração" | "config" = "Configuração" | ||||||
|  |  | ||||||
|  | @ -124,8 +124,12 @@ | ||||||
| "totalData" = "Общий объем данных" | "totalData" = "Общий объем данных" | ||||||
| "sent" = "Отправлено" | "sent" = "Отправлено" | ||||||
| "received" = "Получено" | "received" = "Получено" | ||||||
| "xraySwitchVersionDialog" = "Переключить версию Xray" | "xraySwitchVersionDialog" = "Вы действительно хотите изменить версию Xray?" | ||||||
| "xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?" | "xraySwitchVersionDialogDesc" = "Это изменит версию Xray на #version#." | ||||||
|  | "xraySwitchVersionPopover" = "Xray успешно обновлён" | ||||||
|  | "geofileUpdateDialog" = "Вы действительно хотите обновить геофайл?" | ||||||
|  | "geofileUpdateDialogDesc" = "Это обновит файл #filename#." | ||||||
|  | "geofileUpdatePopover" = "Геофайл успешно обновлён" | ||||||
| "dontRefresh" = "Установка в процессе. Не обновляйте страницу" | "dontRefresh" = "Установка в процессе. Не обновляйте страницу" | ||||||
| "logs" = "Журнал" | "logs" = "Журнал" | ||||||
| "config" = "Конфигурация" | "config" = "Конфигурация" | ||||||
|  |  | ||||||
|  | @ -124,8 +124,12 @@ | ||||||
| "totalData" = "Toplam veri" | "totalData" = "Toplam veri" | ||||||
| "sent" = "Gönderilen" | "sent" = "Gönderilen" | ||||||
| "received" = "Alınan" | "received" = "Alınan" | ||||||
| "xraySwitchVersionDialog" = "Xray Sürümünü Değiştir" | "xraySwitchVersionDialog" = "Xray sürümünü gerçekten değiştirmek istiyor musunuz?" | ||||||
| "xraySwitchVersionDialogDesc" = "Xray sürümünü değiştirmek istediğinizden emin misiniz" | "xraySwitchVersionDialogDesc" = "Bu işlem Xray sürümünü #version# olarak değiştirecektir." | ||||||
|  | "xraySwitchVersionPopover" = "Xray başarıyla güncellendi" | ||||||
|  | "geofileUpdateDialog" = "Geofile'ı gerçekten güncellemek istiyor musunuz?" | ||||||
|  | "geofileUpdateDialogDesc" = "Bu işlem #filename# dosyasını güncelleyecektir." | ||||||
|  | "geofileUpdatePopover" = "Geofile başarıyla güncellendi" | ||||||
| "dontRefresh" = "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin" | "dontRefresh" = "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin" | ||||||
| "logs" = "Günlükler" | "logs" = "Günlükler" | ||||||
| "config" = "Yapılandırma" | "config" = "Yapılandırma" | ||||||
|  |  | ||||||
|  | @ -124,8 +124,12 @@ | ||||||
| "totalData" = "Загальний обсяг даних" | "totalData" = "Загальний обсяг даних" | ||||||
| "sent" = "Відправлено" | "sent" = "Відправлено" | ||||||
| "received" = "Отримано" | "received" = "Отримано" | ||||||
| "xraySwitchVersionDialog" = "Змінити версію Xray" | "xraySwitchVersionDialog" = "Ви дійсно хочете змінити версію Xray?" | ||||||
| "xraySwitchVersionDialogDesc" = "Ви впевнені, що бажаєте змінити версію Xray на" | "xraySwitchVersionDialogDesc" = "Це змінить версію Xray на #version#." | ||||||
|  | "xraySwitchVersionPopover" = "Xray успішно оновлено" | ||||||
|  | "geofileUpdateDialog" = "Ви дійсно хочете оновити геофайл?" | ||||||
|  | "geofileUpdateDialogDesc" = "Це оновить файл #filename#." | ||||||
|  | "geofileUpdatePopover" = "Геофайл успішно оновлено" | ||||||
| "dontRefresh" = "Інсталяція триває, будь ласка, не оновлюйте цю сторінку" | "dontRefresh" = "Інсталяція триває, будь ласка, не оновлюйте цю сторінку" | ||||||
| "logs" = "Журнали" | "logs" = "Журнали" | ||||||
| "config" = "Конфігурація" | "config" = "Конфігурація" | ||||||
|  |  | ||||||
|  | @ -124,8 +124,12 @@ | ||||||
| "totalData" = "Tổng dữ liệu" | "totalData" = "Tổng dữ liệu" | ||||||
| "sent" = "Đã gửi" | "sent" = "Đã gửi" | ||||||
| "received" = "Đã nhận" | "received" = "Đã nhận" | ||||||
| "xraySwitchVersionDialog" = "Chuyển đổi Phiên bản Xray" | "xraySwitchVersionDialog" = "Bạn có chắc chắn muốn thay đổi phiên bản Xray không?" | ||||||
| "xraySwitchVersionDialogDesc" = "Bạn có chắc chắn muốn chuyển đổi phiên bản Xray sang" | "xraySwitchVersionDialogDesc" = "Hành động này sẽ thay đổi phiên bản Xray thành #version#." | ||||||
|  | "xraySwitchVersionPopover" = "Xray đã được cập nhật thành công" | ||||||
|  | "geofileUpdateDialog" = "Bạn có chắc chắn muốn cập nhật geofile không?" | ||||||
|  | "geofileUpdateDialogDesc" = "Hành động này sẽ cập nhật tệp #filename#." | ||||||
|  | "geofileUpdatePopover" = "Geofile đã được cập nhật thành công" | ||||||
| "dontRefresh" = "Đang tiến hành cài đặt, vui lòng không làm mới trang này." | "dontRefresh" = "Đang tiến hành cài đặt, vui lòng không làm mới trang này." | ||||||
| "logs" = "Nhật ký" | "logs" = "Nhật ký" | ||||||
| "config" = "Cấu hình" | "config" = "Cấu hình" | ||||||
|  |  | ||||||
|  | @ -124,8 +124,12 @@ | ||||||
| "totalData" = "总数据" | "totalData" = "总数据" | ||||||
| "sent" = "已发送" | "sent" = "已发送" | ||||||
| "received" = "已接收" | "received" = "已接收" | ||||||
| "xraySwitchVersionDialog" = "切换 Xray 版本" | "xraySwitchVersionDialog" = "您确定要更改Xray版本吗?" | ||||||
| "xraySwitchVersionDialogDesc" = "是否切换 Xray 版本至" | "xraySwitchVersionDialogDesc" = "这将把Xray版本更改为#version#。" | ||||||
|  | "xraySwitchVersionPopover" = "Xray 更新成功" | ||||||
|  | "geofileUpdateDialog" = "您确定要更新地理文件吗?" | ||||||
|  | "geofileUpdateDialogDesc" = "这将更新 #filename# 文件。" | ||||||
|  | "geofileUpdatePopover" = "地理文件更新成功" | ||||||
| "dontRefresh" = "安装中,请勿刷新此页面" | "dontRefresh" = "安装中,请勿刷新此页面" | ||||||
| "logs" = "日志" | "logs" = "日志" | ||||||
| "config" = "配置" | "config" = "配置" | ||||||
|  |  | ||||||
|  | @ -124,8 +124,12 @@ | ||||||
| "totalData" = "總數據" | "totalData" = "總數據" | ||||||
| "sent" = "已發送" | "sent" = "已發送" | ||||||
| "received" = "已接收" | "received" = "已接收" | ||||||
| "xraySwitchVersionDialog" = "切換 Xray 版本" | "xraySwitchVersionDialog" = "您確定要變更Xray版本嗎?" | ||||||
| "xraySwitchVersionDialogDesc" = "是否切換 Xray 版本至" | "xraySwitchVersionDialogDesc" = "這將會把Xray版本變更為#version#。" | ||||||
|  | "xraySwitchVersionPopover" = "Xray 更新成功" | ||||||
|  | "geofileUpdateDialog" = "您確定要更新地理檔案嗎?" | ||||||
|  | "geofileUpdateDialogDesc" = "這將更新 #filename# 檔案。" | ||||||
|  | "geofileUpdatePopover" = "地理檔案更新成功" | ||||||
| "dontRefresh" = "安裝中,請勿重新整理此頁面" | "dontRefresh" = "安裝中,請勿重新整理此頁面" | ||||||
| "logs" = "日誌" | "logs" = "日誌" | ||||||
| "config" = "配置" | "config" = "配置" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue
	
	 Sanaei
						Sanaei