Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.md4
-rw-r--r--database/db.go12
-rw-r--r--main.go3
-rw-r--r--web/assets/css/custom.css2
-rw-r--r--web/assets/js/axios-init.js12
-rw-r--r--web/controller/server.go26
-rw-r--r--web/html/common/text_modal.html3
-rw-r--r--web/html/xui/index.html104
-rw-r--r--web/html/xui/settings.html2
-rw-r--r--web/service/inbound.go11
-rw-r--r--web/service/server.go106
-rw-r--r--web/translation/translate.en_US.toml7
-rw-r--r--web/translation/translate.fa_IR.toml7
-rw-r--r--web/translation/translate.zh_Hans.toml7
15 files changed, 281 insertions, 26 deletions
diff --git a/.gitignore b/.gitignore
index 7ba03558..6277cfc9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.idea
+.vscode
tmp
backup/
bin/
diff --git a/README.md b/README.md
index 86c19e57..1e9e98df 100644
--- a/README.md
+++ b/README.md
@@ -33,8 +33,8 @@ apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run
```
-or you can use x-ui menu then number '16' (Apply for an SSL Certificate)
+or you can use x-ui menu then number '16' (Apply for an SSL Certificate)
# Default settings
@@ -116,6 +116,7 @@ If you want to use routing to WARP follow steps as below:
- For more advanced configuration items, please refer to the panel
- Fix api routes (user setting will create with api)
- Support to change configs by different items provided in panel
+- Support export/import database from panel
# Tg robot use
@@ -194,7 +195,6 @@ Reference syntax:
- Tron USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
-
# Pictures
![1](./media/1.png)
diff --git a/database/db.go b/database/db.go
index ae42a6de..d5caa702 100644
--- a/database/db.go
+++ b/database/db.go
@@ -1,6 +1,8 @@
package database
import (
+ "bytes"
+ "io"
"io/fs"
"os"
"path"
@@ -104,3 +106,13 @@ func GetDB() *gorm.DB {
func IsNotFound(err error) bool {
return err == gorm.ErrRecordNotFound
}
+
+func IsSQLiteDB(file io.Reader) (bool, error) {
+ signature := []byte("SQLite format 3\x00")
+ buf := make([]byte, len(signature))
+ _, err := file.Read(buf)
+ if err != nil {
+ return false, err
+ }
+ return bytes.Equal(buf, signature), nil
+}
diff --git a/main.go b/main.go
index 54ff6bf3..3281048d 100644
--- a/main.go
+++ b/main.go
@@ -212,8 +212,7 @@ func migrateDb() {
log.Fatal(err)
}
fmt.Println("Start migrating database...")
- inboundService.MigrationRequirements()
- inboundService.RemoveOrphanedTraffics()
+ inboundService.MigrateDB()
fmt.Println("Migration done!")
}
diff --git a/web/assets/css/custom.css b/web/assets/css/custom.css
index d13e3159..671ba24f 100644
--- a/web/assets/css/custom.css
+++ b/web/assets/css/custom.css
@@ -1,5 +1,5 @@
#app {
- height: 100%;
+ height: 100vh;
}
.ant-space {
diff --git a/web/assets/js/axios-init.js b/web/assets/js/axios-init.js
index 22d14d76..bd55c3cf 100644
--- a/web/assets/js/axios-init.js
+++ b/web/assets/js/axios-init.js
@@ -3,10 +3,14 @@ axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.interceptors.request.use(
config => {
- config.data = Qs.stringify(config.data, {
- arrayFormat: 'repeat'
- });
+ if (config.data instanceof FormData) {
+ config.headers['Content-Type'] = 'multipart/form-data';
+ } else {
+ config.data = Qs.stringify(config.data, {
+ arrayFormat: 'repeat',
+ });
+ }
return config;
},
error => Promise.reject(error)
-); \ No newline at end of file
+);
diff --git a/web/controller/server.go b/web/controller/server.go
index c365ae4b..9e649e6c 100644
--- a/web/controller/server.go
+++ b/web/controller/server.go
@@ -41,6 +41,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/logs/:count", a.getLogs)
g.POST("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb)
+ g.POST("/importDB", a.importDB)
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
}
@@ -99,8 +100,8 @@ func (a *ServerController) stopXrayService(c *gin.Context) {
return
}
jsonMsg(c, "Xray stoped", err)
-
}
+
func (a *ServerController) restartXrayService(c *gin.Context) {
err := a.serverService.RestartXrayService()
if err != nil {
@@ -108,7 +109,6 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
return
}
jsonMsg(c, "Xray restarted", err)
-
}
func (a *ServerController) getLogs(c *gin.Context) {
@@ -144,6 +144,28 @@ func (a *ServerController) getDb(c *gin.Context) {
c.Writer.Write(db)
}
+func (a *ServerController) importDB(c *gin.Context) {
+ // Get the file from the request body
+ file, _, err := c.Request.FormFile("db")
+ if err != nil {
+ jsonMsg(c, "Error reading db file", err)
+ return
+ }
+ defer file.Close()
+ // Always restart Xray before return
+ defer a.serverService.RestartXrayService()
+ defer func() {
+ a.lastGetStatusTime = time.Now()
+ }()
+ // Import it
+ err = a.serverService.ImportDB(file)
+ if err != nil {
+ jsonMsg(c, "", err)
+ return
+ }
+ jsonObj(c, "Import DB", nil)
+}
+
func (a *ServerController) getNewX25519Cert(c *gin.Context) {
cert, err := a.serverService.GetNewX25519Cert()
if err != nil {
diff --git a/web/html/common/text_modal.html b/web/html/common/text_modal.html
index b2da6160..ce77d0ca 100644
--- a/web/html/common/text_modal.html
+++ b/web/html/common/text_modal.html
@@ -4,7 +4,8 @@
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
- :href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :download="txtModal.fileName">
+ :href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)"
+ :download="txtModal.fileName">
{{ i18n "download" }} [[ txtModal.fileName ]]
</a-button>
<a-input type="textarea" v-model="txtModal.content"
diff --git a/web/html/xui/index.html b/web/html/xui/index.html
index f2babcb0..c2adffde 100644
--- a/web/html/xui/index.html
+++ b/web/html/xui/index.html
@@ -111,9 +111,9 @@
<a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
{{ i18n "menu.link" }}:
- <a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">Log Reports</a-tag>
- <a-tag color="blue" style="cursor: pointer;" @click="openConfig">Config</a-tag>
- <a-tag color="blue" style="cursor: pointer;" @click="getBackup">Backup</a-tag>
+ <a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">{{ i18n "pages.index.logs" }}</a-tag>
+ <a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
+ <a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
@@ -188,6 +188,7 @@
</transition>
</a-layout-content>
</a-layout>
+
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
:closable="true" @ok="() => versionModal.visible = false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
@@ -201,6 +202,7 @@
</a-tag>
</template>
</a-modal>
+
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
@@ -227,10 +229,28 @@
{{ i18n "download" }} x-ui.log
</a-button>
</a-form-item>
- </a-form>
+ </a-form>
<a-input type="textarea" v-model="logModal.logs" disabled="true"
:autosize="{ minRows: 10, maxRows: 22}"></a-input>
</a-modal>
+
+ <a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
+ :closable="true" :class="siderDrawer.isDarkTheme ? darkClass : ''"
+ @ok="() => backupModal.hide()" @cancel="() => backupModal.hide()">
+ <p style="color: inherit; font-size: 16px; padding: 4px 2px;">
+ <a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon>
+ [[ backupModal.description ]]
+ </p>
+ <a-space direction="horizontal" align="center" style="margin-bottom: 10px;">
+ <a-button type="primary" @click="exportDatabase()">
+ [[ backupModal.exportText ]]
+ </a-button>
+ <a-button type="primary" @click="importDatabase()">
+ [[ backupModal.importText ]]
+ </a-button>
+ </a-space>
+ </a-modal>
+
</a-layout>
{{template "js" .}}
{{template "textModal"}}
@@ -339,6 +359,29 @@
},
};
+ const backupModal = {
+ visible: false,
+ title: '',
+ description: '',
+ exportText: '',
+ importText: '',
+ show({
+ title = '{{ i18n "pages.index.backupTitle" }}',
+ description = '{{ i18n "pages.index.backupDescription" }}',
+ exportText = '{{ i18n "pages.index.exportDatabase" }}',
+ importText = '{{ i18n "pages.index.importDatabase" }}',
+ }) {
+ this.title = title;
+ this.description = description;
+ this.exportText = exportText;
+ this.importText = importText;
+ this.visible = true;
+ },
+ hide() {
+ this.visible = false;
+ },
+ };
+
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
@@ -347,6 +390,7 @@
status: new Status(),
versionModal,
logModal,
+ backupModal,
spinning: false,
loadingTip: '{{ i18n "loading"}}',
},
@@ -388,7 +432,6 @@
},
});
},
- //here add stop xray function
async stopXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/stopXrayService');
@@ -397,7 +440,6 @@
return;
}
},
- //here add restart xray function
async restartXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/restartXrayService');
@@ -413,20 +455,60 @@
if (!msg.success) {
return;
}
- logModal.show(msg.obj,rows);
+ logModal.show(msg.obj, rows);
},
- async openConfig(){
+ async openConfig() {
this.loading(true);
const msg = await HttpUtil.post('server/getConfigJson');
this.loading(false);
if (!msg.success) {
return;
}
- txtModal.show('config.json',JSON.stringify(msg.obj, null, 2),'config.json');
+ txtModal.show('config.json', JSON.stringify(msg.obj, null, 2), 'config.json');
},
- getBackup(){
+ openBackup() {
+ backupModal.show({
+ title: '{{ i18n "pages.index.backupTitle" }}',
+ description: '{{ i18n "pages.index.backupDescription" }}',
+ exportText: '{{ i18n "pages.index.exportDatabase" }}',
+ importText: '{{ i18n "pages.index.importDatabase" }}',
+ });
+ },
+ exportDatabase() {
window.location = basePath + 'server/getDb';
- }
+ },
+ importDatabase() {
+ const fileInput = document.createElement('input');
+ fileInput.type = 'file';
+ fileInput.accept = '.db';
+ fileInput.addEventListener('change', async (event) => {
+ const dbFile = event.target.files[0];
+ if (dbFile) {
+ const formData = new FormData();
+ formData.append('db', dbFile);
+ backupModal.hide();
+ this.loading(true);
+ const uploadMsg = await HttpUtil.post('server/importDB', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ }
+ });
+ this.loading(false);
+ if (!uploadMsg.success) {
+ return;
+ }
+ this.loading(true);
+ const restartMsg = await HttpUtil.post("/xui/setting/restartPanel");
+ this.loading(false);
+ if (restartMsg.success) {
+ this.loading(true);
+ await PromiseUtil.sleep(5000);
+ location.reload();
+ }
+ }
+ });
+ fileInput.click();
+ },
},
async mounted() {
while (true) {
diff --git a/web/html/xui/settings.html b/web/html/xui/settings.html
index 5b5a91dc..e1237971 100644
--- a/web/html/xui/settings.html
+++ b/web/html/xui/settings.html
@@ -74,7 +74,7 @@
</a-list>
</a-tab-pane>
- <a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings"}}' style="padding-top: 5px;">
+ <a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings"}}' style="padding: 20px;">
<a-tabs default-active-key="sec-1" :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-tab-pane key="sec-1" tab='{{ i18n "pages.settings.security.admin"}}'>
<a-form :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65); padding: 20px;': 'background: white; padding: 20px;'">
diff --git a/web/service/inbound.go b/web/service/inbound.go
index dd633c4b..b14f7b4f 100644
--- a/web/service/inbound.go
+++ b/web/service/inbound.go
@@ -595,6 +595,7 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) {
count := result.RowsAffected
return count, err
}
+
func (s *InboundService) DisableInvalidClients() (int64, error) {
db := database.GetDB()
now := time.Now().Unix() * 1000
@@ -605,7 +606,8 @@ func (s *InboundService) DisableInvalidClients() (int64, error) {
count := result.RowsAffected
return count, err
}
-func (s *InboundService) RemoveOrphanedTraffics() {
+
+func (s *InboundService) MigrationRemoveOrphanedTraffics() {
db := database.GetDB()
db.Exec(`
DELETE FROM client_traffics
@@ -616,6 +618,7 @@ func (s *InboundService) RemoveOrphanedTraffics() {
)
`)
}
+
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
db := database.GetDB()
@@ -634,6 +637,7 @@ func (s *InboundService) AddClientStat(inboundId int, client *model.Client) erro
}
return nil
}
+
func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
db := database.GetDB()
@@ -1166,3 +1170,8 @@ func (s *InboundService) MigrationRequirements() {
// Remove orphaned traffics
db.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
}
+
+func (s *InboundService) MigrateDB() {
+ s.MigrationRequirements()
+ s.MigrationRemoveOrphanedTraffics()
+}
diff --git a/web/service/server.go b/web/service/server.go
index 1108926b..d8a2239b 100644
--- a/web/service/server.go
+++ b/web/service/server.go
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"io/fs"
+ "mime/multipart"
"net/http"
"os"
"os/exec"
@@ -14,7 +15,9 @@ import (
"strings"
"time"
"x-ui/config"
+ "x-ui/database"
"x-ui/logger"
+ "x-ui/util/common"
"x-ui/util/sys"
"x-ui/xray"
@@ -73,7 +76,8 @@ type Release struct {
}
type ServerService struct {
- xrayService XrayService
+ xrayService XrayService
+ inboundService InboundService
}
func (s *ServerService) GetStatus(lastStatus *Status) *Status {
@@ -395,6 +399,106 @@ func (s *ServerService) GetDb() ([]byte, error) {
return fileContents, nil
}
+func (s *ServerService) ImportDB(file multipart.File) error {
+ // Check if the file is a SQLite database
+ isValidDb, err := database.IsSQLiteDB(file)
+ if err != nil {
+ return common.NewErrorf("Error checking db file format: %v", err)
+ }
+ if !isValidDb {
+ return common.NewError("Invalid db file format")
+ }
+
+ // Reset the file reader to the beginning
+ _, err = file.Seek(0, 0)
+ if err != nil {
+ return common.NewErrorf("Error resetting file reader: %v", err)
+ }
+
+ // Save the file as temporary file
+ tempPath := fmt.Sprintf("%s.temp", config.GetDBPath())
+ // Remove the existing fallback file (if any) before creating one
+ _, err = os.Stat(tempPath)
+ if err == nil {
+ errRemove := os.Remove(tempPath)
+ if errRemove != nil {
+ return common.NewErrorf("Error removing existing temporary db file: %v", errRemove)
+ }
+ }
+ // Create the temporary file
+ tempFile, err := os.Create(tempPath)
+ if err != nil {
+ return common.NewErrorf("Error creating temporary db file: %v", err)
+ }
+ defer tempFile.Close()
+
+ // Remove temp file before returning
+ defer os.Remove(tempPath)
+
+ // Save uploaded file to temporary file
+ _, err = io.Copy(tempFile, file)
+ if err != nil {
+ return common.NewErrorf("Error saving db: %v", err)
+ }
+
+ // Check if we can init db or not
+ err = database.InitDB(tempPath)
+ if err != nil {
+ return common.NewErrorf("Error checking db: %v", err)
+ }
+
+ // Stop Xray
+ s.StopXrayService()
+
+ // Backup the current database for fallback
+ fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
+ // Remove the existing fallback file (if any)
+ _, err = os.Stat(fallbackPath)
+ if err == nil {
+ errRemove := os.Remove(fallbackPath)
+ if errRemove != nil {
+ return common.NewErrorf("Error removing existing fallback db file: %v", errRemove)
+ }
+ }
+ // Move the current database to the fallback location
+ err = os.Rename(config.GetDBPath(), fallbackPath)
+ if err != nil {
+ return common.NewErrorf("Error backing up temporary db file: %v", err)
+ }
+
+ // Remove the temporary file before returning
+ defer os.Remove(fallbackPath)
+
+ // Move temp to DB path
+ err = os.Rename(tempPath, config.GetDBPath())
+ if err != nil {
+ errRename := os.Rename(fallbackPath, config.GetDBPath())
+ if errRename != nil {
+ return common.NewErrorf("Error moving db file and restoring fallback: %v", errRename)
+ }
+ return common.NewErrorf("Error moving db file: %v", err)
+ }
+
+ // Migrate DB
+ err = database.InitDB(config.GetDBPath())
+ if err != nil {
+ errRename := os.Rename(fallbackPath, config.GetDBPath())
+ if errRename != nil {
+ return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
+ }
+ return common.NewErrorf("Error migrating db: %v", err)
+ }
+ s.inboundService.MigrateDB()
+
+ // Start Xray
+ err = s.RestartXrayService()
+ if err != nil {
+ return common.NewErrorf("Imported DB but Failed to start Xray: %v", err)
+ }
+
+ return nil
+}
+
func (s *ServerService) GetNewX25519Cert() (interface{}, error) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "x25519")
diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml
index bc5bb942..42d67daf 100644
--- a/web/translation/translate.en_US.toml
+++ b/web/translation/translate.en_US.toml
@@ -90,6 +90,13 @@
"xraySwitchVersionDialog" = "Switch Xray Version"
"xraySwitchVersionDialogDesc" = "Are you sure you want to switch the Xray version to"
"dontRefresh" = "Installation is in progress, please do not refresh this page."
+"logs" = "Logs"
+"config" = "Config"
+"backup" = "Backup"
+"backupTitle" = "Backup Database"
+"backupDescription" = "Remember to backup before importing a new database."
+"exportDatabase" = "Download Database"
+"importDatabase" = "Upload Database"
[pages.inbounds]
"title" = "Inbounds"
diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml
index f2e7e93f..b0bb6a8c 100644
--- a/web/translation/translate.fa_IR.toml
+++ b/web/translation/translate.fa_IR.toml
@@ -90,6 +90,13 @@
"xraySwitchVersionDialog" = "تغییر ورژن"
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
"dontRefresh" = "در حال نصب ، لطفا رفرش نکنید "
+"logs" = "گزارش ها"
+"config" = "تنظیمات"
+"backup" = "پشتیبان گیری"
+"backupTitle" = "پشتیبان گیری دیتابیس"
+"backupDescription" = "به یاد داشته باشید که قبل از وارد کردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید."
+"exportDatabase" = "دانلود دیتابیس"
+"importDatabase" = "آپلود دیتابیس"
[pages.inbounds]
"title" = "کاربران"
diff --git a/web/translation/translate.zh_Hans.toml b/web/translation/translate.zh_Hans.toml
index c0ba24c6..6e78be09 100644
--- a/web/translation/translate.zh_Hans.toml
+++ b/web/translation/translate.zh_Hans.toml
@@ -90,6 +90,13 @@
"xraySwitchVersionDialog" = "切换 xray 版本"
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
"dontRefresh" = "安装中,请不要刷新此页面"
+"logs" = "日志"
+"config" = "配置"
+"backup" = "备份"
+"backupTitle" = "备份数据库"
+"backupDescription" = "请记住在导入新数据库之前进行备份。"
+"exportDatabase" = "下载数据库"
+"importDatabase" = "上传数据库"
[pages.inbounds]
"title" = "入站列表"