diff options
| author | MHSanaei <ho3ein.sanaei@gmail.com> | 2026-02-12 00:21:09 +0300 |
|---|---|---|
| committer | MHSanaei <ho3ein.sanaei@gmail.com> | 2026-02-12 00:21:09 +0300 |
| commit | e5c0fe3edf3bc8ee44c13503cc39d4caba735ae9 (patch) | |
| tree | 87d72b513ebf0a34774b95e11d0598047c71fa95 /web | |
| parent | f4057989f520daaef30b9d1cc0b0b0f12dbd7edc (diff) | |
bug fix #3785
Diffstat (limited to 'web')
| -rw-r--r-- | web/controller/inbound.go | 32 | ||||
| -rw-r--r-- | web/html/modals/inbound_info_modal.html | 146 | ||||
| -rw-r--r-- | web/service/inbound.go | 37 | ||||
| -rw-r--r-- | web/service/tgbot.go | 35 |
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( |
