From 258b08fff35fc6c445b77149996f6dd9f7b4a789 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 8 Mar 2026 11:53:34 +0100 Subject: [PATCH 01/15] Update fail2ban filter regex in x-ui.sh --- x-ui.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-ui.sh b/x-ui.sh index 0a2b818b..2e555b25 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -2012,7 +2012,7 @@ EOF cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf [Definition] datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S -failregex = \[LIMIT_IP\]\s*Email\s*=\s*.+\s*\|\|\s*SRC\s*=\s* +failregex = \[LIMIT_IP\]\s*Email\s*=\s*.+\s*\|\|\s*Disconnecting OLD IP\s*=\s*\s*\|\|\s*Timestamp\s*=\s*\d+ ignoreregex = EOF From 7b03346cfcb50b49b124cc8333aeeadeb76a8455 Mon Sep 17 00:00:00 2001 From: Sanaei Date: Tue, 17 Mar 2026 21:03:32 +0100 Subject: [PATCH 02/15] Set package ecosystem to GitHub Actions in dependabot.yml --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..0d08e261 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" From ee84d585f9e52a8fa794c16eb765f1b1dc1411b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:04:41 +0100 Subject: [PATCH 03/15] Bump docker/login-action from 3 to 4 (#3939) Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 39ddf2e0..921a8e5c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -40,13 +40,13 @@ jobs: install: true - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} From 5bbb48a8fd1ef3e15af4d95cb2c8aa48188e337e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:04:54 +0100 Subject: [PATCH 04/15] Bump docker/setup-qemu-action from 3 to 4 (#3936) Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 921a8e5c..53d81cdc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -32,7 +32,7 @@ jobs: type=semver,pattern={{version}} - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 From a3e1bd59df6725e815ce190575a2050f46c74a8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:05:07 +0100 Subject: [PATCH 05/15] Bump docker/build-push-action from 6 to 7 (#3937) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6...v7) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 53d81cdc..df62ebd4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -53,7 +53,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . push: true From ff72090e1a0514a2a270d34df3d15b300ebc28b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:05:28 +0100 Subject: [PATCH 06/15] Bump docker/setup-buildx-action from 3 to 4 (#3938) Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index df62ebd4..343a9339 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -35,7 +35,7 @@ jobs: uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 with: install: true From e4add73c9e9f22cca7560907e2fab702e08d96ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:05:43 +0100 Subject: [PATCH 07/15] Bump actions/checkout from 5 to 6 (#3940) Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- .github/workflows/release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 343a9339..eeaaebcb 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8b8d6902..18cf2667 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Go uses: actions/setup-go@v6 @@ -165,7 +165,7 @@ jobs: runs-on: windows-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Go uses: actions/setup-go@v6 From 6767f76ccf2b0e6c5463731686d094d2fa659baf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:09:56 +0100 Subject: [PATCH 08/15] Bump actions/upload-artifact from 4 to 7 (#3941) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 18cf2667..9e94fb74 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -133,7 +133,7 @@ jobs: run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui - name: Upload files to Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: x-ui-linux-${{ matrix.platform }} path: ./x-ui-linux-${{ matrix.platform }}.tar.gz @@ -230,7 +230,7 @@ jobs: Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip" - name: Upload files to Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: x-ui-windows-amd64 path: ./x-ui-windows-amd64.zip From a6d0100381c5f3150d5d5b53b2f450b5b2dc308e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:10:09 +0100 Subject: [PATCH 09/15] Bump docker/metadata-action from 5 to 6 (#3942) Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/v5...v6) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index eeaaebcb..0dd4847d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -21,7 +21,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: | hsanaeii/3x-ui From 60abeaad66a14e29250f0e0b3f2ee9b45c53cbc2 Mon Sep 17 00:00:00 2001 From: HamidReza Sadeghzadeh Date: Tue, 17 Mar 2026 23:48:10 +0330 Subject: [PATCH 10/15] fix: Ban new IPs with fail2ban instead of disconnected the client. (#3919) * fix: Ban new IPs with fail2ban instead of disconnected the client. * fix: Remove unused strconv import * fix: Revert log fail2ban format --- web/job/check_client_ip_job.go | 79 ++++------------------------------ 1 file changed, 9 insertions(+), 70 deletions(-) diff --git a/web/job/check_client_ip_job.go b/web/job/check_client_ip_job.go index d3c1a1d1..cbc352dc 100644 --- a/web/job/check_client_ip_job.go +++ b/web/job/check_client_ip_job.go @@ -10,7 +10,6 @@ import ( "regexp" "runtime" "sort" - "strconv" "time" "github.com/mhsanaei/3x-ui/v2/database" @@ -319,13 +318,14 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun } } - // Convert back to slice and sort by timestamp (newest first) + // Convert back to slice and sort by timestamp (oldest first) + // This ensures we always protect the original/current connections and ban new excess ones. allIps := make([]IPWithTimestamp, 0, len(ipMap)) for ip, timestamp := range ipMap { allIps = append(allIps, IPWithTimestamp{IP: ip, Timestamp: timestamp}) } sort.Slice(allIps, func(i, j int) bool { - return allIps[i].Timestamp > allIps[j].Timestamp // Descending order (newest first) + return allIps[i].Timestamp < allIps[j].Timestamp // Ascending order (oldest first) }) shouldCleanLog := false @@ -345,23 +345,17 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun if len(allIps) > limitIp { shouldCleanLog = true - // Keep only the newest IPs (up to limitIp) + // Keep the oldest IPs (currently active connections) and ban the new excess ones. keptIps := allIps[:limitIp] - disconnectedIps := allIps[limitIp:] + bannedIps := allIps[limitIp:] - // Log the disconnected IPs (old ones) - for _, ipTime := range disconnectedIps { + // Log banned IPs in the format fail2ban filters expect: [LIMIT_IP] Email = X || Disconnecting OLD IP = Y || Timestamp = Z + for _, ipTime := range bannedIps { j.disAllowedIps = append(j.disAllowedIps, ipTime.IP) log.Printf("[LIMIT_IP] Email = %s || Disconnecting OLD IP = %s || Timestamp = %d", clientEmail, ipTime.IP, ipTime.Timestamp) } - // Actually disconnect old IPs by temporarily removing and re-adding user - // This forces Xray to drop existing connections from old IPs - if len(disconnectedIps) > 0 { - j.disconnectClientTemporarily(inbound, clientEmail, clients) - } - - // Update database with only the newest IPs + // Update database with only the currently active (kept) IPs jsonIps, _ := json.Marshal(keptIps) inboundClientIps.Ips = string(jsonIps) } else { @@ -378,67 +372,12 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun } if len(j.disAllowedIps) > 0 { - logger.Infof("[LIMIT_IP] Client %s: Kept %d newest IPs, disconnected %d old IPs", clientEmail, limitIp, len(j.disAllowedIps)) + logger.Infof("[LIMIT_IP] Client %s: Kept %d current IPs, queued %d new IPs for fail2ban", clientEmail, limitIp, len(j.disAllowedIps)) } return shouldCleanLog } -// disconnectClientTemporarily removes and re-adds a client to force disconnect old connections -func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, clientEmail string, clients []model.Client) { - var xrayAPI xray.XrayAPI - - // Get panel settings for API port - db := database.GetDB() - var apiPort int - var apiPortSetting model.Setting - if err := db.Where("key = ?", "xrayApiPort").First(&apiPortSetting).Error; err == nil { - apiPort, _ = strconv.Atoi(apiPortSetting.Value) - } - - if apiPort == 0 { - apiPort = 10085 // Default API port - } - - err := xrayAPI.Init(apiPort) - if err != nil { - logger.Warningf("[LIMIT_IP] Failed to init Xray API for disconnection: %v", err) - return - } - defer xrayAPI.Close() - - // Find the client config - var clientConfig map[string]any - for _, client := range clients { - if client.Email == clientEmail { - // Convert client to map for API - clientBytes, _ := json.Marshal(client) - json.Unmarshal(clientBytes, &clientConfig) - break - } - } - - if clientConfig == nil { - return - } - - // Remove user to disconnect all connections - err = xrayAPI.RemoveUser(inbound.Tag, clientEmail) - if err != nil { - logger.Warningf("[LIMIT_IP] Failed to remove user %s: %v", clientEmail, err) - return - } - - // Wait a moment for disconnection to take effect - time.Sleep(100 * time.Millisecond) - - // Re-add user to allow new connections - err = xrayAPI.AddUser(string(inbound.Protocol), inbound.Tag, clientConfig) - if err != nil { - logger.Warningf("[LIMIT_IP] Failed to re-add user %s: %v", clientEmail, err) - } -} - func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) { db := database.GetDB() inbound := &model.Inbound{} From 7f7ae0c547dccea93607d21a6283c91165ce52a5 Mon Sep 17 00:00:00 2001 From: Alimpo <42714856+Alimpo@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:50:24 +0330 Subject: [PATCH 11/15] fix: stop overwriting client_traffics.enable with JSON enable in GetClientTrafficByEmail (#3931) When a client hit traffic/expiry limit, disableInvalidClients sets client_traffics.enable=false and removes the user from Xray. GetClientTrafficByEmail was overwriting that with settings.clients[].enable (admin config), so ResetClientTraffic never saw the client as disabled and did not re-add the user. Clients could not connect until manually disabled/re-enabled. Now the DB runtime enable flag is preserved; reset correctly re-adds the user to Xray. --- web/service/inbound.go | 1 - 1 file changed, 1 deletion(-) diff --git a/web/service/inbound.go b/web/service/inbound.go index 101c79d9..8a3a4ae2 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -2032,7 +2032,6 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl return nil, err } if t != nil && client != nil { - t.Enable = client.Enable t.UUID = client.ID t.SubId = client.SubID return t, nil From a08f1c6c13521cff13e7786bbefc2d83026c9e61 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Tue, 17 Mar 2026 23:24:09 +0300 Subject: [PATCH 12/15] Update translate.ru_RU.toml (#3889) Change to plural (geofiles, not geofile) --- web/translation/translate.ru_RU.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 8a403a1c..0425db96 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -149,7 +149,7 @@ "geofileUpdateDialogDesc" = "Это обновит файл #filename#." "geofilesUpdateDialogDesc" = "Это обновит все геофайлы." "geofilesUpdateAll" = "Обновить все" -"geofileUpdatePopover" = "Геофайл успешно обновлён" +"geofileUpdatePopover" = "Геофайлы успешно обновлены" "dontRefresh" = "Установка в процессе. Не обновляйте страницу" "logs" = "Журнал" "config" = "Конфигурация" From 554981d9d347c88a5c5973aa1fd711676c0cd7e9 Mon Sep 17 00:00:00 2001 From: Abdalrahman Date: Tue, 17 Mar 2026 23:09:49 +0200 Subject: [PATCH 13/15] feat(tgbot): send connection links and qrs on client creation (closes #3320)\n\n- Refactored inline keyboards into getCommonClientButtons to respect DRY\n- Extended SubmitAddClient callback handlers to dispatch individual links and QR codes to the bot chat on success. (#3888) --- web/service/tgbot.go | 102 ++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 64 deletions(-) diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 6a49f1d3..1649f2ed 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -1926,6 +1926,8 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool } else { t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove()) + t.sendClientIndividualLinks(chatId, client_Email) + t.sendClientQRLinks(chatId, client_Email) } case "add_client_submit_enable": client_Enable = true @@ -1936,6 +1938,8 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool } else { t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove()) + t.sendClientIndividualLinks(chatId, client_Email) + t.sendClientQRLinks(chatId, client_Email) } case "reset_all_traffics_cancel": t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) @@ -3302,6 +3306,27 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) { } } +// getCommonClientButtons returns the shared inline keyboard rows for client configuration +func (t *Tgbot) getCommonClientButtons() [][]telego.InlineKeyboardButton { + return [][]telego.InlineKeyboardButton{ + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData("add_client_ch_default_ip_limit"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"), + ), + } +} + // addClient handles the process of adding a new client to an inbound. func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) { inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) @@ -3312,91 +3337,40 @@ func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) { protocol := inbound.Protocol + var protocolRows [][]telego.InlineKeyboardButton switch protocol { case model.VMESS, model.VLESS: - inlineKeyboard := tu.InlineKeyboard( + protocolRows = [][]telego.InlineKeyboardButton{ tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_id")).WithCallbackData("add_client_ch_default_id"), ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"), - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"), - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData("add_client_ch_default_ip_limit"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"), - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"), - ), - ) - if len(messageID) > 0 { - t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard) - } else { - t.SendMsgToTgbot(chatId, msg, inlineKeyboard) } case model.Trojan: - inlineKeyboard := tu.InlineKeyboard( + protocolRows = [][]telego.InlineKeyboardButton{ tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_tr"), ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"), - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"), - tu.InlineKeyboardButton("ip limit").WithCallbackData("add_client_ch_default_ip_limit"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"), - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"), - ), - ) - if len(messageID) > 0 { - t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard) - } else { - t.SendMsgToTgbot(chatId, msg, inlineKeyboard) } case model.Shadowsocks: - inlineKeyboard := tu.InlineKeyboard( + protocolRows = [][]telego.InlineKeyboardButton{ tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_sh"), ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"), - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"), - tu.InlineKeyboardButton("ip limit").WithCallbackData("add_client_ch_default_ip_limit"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"), - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"), - ), - ) - - if len(messageID) > 0 { - t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard) - } else { - t.SendMsgToTgbot(chatId, msg, inlineKeyboard) } } + commonRows := t.getCommonClientButtons() + inlineKeyboard := tu.InlineKeyboard(append(protocolRows, commonRows...)...) + + if len(messageID) > 0 { + t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard) + } else { + t.SendMsgToTgbot(chatId, msg, inlineKeyboard) + } + } // searchInbound searches for inbounds by remark and sends the results. From f0f98c712225269e1a3db1796eff88ebecaf2a2a Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 17 Mar 2026 22:30:05 +0100 Subject: [PATCH 14/15] Add Go code analyzer workflow --- .github/workflows/release.yml | 56 ++++++++++++++++++++++++++++------- web/controller/index.go | 10 +++---- web/service/user.go | 2 +- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e94fb74..ed9417c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,11 +2,9 @@ name: Release 3X-UI on: workflow_dispatch: - release: - types: [published] push: branches: - - main + - '**' tags: - "v*.*.*" paths: @@ -20,9 +18,48 @@ on: - 'x-ui.service.debian' - 'x-ui.service.arch' - 'x-ui.service.rhel' + pull_request: jobs: + analyze: + name: Analyze Go code + permissions: + contents: read + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + + - name: Check formatting + run: | + unformatted=$(gofmt -l .) + if [ -n "$unformatted" ]; then + echo "These files are not gofmt-formatted:" + echo "$unformatted" + exit 1 + fi + + - name: Run go vet + run: go vet ./... + + - name: Run staticcheck + uses: dominikh/staticcheck-action@v1 + with: + version: "latest" + install-go: false + + - name: Run tests + run: go test -race -shuffle=on ./... + build: + needs: analyze permissions: contents: write strategy: @@ -140,12 +177,10 @@ jobs: - name: Upload files to GH release uses: svenstaro/upload-release-action@v2 - if: | - (github.event_name == 'release' && github.event.action == 'published') || - (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') with: repo_token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ github.ref }} + tag: ${{ github.ref_name }} file: x-ui-linux-${{ matrix.platform }}.tar.gz asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz overwrite: true @@ -156,6 +191,7 @@ jobs: # ================================= build-windows: name: Build for Windows + needs: analyze permissions: contents: write strategy: @@ -237,12 +273,10 @@ jobs: - name: Upload files to GH release uses: svenstaro/upload-release-action@v2 - if: | - (github.event_name == 'release' && github.event.action == 'published') || - (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') with: repo_token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ github.ref }} + tag: ${{ github.ref_name }} file: x-ui-windows-amd64.zip asset_name: x-ui-windows-amd64.zip overwrite: true diff --git a/web/controller/index.go b/web/controller/index.go index 605f874f..dd58e5e5 100644 --- a/web/controller/index.go +++ b/web/controller/index.go @@ -1,10 +1,10 @@ package controller import ( + "fmt" "net/http" "text/template" "time" - "fmt" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/web/service" @@ -79,12 +79,12 @@ func (a *IndexController) login(c *gin.Context) { if user == nil { logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c)) - - notifyPass := safePass - + + notifyPass := safePass + if checkErr != nil && checkErr.Error() == "invalid 2fa code" { translatedError := a.tgbot.I18nBot("tgbot.messages.2faFailed") - notifyPass = fmt.Sprintf("*** (%s)", translatedError) + notifyPass = fmt.Sprintf("*** (%s)", translatedError) } a.tgbot.UserLoginNotify(safeUser, notifyPass, getRemoteIp(c), timeStr, 0) diff --git a/web/service/user.go b/web/service/user.go index 0a2a3f3e..6fcf17e7 100644 --- a/web/service/user.go +++ b/web/service/user.go @@ -95,7 +95,7 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode } if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode { - return nil, errors.New("invalid 2fa code") + return nil, errors.New("invalid 2fa code") } } From 38d87230d326dd9ffd9ef9bc29b2e1c70a5b3f88 Mon Sep 17 00:00:00 2001 From: kazan417 Date: Thu, 19 Mar 2026 01:45:45 +0700 Subject: [PATCH 15/15] Update x-ui.sh (#3947) looks like now cert management is option 19 --- x-ui.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-ui.sh b/x-ui.sh index 2e555b25..e9e2d831 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -317,12 +317,12 @@ check_config() { start >/dev/null 2>&1 else LOGE "IP certificate setup failed." - echo -e "${yellow}You can try again via option 18 (SSL Certificate Management).${plain}" + echo -e "${yellow}You can try again via option 19 (SSL Certificate Management).${plain}" start >/dev/null 2>&1 fi else echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}" - echo -e "${yellow}For security, please configure SSL certificate using option 18 (SSL Certificate Management)${plain}" + echo -e "${yellow}For security, please configure SSL certificate using option 19 (SSL Certificate Management)${plain}" fi fi }