mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-03-19 09:05:49 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38d87230d3 | ||
|
|
f0f98c7122 | ||
|
|
554981d9d3 | ||
|
|
a08f1c6c13 | ||
|
|
7f7ae0c547 | ||
|
|
60abeaad66 | ||
|
|
a6d0100381 | ||
|
|
6767f76ccf | ||
|
|
e4add73c9e | ||
|
|
ff72090e1a | ||
|
|
a3e1bd59df | ||
|
|
5bbb48a8fd | ||
|
|
ee84d585f9 | ||
|
|
7b03346cfc | ||
|
|
258b08fff3 | ||
|
|
a2097ad062 |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -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"
|
||||||
14
.github/workflows/docker.yml
vendored
14
.github/workflows/docker.yml
vendored
@@ -15,13 +15,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
hsanaeii/3x-ui
|
hsanaeii/3x-ui
|
||||||
@@ -32,28 +32,28 @@ jobs:
|
|||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
with:
|
with:
|
||||||
install: true
|
install: true
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
|||||||
64
.github/workflows/release.yml
vendored
64
.github/workflows/release.yml
vendored
@@ -2,11 +2,9 @@ name: Release 3X-UI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- '**'
|
||||||
tags:
|
tags:
|
||||||
- "v*.*.*"
|
- "v*.*.*"
|
||||||
paths:
|
paths:
|
||||||
@@ -20,9 +18,48 @@ on:
|
|||||||
- 'x-ui.service.debian'
|
- 'x-ui.service.debian'
|
||||||
- 'x-ui.service.arch'
|
- 'x-ui.service.arch'
|
||||||
- 'x-ui.service.rhel'
|
- 'x-ui.service.rhel'
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
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:
|
build:
|
||||||
|
needs: analyze
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
strategy:
|
strategy:
|
||||||
@@ -38,7 +75,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
@@ -133,19 +170,17 @@ jobs:
|
|||||||
run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
|
run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
|
||||||
|
|
||||||
- name: Upload files to Artifacts
|
- name: Upload files to Artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: x-ui-linux-${{ matrix.platform }}
|
name: x-ui-linux-${{ matrix.platform }}
|
||||||
path: ./x-ui-linux-${{ matrix.platform }}.tar.gz
|
path: ./x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||||
|
|
||||||
- name: Upload files to GH release
|
- name: Upload files to GH release
|
||||||
uses: svenstaro/upload-release-action@v2
|
uses: svenstaro/upload-release-action@v2
|
||||||
if: |
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
(github.event_name == 'release' && github.event.action == 'published') ||
|
|
||||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
|
||||||
with:
|
with:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tag: ${{ github.ref }}
|
tag: ${{ github.ref_name }}
|
||||||
file: x-ui-linux-${{ matrix.platform }}.tar.gz
|
file: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||||
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
|
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||||
overwrite: true
|
overwrite: true
|
||||||
@@ -156,6 +191,7 @@ jobs:
|
|||||||
# =================================
|
# =================================
|
||||||
build-windows:
|
build-windows:
|
||||||
name: Build for Windows
|
name: Build for Windows
|
||||||
|
needs: analyze
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
strategy:
|
strategy:
|
||||||
@@ -165,7 +201,7 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
@@ -230,19 +266,17 @@ jobs:
|
|||||||
Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
|
Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
|
||||||
|
|
||||||
- name: Upload files to Artifacts
|
- name: Upload files to Artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: x-ui-windows-amd64
|
name: x-ui-windows-amd64
|
||||||
path: ./x-ui-windows-amd64.zip
|
path: ./x-ui-windows-amd64.zip
|
||||||
|
|
||||||
- name: Upload files to GH release
|
- name: Upload files to GH release
|
||||||
uses: svenstaro/upload-release-action@v2
|
uses: svenstaro/upload-release-action@v2
|
||||||
if: |
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
(github.event_name == 'release' && github.event.action == 'published') ||
|
|
||||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
|
||||||
with:
|
with:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tag: ${{ github.ref }}
|
tag: ${{ github.ref_name }}
|
||||||
file: x-ui-windows-amd64.zip
|
file: x-ui-windows-amd64.zip
|
||||||
asset_name: x-ui-windows-amd64.zip
|
asset_name: x-ui-windows-amd64.zip
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
@@ -71,14 +72,22 @@ func (a *IndexController) login(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
|
user, checkErr := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
|
||||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||||
safeUser := template.HTMLEscapeString(form.Username)
|
safeUser := template.HTMLEscapeString(form.Username)
|
||||||
safePass := template.HTMLEscapeString(form.Password)
|
safePass := template.HTMLEscapeString(form.Password)
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c))
|
logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c))
|
||||||
a.tgbot.UserLoginNotify(safeUser, safePass, getRemoteIp(c), timeStr, 0)
|
|
||||||
|
notifyPass := safePass
|
||||||
|
|
||||||
|
if checkErr != nil && checkErr.Error() == "invalid 2fa code" {
|
||||||
|
translatedError := a.tgbot.I18nBot("tgbot.messages.2faFailed")
|
||||||
|
notifyPass = fmt.Sprintf("*** (%s)", translatedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tgbot.UserLoginNotify(safeUser, notifyPass, getRemoteIp(c), timeStr, 0)
|
||||||
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/database"
|
"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))
|
allIps := make([]IPWithTimestamp, 0, len(ipMap))
|
||||||
for ip, timestamp := range ipMap {
|
for ip, timestamp := range ipMap {
|
||||||
allIps = append(allIps, IPWithTimestamp{IP: ip, Timestamp: timestamp})
|
allIps = append(allIps, IPWithTimestamp{IP: ip, Timestamp: timestamp})
|
||||||
}
|
}
|
||||||
sort.Slice(allIps, func(i, j int) bool {
|
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
|
shouldCleanLog := false
|
||||||
@@ -345,23 +345,17 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
|
|||||||
if len(allIps) > limitIp {
|
if len(allIps) > limitIp {
|
||||||
shouldCleanLog = true
|
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]
|
keptIps := allIps[:limitIp]
|
||||||
disconnectedIps := allIps[limitIp:]
|
bannedIps := allIps[limitIp:]
|
||||||
|
|
||||||
// Log the disconnected IPs (old ones)
|
// Log banned IPs in the format fail2ban filters expect: [LIMIT_IP] Email = X || Disconnecting OLD IP = Y || Timestamp = Z
|
||||||
for _, ipTime := range disconnectedIps {
|
for _, ipTime := range bannedIps {
|
||||||
j.disAllowedIps = append(j.disAllowedIps, ipTime.IP)
|
j.disAllowedIps = append(j.disAllowedIps, ipTime.IP)
|
||||||
log.Printf("[LIMIT_IP] Email = %s || Disconnecting OLD IP = %s || Timestamp = %d", clientEmail, ipTime.IP, ipTime.Timestamp)
|
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
|
// Update database with only the currently active (kept) IPs
|
||||||
// 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
|
|
||||||
jsonIps, _ := json.Marshal(keptIps)
|
jsonIps, _ := json.Marshal(keptIps)
|
||||||
inboundClientIps.Ips = string(jsonIps)
|
inboundClientIps.Ips = string(jsonIps)
|
||||||
} else {
|
} else {
|
||||||
@@ -378,67 +372,12 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(j.disAllowedIps) > 0 {
|
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
|
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) {
|
func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
inbound := &model.Inbound{}
|
inbound := &model.Inbound{}
|
||||||
|
|||||||
@@ -2032,7 +2032,6 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if t != nil && client != nil {
|
if t != nil && client != nil {
|
||||||
t.Enable = client.Enable
|
|
||||||
t.UUID = client.ID
|
t.UUID = client.ID
|
||||||
t.SubId = client.SubID
|
t.SubId = client.SubID
|
||||||
return t, nil
|
return t, nil
|
||||||
|
|||||||
@@ -1926,6 +1926,8 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||||||
} else {
|
} else {
|
||||||
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
||||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove())
|
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":
|
case "add_client_submit_enable":
|
||||||
client_Enable = true
|
client_Enable = true
|
||||||
@@ -1936,6 +1938,8 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||||||
} else {
|
} else {
|
||||||
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
||||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove())
|
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":
|
case "reset_all_traffics_cancel":
|
||||||
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
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.
|
// addClient handles the process of adding a new client to an inbound.
|
||||||
func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
|
func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
|
||||||
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
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
|
protocol := inbound.Protocol
|
||||||
|
|
||||||
|
var protocolRows [][]telego.InlineKeyboardButton
|
||||||
switch protocol {
|
switch protocol {
|
||||||
case model.VMESS, model.VLESS:
|
case model.VMESS, model.VLESS:
|
||||||
inlineKeyboard := tu.InlineKeyboard(
|
protocolRows = [][]telego.InlineKeyboardButton{
|
||||||
tu.InlineKeyboardRow(
|
tu.InlineKeyboardRow(
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
|
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.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:
|
case model.Trojan:
|
||||||
inlineKeyboard := tu.InlineKeyboard(
|
protocolRows = [][]telego.InlineKeyboardButton{
|
||||||
tu.InlineKeyboardRow(
|
tu.InlineKeyboardRow(
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
|
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.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:
|
case model.Shadowsocks:
|
||||||
inlineKeyboard := tu.InlineKeyboard(
|
protocolRows = [][]telego.InlineKeyboardButton{
|
||||||
tu.InlineKeyboardRow(
|
tu.InlineKeyboardRow(
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
|
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.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.
|
// searchInbound searches for inbounds by remark and sends the results.
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func (s *UserService) GetFirstUser() (*model.User, error) {
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) CheckUser(username string, password string, twoFactorCode string) *model.User {
|
func (s *UserService) CheckUser(username string, password string, twoFactorCode string) (*model.User, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
user := &model.User{}
|
user := &model.User{}
|
||||||
@@ -43,17 +43,16 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
|
|||||||
First(user).
|
First(user).
|
||||||
Error
|
Error
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
return nil
|
return nil, errors.New("invalid credentials")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
logger.Warning("check user err:", err)
|
logger.Warning("check user err:", err)
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If LDAP enabled and local password check fails, attempt LDAP auth
|
|
||||||
if !crypto.CheckPasswordHash(user.Password, password) {
|
if !crypto.CheckPasswordHash(user.Password, password) {
|
||||||
ldapEnabled, _ := s.settingService.GetLdapEnable()
|
ldapEnabled, _ := s.settingService.GetLdapEnable()
|
||||||
if !ldapEnabled {
|
if !ldapEnabled {
|
||||||
return nil
|
return nil, errors.New("invalid credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
host, _ := s.settingService.GetLdapHost()
|
host, _ := s.settingService.GetLdapHost()
|
||||||
@@ -77,15 +76,14 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
|
|||||||
}
|
}
|
||||||
ok, err := ldaputil.AuthenticateUser(cfg, username, password)
|
ok, err := ldaputil.AuthenticateUser(cfg, username, password)
|
||||||
if err != nil || !ok {
|
if err != nil || !ok {
|
||||||
return nil
|
return nil, errors.New("invalid credentials")
|
||||||
}
|
}
|
||||||
// On successful LDAP auth, continue 2FA checks below
|
|
||||||
}
|
}
|
||||||
|
|
||||||
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
|
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("check two factor err:", err)
|
logger.Warning("check two factor err:", err)
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if twoFactorEnable {
|
if twoFactorEnable {
|
||||||
@@ -93,15 +91,15 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("check two factor token err:", err)
|
logger.Warning("check two factor token err:", err)
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode {
|
if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode {
|
||||||
return nil
|
return nil, errors.New("invalid 2fa code")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return user
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) UpdateUser(id int, username string, password string) error {
|
func (s *UserService) UpdateUser(id int, username string, password string) error {
|
||||||
|
|||||||
@@ -663,6 +663,7 @@
|
|||||||
"userSaved" = "✅ حفظت بيانات مستخدم Telegram."
|
"userSaved" = "✅ حفظت بيانات مستخدم Telegram."
|
||||||
"loginSuccess" = "✅ تسجيل الدخول للبانل تم بنجاح.\r\n"
|
"loginSuccess" = "✅ تسجيل الدخول للبانل تم بنجاح.\r\n"
|
||||||
"loginFailed" = "❗️فشل محاولة تسجيل الدخول للبانل.\r\n"
|
"loginFailed" = "❗️فشل محاولة تسجيل الدخول للبانل.\r\n"
|
||||||
|
"2faFailed" = "فشل 2FA"
|
||||||
"report" = "🕰 التقارير المجدولة: {{ .RunTime }}\r\n"
|
"report" = "🕰 التقارير المجدولة: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ التاريخ والوقت: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ التاريخ والوقت: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 السيرفر: {{ .Hostname }}\r\n"
|
"hostname" = "💻 السيرفر: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -663,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Telegram User saved."
|
"userSaved" = "✅ Telegram User saved."
|
||||||
"loginSuccess" = "✅ Logged in to the panel successfully.\r\n"
|
"loginSuccess" = "✅ Logged in to the panel successfully.\r\n"
|
||||||
"loginFailed" = "❗️Login attempt to the panel failed.\r\n"
|
"loginFailed" = "❗️Login attempt to the panel failed.\r\n"
|
||||||
|
"2faFailed" = "2FA Failed"
|
||||||
"report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n"
|
"report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Date&Time: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Date&Time: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -663,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Usuario de Telegram guardado."
|
"userSaved" = "✅ Usuario de Telegram guardado."
|
||||||
"loginSuccess" = "✅ Has iniciado sesión en el panel con éxito.\r\n"
|
"loginSuccess" = "✅ Has iniciado sesión en el panel con éxito.\r\n"
|
||||||
"loginFailed" = "❗️ Falló el inicio de sesión en el panel.\r\n"
|
"loginFailed" = "❗️ Falló el inicio de sesión en el panel.\r\n"
|
||||||
|
"2faFailed" = "Error de 2FA"
|
||||||
"report" = "🕰 Informes programados: {{ .RunTime }}\r\n"
|
"report" = "🕰 Informes programados: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Fecha y Hora: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Fecha y Hora: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Nombre del Host: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Nombre del Host: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -663,6 +663,7 @@
|
|||||||
"userSaved" = "✅ کاربر تلگرام ذخیره شد."
|
"userSaved" = "✅ کاربر تلگرام ذخیره شد."
|
||||||
"loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n"
|
"loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n"
|
||||||
"loginFailed" = "❗️ ورود به پنل ناموفقبود \r\n"
|
"loginFailed" = "❗️ ورود به پنل ناموفقبود \r\n"
|
||||||
|
"2faFailed" = "خطای 2FA"
|
||||||
"report" = "🕰 گزارشاتزمانبندیشده: {{ .RunTime }}\r\n"
|
"report" = "🕰 گزارشاتزمانبندیشده: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ تاریخوزمان: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ تاریخوزمان: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 ناممیزبان: {{ .Hostname }}\r\n"
|
"hostname" = "💻 ناممیزبان: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -663,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Pengguna Telegram tersimpan."
|
"userSaved" = "✅ Pengguna Telegram tersimpan."
|
||||||
"loginSuccess" = "✅ Berhasil masuk ke panel.\r\n"
|
"loginSuccess" = "✅ Berhasil masuk ke panel.\r\n"
|
||||||
"loginFailed" = "❗️ Gagal masuk ke panel.\r\n"
|
"loginFailed" = "❗️ Gagal masuk ke panel.\r\n"
|
||||||
|
"2faFailed" = "2FA Gagal"
|
||||||
"report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n"
|
"report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -663,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Telegramユーザーが保存されました。"
|
"userSaved" = "✅ Telegramユーザーが保存されました。"
|
||||||
"loginSuccess" = "✅ パネルに正常にログインしました。\r\n"
|
"loginSuccess" = "✅ パネルに正常にログインしました。\r\n"
|
||||||
"loginFailed" = "❗️ パネルのログインに失敗しました。\r\n"
|
"loginFailed" = "❗️ パネルのログインに失敗しました。\r\n"
|
||||||
|
"2faFailed" = "2FAエラー"
|
||||||
"report" = "🕰 定期報告:{{ .RunTime }}\r\n"
|
"report" = "🕰 定期報告:{{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ 日時:{{ .DateTime }}\r\n"
|
"datetime" = "⏰ 日時:{{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 ホスト名:{{ .Hostname }}\r\n"
|
"hostname" = "💻 ホスト名:{{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -663,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Usuário do Telegram salvo."
|
"userSaved" = "✅ Usuário do Telegram salvo."
|
||||||
"loginSuccess" = "✅ Conectado ao painel com sucesso.\r\n"
|
"loginSuccess" = "✅ Conectado ao painel com sucesso.\r\n"
|
||||||
"loginFailed" = "❗️Tentativa de login no painel falhou.\r\n"
|
"loginFailed" = "❗️Tentativa de login no painel falhou.\r\n"
|
||||||
|
"2faFailed" = "Falha no 2FA"
|
||||||
"report" = "🕰 Relatórios agendados: {{ .RunTime }}\r\n"
|
"report" = "🕰 Relatórios agendados: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Data&Hora: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Data&Hora: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -149,7 +149,7 @@
|
|||||||
"geofileUpdateDialogDesc" = "Это обновит файл #filename#."
|
"geofileUpdateDialogDesc" = "Это обновит файл #filename#."
|
||||||
"geofilesUpdateDialogDesc" = "Это обновит все геофайлы."
|
"geofilesUpdateDialogDesc" = "Это обновит все геофайлы."
|
||||||
"geofilesUpdateAll" = "Обновить все"
|
"geofilesUpdateAll" = "Обновить все"
|
||||||
"geofileUpdatePopover" = "Геофайл успешно обновлён"
|
"geofileUpdatePopover" = "Геофайлы успешно обновлены"
|
||||||
"dontRefresh" = "Установка в процессе. Не обновляйте страницу"
|
"dontRefresh" = "Установка в процессе. Не обновляйте страницу"
|
||||||
"logs" = "Журнал"
|
"logs" = "Журнал"
|
||||||
"config" = "Конфигурация"
|
"config" = "Конфигурация"
|
||||||
@@ -663,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Пользователь Telegram сохранен."
|
"userSaved" = "✅ Пользователь Telegram сохранен."
|
||||||
"loginSuccess" = "✅ Успешный вход в панель.\r\n"
|
"loginSuccess" = "✅ Успешный вход в панель.\r\n"
|
||||||
"loginFailed" = "❗️ Ошибка входа в панель.\r\n"
|
"loginFailed" = "❗️ Ошибка входа в панель.\r\n"
|
||||||
|
"2faFailed" = "Ошибка 2FA"
|
||||||
"report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n"
|
"report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -663,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Telegram Kullanıcısı kaydedildi."
|
"userSaved" = "✅ Telegram Kullanıcısı kaydedildi."
|
||||||
"loginSuccess" = "✅ Panele başarıyla giriş yapıldı.\r\n"
|
"loginSuccess" = "✅ Panele başarıyla giriş yapıldı.\r\n"
|
||||||
"loginFailed" = "❗️Panele giriş denemesi başarısız oldu.\r\n"
|
"loginFailed" = "❗️Panele giriş denemesi başarısız oldu.\r\n"
|
||||||
|
"2faFailed" = "2FA Hatası"
|
||||||
"report" = "🕰 Planlanmış Raporlar: {{ .RunTime }}\r\n"
|
"report" = "🕰 Planlanmış Raporlar: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Tarih&Zaman: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Tarih&Zaman: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Sunucu: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Sunucu: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -663,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Користувача Telegram збережено."
|
"userSaved" = "✅ Користувача Telegram збережено."
|
||||||
"loginSuccess" = "✅ Успішно ввійшли в панель\r\n"
|
"loginSuccess" = "✅ Успішно ввійшли в панель\r\n"
|
||||||
"loginFailed" = "❗️ Помилка входу в панель.\r\n"
|
"loginFailed" = "❗️ Помилка входу в панель.\r\n"
|
||||||
|
"2faFailed" = "Помилка 2FA"
|
||||||
"report" = "🕰 Заплановані звіти: {{ .RunTime }}\r\n"
|
"report" = "🕰 Заплановані звіти: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Дата й час: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Дата й час: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Хост: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Хост: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -663,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Người dùng Telegram đã được lưu."
|
"userSaved" = "✅ Người dùng Telegram đã được lưu."
|
||||||
"loginSuccess" = "✅ Đăng nhập thành công vào bảng điều khiển.\r\n"
|
"loginSuccess" = "✅ Đăng nhập thành công vào bảng điều khiển.\r\n"
|
||||||
"loginFailed" = "❗️ Đăng nhập vào bảng điều khiển thất bại.\r\n"
|
"loginFailed" = "❗️ Đăng nhập vào bảng điều khiển thất bại.\r\n"
|
||||||
|
"2faFailed" = "Lỗi 2FA"
|
||||||
"report" = "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n"
|
"report" = "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Ngày-Giờ: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Ngày-Giờ: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Tên máy chủ: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Tên máy chủ: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -663,6 +663,7 @@
|
|||||||
"userSaved" = "✅ 电报用户已保存。"
|
"userSaved" = "✅ 电报用户已保存。"
|
||||||
"loginSuccess" = "✅ 成功登录到面板。\r\n"
|
"loginSuccess" = "✅ 成功登录到面板。\r\n"
|
||||||
"loginFailed" = "❗️ 面板登录失败。\r\n"
|
"loginFailed" = "❗️ 面板登录失败。\r\n"
|
||||||
|
"2faFailed" = "2FA 失败"
|
||||||
"report" = "🕰 定时报告:{{ .RunTime }}\r\n"
|
"report" = "🕰 定时报告:{{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n"
|
"datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 主机名:{{ .Hostname }}\r\n"
|
"hostname" = "💻 主机名:{{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -663,6 +663,7 @@
|
|||||||
"userSaved" = "✅ 電報使用者已儲存。"
|
"userSaved" = "✅ 電報使用者已儲存。"
|
||||||
"loginSuccess" = "✅ 成功登入到面板。\r\n"
|
"loginSuccess" = "✅ 成功登入到面板。\r\n"
|
||||||
"loginFailed" = "❗️ 面板登入失敗。\r\n"
|
"loginFailed" = "❗️ 面板登入失敗。\r\n"
|
||||||
|
"2faFailed" = "2FA 失敗"
|
||||||
"report" = "🕰 定時報告:{{ .RunTime }}\r\n"
|
"report" = "🕰 定時報告:{{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ 日期時間:{{ .DateTime }}\r\n"
|
"datetime" = "⏰ 日期時間:{{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 主機名:{{ .Hostname }}\r\n"
|
"hostname" = "💻 主機名:{{ .Hostname }}\r\n"
|
||||||
|
|||||||
6
x-ui.sh
6
x-ui.sh
@@ -317,12 +317,12 @@ check_config() {
|
|||||||
start >/dev/null 2>&1
|
start >/dev/null 2>&1
|
||||||
else
|
else
|
||||||
LOGE "IP certificate setup failed."
|
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
|
start >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
|
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
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -2012,7 +2012,7 @@ EOF
|
|||||||
cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
|
cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
|
||||||
[Definition]
|
[Definition]
|
||||||
datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
|
datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
|
||||||
failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*SRC\s*=\s*<ADDR>
|
failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*Disconnecting OLD IP\s*=\s*<ADDR>\s*\|\|\s*Timestamp\s*=\s*\d+
|
||||||
ignoreregex =
|
ignoreregex =
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user