mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-02-13 13:57:59 +00:00
bug fix #3785
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
This commit is contained in:
parent
f4057989f5
commit
e5c0fe3edf
4 changed files with 231 additions and 19 deletions
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
|
@ -193,6 +194,37 @@ func (a *InboundController) getClientIps(c *gin.Context) {
|
||||||
return
|
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)
|
jsonObj(c, ips, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -260,15 +260,31 @@
|
||||||
v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
|
v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
|
||||||
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
|
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a-tag>[[ infoModal.clientIps ]]</a-tag>
|
<div
|
||||||
<a-icon type="sync" :spin="refreshing" @click="refreshIPs"
|
style="max-height: 150px; overflow-y: auto; text-align: left;">
|
||||||
:style="{ margin: '0 5px' }"></a-icon>
|
<div
|
||||||
<a-tooltip :title="[[ dbInbound.address ]]">
|
v-if="infoModal.clientIpsArray && infoModal.clientIpsArray.length > 0">
|
||||||
<template slot="title">
|
<a-tag
|
||||||
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
|
v-for="(ipInfo, idx) in infoModal.clientIpsArray"
|
||||||
</template>
|
:key="idx"
|
||||||
<a-icon type="delete" @click="clearClientIps"></a-icon>
|
color="blue"
|
||||||
</a-tooltip>
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -542,12 +558,73 @@
|
||||||
<script>
|
<script>
|
||||||
function refreshIPs(email) {
|
function refreshIPs(email) {
|
||||||
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
|
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
|
||||||
if (msg.success) {
|
if (!msg.success) {
|
||||||
try {
|
return { text: 'No IP Record', array: [] };
|
||||||
return JSON.parse(msg.obj).join(', ');
|
}
|
||||||
} catch (e) {
|
|
||||||
return msg.obj;
|
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: '',
|
subLink: '',
|
||||||
subJsonLink: '',
|
subJsonLink: '',
|
||||||
clientIps: '',
|
clientIps: '',
|
||||||
|
clientIpsArray: [],
|
||||||
show(dbInbound, index) {
|
show(dbInbound, index) {
|
||||||
this.index = index;
|
this.index = index;
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = dbInbound.toInbound();
|
||||||
|
|
@ -583,8 +661,9 @@
|
||||||
].includes(this.inbound.protocol)
|
].includes(this.inbound.protocol)
|
||||||
) {
|
) {
|
||||||
if (app.ipLimitEnable && this.clientSettings.limitIp) {
|
if (app.ipLimitEnable && this.clientSettings.limitIp) {
|
||||||
refreshIPs(this.clientStats.email).then((ips) => {
|
refreshIPs(this.clientStats.email).then((result) => {
|
||||||
this.clientIps = ips;
|
this.clientIps = result.text;
|
||||||
|
this.clientIpsArray = result.array;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -655,6 +734,35 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
copy(content) {
|
||||||
ClipboardManager
|
ClipboardManager
|
||||||
.copyText(content)
|
.copyText(content)
|
||||||
|
|
@ -672,8 +780,9 @@
|
||||||
refreshIPs() {
|
refreshIPs() {
|
||||||
this.refreshing = true;
|
this.refreshing = true;
|
||||||
refreshIPs(this.infoModal.clientStats.email)
|
refreshIPs(this.infoModal.clientStats.email)
|
||||||
.then((ips) => {
|
.then((result) => {
|
||||||
this.infoModal.clientIps = ips;
|
this.infoModal.clientIps = result.text;
|
||||||
|
this.infoModal.clientIpsArray = result.array;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.refreshing = false;
|
this.refreshing = false;
|
||||||
|
|
@ -686,6 +795,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.infoModal.clientIps = 'No IP Record';
|
this.infoModal.clientIps = 'No IP Record';
|
||||||
|
this.infoModal.clientIpsArray = [];
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2141,6 +2141,43 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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
|
return InboundClientIps.Ips, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -3083,9 +3084,41 @@ func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
|
||||||
ips = t.I18nBot("tgbot.noIpRecord")
|
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 := ""
|
||||||
output += t.I18nBot("tgbot.messages.email", "Email=="+email)
|
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"))
|
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
inlineKeyboard := tu.InlineKeyboard(
|
inlineKeyboard := tu.InlineKeyboard(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue