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
path: root/web
diff options
context:
space:
mode:
authorMHSanaei <ho3ein.sanaei@gmail.com>2026-02-12 00:21:09 +0300
committerMHSanaei <ho3ein.sanaei@gmail.com>2026-02-12 00:21:09 +0300
commite5c0fe3edf3bc8ee44c13503cc39d4caba735ae9 (patch)
tree87d72b513ebf0a34774b95e11d0598047c71fa95 /web
parentf4057989f520daaef30b9d1cc0b0b0f12dbd7edc (diff)
bug fix #3785
Diffstat (limited to 'web')
-rw-r--r--web/controller/inbound.go32
-rw-r--r--web/html/modals/inbound_info_modal.html146
-rw-r--r--web/service/inbound.go37
-rw-r--r--web/service/tgbot.go35
4 files changed, 231 insertions, 19 deletions
diff --git a/web/controller/inbound.go b/web/controller/inbound.go
index 8317de31..b012ec95 100644
--- a/web/controller/inbound.go
+++ b/web/controller/inbound.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"strconv"
+ "time"
"github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/web/service"
@@ -193,6 +194,37 @@ func (a *InboundController) getClientIps(c *gin.Context) {
return
}
+ // Prefer returning a normalized string list for consistent UI rendering
+ type ipWithTimestamp struct {
+ IP string `json:"ip"`
+ Timestamp int64 `json:"timestamp"`
+ }
+
+ var ipsWithTime []ipWithTimestamp
+ if err := json.Unmarshal([]byte(ips), &ipsWithTime); err == nil && len(ipsWithTime) > 0 {
+ formatted := make([]string, 0, len(ipsWithTime))
+ for _, item := range ipsWithTime {
+ if item.IP == "" {
+ continue
+ }
+ if item.Timestamp > 0 {
+ ts := time.Unix(item.Timestamp, 0).Local().Format("2006-01-02 15:04:05")
+ formatted = append(formatted, fmt.Sprintf("%s (%s)", item.IP, ts))
+ continue
+ }
+ formatted = append(formatted, item.IP)
+ }
+ jsonObj(c, formatted, nil)
+ return
+ }
+
+ var oldIps []string
+ if err := json.Unmarshal([]byte(ips), &oldIps); err == nil && len(oldIps) > 0 {
+ jsonObj(c, oldIps, nil)
+ return
+ }
+
+ // If parsing fails, return as string
jsonObj(c, ips, nil)
}
diff --git a/web/html/modals/inbound_info_modal.html b/web/html/modals/inbound_info_modal.html
index 1ab187ee..37f8248a 100644
--- a/web/html/modals/inbound_info_modal.html
+++ b/web/html/modals/inbound_info_modal.html
@@ -260,15 +260,31 @@
v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
<td>
- <a-tag>[[ infoModal.clientIps ]]</a-tag>
- <a-icon type="sync" :spin="refreshing" @click="refreshIPs"
- :style="{ margin: '0 5px' }"></a-icon>
- <a-tooltip :title="[[ dbInbound.address ]]">
- <template slot="title">
- <span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
- </template>
- <a-icon type="delete" @click="clearClientIps"></a-icon>
- </a-tooltip>
+ <div
+ style="max-height: 150px; overflow-y: auto; text-align: left;">
+ <div
+ v-if="infoModal.clientIpsArray && infoModal.clientIpsArray.length > 0">
+ <a-tag
+ v-for="(ipInfo, idx) in infoModal.clientIpsArray"
+ :key="idx"
+ color="blue"
+ style="margin: 2px 0; display: block; font-family: monospace; font-size: 11px;">
+ [[ formatIpInfo(ipInfo) ]]
+ </a-tag>
+ </div>
+ <a-tag v-else>[[ infoModal.clientIps || 'No IP Record'
+ ]]</a-tag>
+ </div>
+ <div style="margin-top: 5px;">
+ <a-icon type="sync" :spin="refreshing" @click="refreshIPs"
+ :style="{ margin: '0 5px' }"></a-icon>
+ <a-tooltip>
+ <template slot="title">
+ <span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
+ </template>
+ <a-icon type="delete" @click="clearClientIps"></a-icon>
+ </a-tooltip>
+ </div>
</td>
</tr>
</table>
@@ -542,12 +558,73 @@
<script>
function refreshIPs(email) {
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
- if (msg.success) {
- try {
- return JSON.parse(msg.obj).join(', ');
- } catch (e) {
- return msg.obj;
+ if (!msg.success) {
+ return { text: 'No IP Record', array: [] };
+ }
+
+ const formatIpRecord = (record) => {
+ if (record == null) {
+ return '';
+ }
+ if (typeof record === 'string' || typeof record === 'number') {
+ return String(record);
+ }
+ const ip = record.ip || record.IP || '';
+ const timestamp = record.timestamp || record.Timestamp || 0;
+ if (!ip) {
+ return String(record);
+ }
+ if (!timestamp) {
+ return String(ip);
+ }
+ const date = new Date(Number(timestamp) * 1000);
+ const timeStr = date
+ .toLocaleString('en-GB', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false,
+ })
+ .replace(',', '');
+ return `${ip} (${timeStr})`;
+ };
+
+ try {
+ let ips = msg.obj;
+ // If msg.obj is a string, try to parse it
+ if (typeof ips === 'string') {
+ try {
+ ips = JSON.parse(ips);
+ } catch (e) {
+ return { text: String(ips), array: [String(ips)] };
+ }
+ }
+
+ // Normalize single object response to array
+ if (ips && !Array.isArray(ips) && typeof ips === 'object') {
+ ips = [ips];
}
+
+ // New format or object array
+ if (Array.isArray(ips) && ips.length > 0 && typeof ips[0] === 'object') {
+ const result = ips.map((item) => formatIpRecord(item)).filter(Boolean);
+ return { text: result.join(' | '), array: result };
+ }
+
+ // Old format - simple array of IPs
+ if (Array.isArray(ips) && ips.length > 0) {
+ const result = ips.map((ip) => String(ip));
+ return { text: result.join(', '), array: result };
+ }
+
+ // Fallback for any other format
+ return { text: String(ips), array: [String(ips)] };
+
+ } catch (e) {
+ return { text: 'Error loading IPs', array: [] };
}
});
}
@@ -566,6 +643,7 @@
subLink: '',
subJsonLink: '',
clientIps: '',
+ clientIpsArray: [],
show(dbInbound, index) {
this.index = index;
this.inbound = dbInbound.toInbound();
@@ -583,8 +661,9 @@
].includes(this.inbound.protocol)
) {
if (app.ipLimitEnable && this.clientSettings.limitIp) {
- refreshIPs(this.clientStats.email).then((ips) => {
- this.clientIps = ips;
+ refreshIPs(this.clientStats.email).then((result) => {
+ this.clientIps = result.text;
+ this.clientIpsArray = result.array;
})
}
}
@@ -655,6 +734,35 @@
},
},
methods: {
+ formatIpInfo(ipInfo) {
+ if (ipInfo == null) {
+ return '';
+ }
+ if (typeof ipInfo === 'string' || typeof ipInfo === 'number') {
+ return String(ipInfo);
+ }
+ const ip = ipInfo.ip || ipInfo.IP || '';
+ const timestamp = ipInfo.timestamp || ipInfo.Timestamp || 0;
+ if (!ip) {
+ return String(ipInfo);
+ }
+ if (!timestamp) {
+ return String(ip);
+ }
+ const date = new Date(Number(timestamp) * 1000);
+ const timeStr = date
+ .toLocaleString('en-GB', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false,
+ })
+ .replace(',', '');
+ return `${ip} (${timeStr})`;
+ },
copy(content) {
ClipboardManager
.copyText(content)
@@ -672,8 +780,9 @@
refreshIPs() {
this.refreshing = true;
refreshIPs(this.infoModal.clientStats.email)
- .then((ips) => {
- this.infoModal.clientIps = ips;
+ .then((result) => {
+ this.infoModal.clientIps = result.text;
+ this.infoModal.clientIpsArray = result.array;
})
.finally(() => {
this.refreshing = false;
@@ -686,6 +795,7 @@
return;
}
this.infoModal.clientIps = 'No IP Record';
+ this.infoModal.clientIpsArray = [];
})
.catch(() => {});
},
diff --git a/web/service/inbound.go b/web/service/inbound.go
index 469fa561..ec51bc27 100644
--- a/web/service/inbound.go
+++ b/web/service/inbound.go
@@ -2141,6 +2141,43 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
if err != nil {
return "", err
}
+
+ if InboundClientIps.Ips == "" {
+ return "", nil
+ }
+
+ // Try to parse as new format (with timestamps)
+ type IPWithTimestamp struct {
+ IP string `json:"ip"`
+ Timestamp int64 `json:"timestamp"`
+ }
+
+ var ipsWithTime []IPWithTimestamp
+ err = json.Unmarshal([]byte(InboundClientIps.Ips), &ipsWithTime)
+
+ // If successfully parsed as new format, return with timestamps
+ if err == nil && len(ipsWithTime) > 0 {
+ return InboundClientIps.Ips, nil
+ }
+
+ // Otherwise, assume it's old format (simple string array)
+ // Try to parse as simple array and convert to new format
+ var oldIps []string
+ err = json.Unmarshal([]byte(InboundClientIps.Ips), &oldIps)
+ if err == nil && len(oldIps) > 0 {
+ // Convert old format to new format with current timestamp
+ newIpsWithTime := make([]IPWithTimestamp, len(oldIps))
+ for i, ip := range oldIps {
+ newIpsWithTime[i] = IPWithTimestamp{
+ IP: ip,
+ Timestamp: time.Now().Unix(),
+ }
+ }
+ result, _ := json.Marshal(newIpsWithTime)
+ return string(result), nil
+ }
+
+ // Return as-is if parsing fails
return InboundClientIps.Ips, nil
}
diff --git a/web/service/tgbot.go b/web/service/tgbot.go
index cb84142c..52df9092 100644
--- a/web/service/tgbot.go
+++ b/web/service/tgbot.go
@@ -5,6 +5,7 @@ import (
"crypto/rand"
"embed"
"encoding/base64"
+ "encoding/json"
"errors"
"fmt"
"io"
@@ -3083,9 +3084,41 @@ func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
ips = t.I18nBot("tgbot.noIpRecord")
}
+ formattedIps := ips
+ if err == nil && len(ips) > 0 {
+ type ipWithTimestamp struct {
+ IP string `json:"ip"`
+ Timestamp int64 `json:"timestamp"`
+ }
+
+ var ipsWithTime []ipWithTimestamp
+ if json.Unmarshal([]byte(ips), &ipsWithTime) == nil && len(ipsWithTime) > 0 {
+ lines := make([]string, 0, len(ipsWithTime))
+ for _, item := range ipsWithTime {
+ if item.IP == "" {
+ continue
+ }
+ if item.Timestamp > 0 {
+ ts := time.Unix(item.Timestamp, 0).Format("2006-01-02 15:04:05")
+ lines = append(lines, fmt.Sprintf("%s (%s)", item.IP, ts))
+ continue
+ }
+ lines = append(lines, item.IP)
+ }
+ if len(lines) > 0 {
+ formattedIps = strings.Join(lines, "\n")
+ }
+ } else {
+ var oldIps []string
+ if json.Unmarshal([]byte(ips), &oldIps) == nil && len(oldIps) > 0 {
+ formattedIps = strings.Join(oldIps, "\n")
+ }
+ }
+ }
+
output := ""
output += t.I18nBot("tgbot.messages.email", "Email=="+email)
- output += t.I18nBot("tgbot.messages.ips", "IPs=="+ips)
+ output += t.I18nBot("tgbot.messages.ips", "IPs=="+formattedIps)
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
inlineKeyboard := tu.InlineKeyboard(