From 21983971971b14377b36c8db92c8603f723f955d Mon Sep 17 00:00:00 2001
From: Ali Golzar <57574919+aliglzr@users.noreply.github.com>
Date: Wed, 27 Aug 2025 21:00:49 +0330
Subject: [PATCH] Created / Updated fields for clients (#3384)
* feat(backend): add created_at/updated_at to clients and maintain on create/update
backfill existing clients and set updated_at on mutations
* feat(frontend): carry created_at/updated_at in client models and round-trip via JSON
* feat(frontend): display Created and Updated columns in client table with proper date formatting
* i18n: add pages.inbounds.createdAt/updatedAt across all locales
* Update inbound.go
Remove duplicate code
---
database/model/model.go | 2 +
web/assets/js/model/inbound.js | 36 ++++++++-
web/html/component/aClientTable.html | 26 ++++++
web/html/inbounds.html | 2 +
web/service/inbound.go | 117 +++++++++++++++++++++++++++
web/translation/translate.ar_EG.toml | 2 +
web/translation/translate.en_US.toml | 2 +
web/translation/translate.es_ES.toml | 2 +
web/translation/translate.fa_IR.toml | 2 +
web/translation/translate.id_ID.toml | 2 +
web/translation/translate.ja_JP.toml | 2 +
web/translation/translate.pt_BR.toml | 2 +
web/translation/translate.ru_RU.toml | 2 +
web/translation/translate.tr_TR.toml | 2 +
web/translation/translate.uk_UA.toml | 2 +
web/translation/translate.vi_VN.toml | 2 +
web/translation/translate.zh_CN.toml | 2 +
web/translation/translate.zh_TW.toml | 2 +
18 files changed, 205 insertions(+), 4 deletions(-)
diff --git a/database/model/model.go b/database/model/model.go
index 2e7095d3..86ab0487 100644
--- a/database/model/model.go
+++ b/database/model/model.go
@@ -104,4 +104,6 @@ type Client struct {
SubID string `json:"subId" form:"subId"`
Comment string `json:"comment" form:"comment"`
Reset int `json:"reset" form:"reset"`
+ CreatedAt int64 `json:"created_at,omitempty"`
+ UpdatedAt int64 `json:"updated_at,omitempty"`
}
diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js
index 803b5d94..33aa24e0 100644
--- a/web/assets/js/model/inbound.js
+++ b/web/assets/js/model/inbound.js
@@ -1817,7 +1817,9 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
tgId = '',
subId = RandomUtil.randomLowerAndNum(16),
comment = '',
- reset = 0
+ reset = 0,
+ created_at = undefined,
+ updated_at = undefined
) {
super();
this.id = id;
@@ -1831,6 +1833,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
this.subId = subId;
this.comment = comment;
this.reset = reset;
+ this.created_at = created_at;
+ this.updated_at = updated_at;
}
static fromJson(json = {}) {
@@ -1846,6 +1850,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
json.subId,
json.comment,
json.reset,
+ json.created_at,
+ json.updated_at,
);
}
get _expiryTime() {
@@ -1926,7 +1932,9 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
tgId = '',
subId = RandomUtil.randomLowerAndNum(16),
comment = '',
- reset = 0
+ reset = 0,
+ created_at = undefined,
+ updated_at = undefined
) {
super();
this.id = id;
@@ -1940,6 +1948,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
this.subId = subId;
this.comment = comment;
this.reset = reset;
+ this.created_at = created_at;
+ this.updated_at = updated_at;
}
static fromJson(json = {}) {
@@ -1955,6 +1965,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
json.subId,
json.comment,
json.reset,
+ json.created_at,
+ json.updated_at,
);
}
@@ -2065,7 +2077,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
tgId = '',
subId = RandomUtil.randomLowerAndNum(16),
comment = '',
- reset = 0
+ reset = 0,
+ created_at = undefined,
+ updated_at = undefined
) {
super();
this.password = password;
@@ -2078,6 +2092,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
this.subId = subId;
this.comment = comment;
this.reset = reset;
+ this.created_at = created_at;
+ this.updated_at = updated_at;
}
toJson() {
@@ -2092,6 +2108,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
subId: this.subId,
comment: this.comment,
reset: this.reset,
+ created_at: this.created_at,
+ updated_at: this.updated_at,
};
}
@@ -2107,6 +2125,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
json.subId,
json.comment,
json.reset,
+ json.created_at,
+ json.updated_at,
);
}
@@ -2226,7 +2246,9 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
tgId = '',
subId = RandomUtil.randomLowerAndNum(16),
comment = '',
- reset = 0
+ reset = 0,
+ created_at = undefined,
+ updated_at = undefined
) {
super();
this.method = method;
@@ -2240,6 +2262,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
this.subId = subId;
this.comment = comment;
this.reset = reset;
+ this.created_at = created_at;
+ this.updated_at = updated_at;
}
toJson() {
@@ -2255,6 +2279,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
subId: this.subId,
comment: this.comment,
reset: this.reset,
+ created_at: this.created_at,
+ updated_at: this.updated_at,
};
}
@@ -2271,6 +2297,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
json.subId,
json.comment,
json.reset,
+ json.created_at,
+ json.updated_at,
);
}
diff --git a/web/html/component/aClientTable.html b/web/html/component/aClientTable.html
index 96bd502f..359e6e74 100644
--- a/web/html/component/aClientTable.html
+++ b/web/html/component/aClientTable.html
@@ -278,4 +278,30 @@
+
+
+
+ [[ DateUtil.formatMillis(client.created_at) ]]
+
+
+ [[ DateUtil.convertToJalalian(moment(client.created_at)) ]]
+
+
+
+ -
+
+
+
+
+
+ [[ DateUtil.formatMillis(client.updated_at) ]]
+
+
+ [[ DateUtil.convertToJalalian(moment(client.updated_at)) ]]
+
+
+
+ -
+
+
{{end}}
diff --git a/web/html/inbounds.html b/web/html/inbounds.html
index 58d2d07a..010296eb 100644
--- a/web/html/inbounds.html
+++ b/web/html/inbounds.html
@@ -760,6 +760,8 @@
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
+ { title: '{{ i18n "pages.inbounds.createdAt" }}', width: 90, align: 'center', scopedSlots: { customRender: 'createdAt' } },
+ { title: '{{ i18n "pages.inbounds.updatedAt" }}', width: 90, align: 'center', scopedSlots: { customRender: 'updatedAt' } },
];
const innerMobileColumns = [
diff --git a/web/service/inbound.go b/web/service/inbound.go
index 6e10e798..4ef5fce3 100644
--- a/web/service/inbound.go
+++ b/web/service/inbound.go
@@ -175,6 +175,30 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
return inbound, false, err
}
+ // Ensure created_at and updated_at on clients in settings
+ if len(clients) > 0 {
+ var settings map[string]any
+ if err2 := json.Unmarshal([]byte(inbound.Settings), &settings); err2 == nil && settings != nil {
+ now := time.Now().Unix() * 1000
+ updatedClients := make([]model.Client, 0, len(clients))
+ for _, c := range clients {
+ if c.CreatedAt == 0 {
+ c.CreatedAt = now
+ }
+ c.UpdatedAt = now
+ updatedClients = append(updatedClients, c)
+ }
+ settings["clients"] = updatedClients
+ if bs, err3 := json.MarshalIndent(settings, "", " "); err3 == nil {
+ inbound.Settings = string(bs)
+ } else {
+ logger.Debug("Unable to marshal inbound settings with timestamps:", err3)
+ }
+ } else if err2 != nil {
+ logger.Debug("Unable to parse inbound settings for timestamps:", err2)
+ }
+ }
+
// Secure client ID
for _, client := range clients {
switch inbound.Protocol {
@@ -320,6 +344,53 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
return inbound, false, err
}
+ // Ensure created_at and updated_at exist in inbound.Settings clients
+ {
+ var oldSettings map[string]any
+ _ = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
+ emailToCreated := map[string]int64{}
+ if oldSettings != nil {
+ if oc, ok := oldSettings["clients"].([]any); ok {
+ for _, it := range oc {
+ if m, ok2 := it.(map[string]any); ok2 {
+ if email, ok3 := m["email"].(string); ok3 {
+ switch v := m["created_at"].(type) {
+ case float64:
+ emailToCreated[email] = int64(v)
+ case int64:
+ emailToCreated[email] = v
+ }
+ }
+ }
+ }
+ }
+ }
+ var newSettings map[string]any
+ if err2 := json.Unmarshal([]byte(inbound.Settings), &newSettings); err2 == nil && newSettings != nil {
+ now := time.Now().Unix() * 1000
+ if nSlice, ok := newSettings["clients"].([]any); ok {
+ for i := range nSlice {
+ if m, ok2 := nSlice[i].(map[string]any); ok2 {
+ email, _ := m["email"].(string)
+ if _, ok3 := m["created_at"]; !ok3 {
+ if v, ok4 := emailToCreated[email]; ok4 && v > 0 {
+ m["created_at"] = v
+ } else {
+ m["created_at"] = now
+ }
+ }
+ m["updated_at"] = now
+ nSlice[i] = m
+ }
+ }
+ newSettings["clients"] = nSlice
+ if bs, err3 := json.MarshalIndent(newSettings, "", " "); err3 == nil {
+ inbound.Settings = string(bs)
+ }
+ }
+ }
+ }
+
oldInbound.Up = inbound.Up
oldInbound.Down = inbound.Down
oldInbound.Total = inbound.Total
@@ -422,6 +493,17 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
}
interfaceClients := settings["clients"].([]any)
+ // Add timestamps for new clients being appended
+ nowTs := time.Now().Unix() * 1000
+ for i := range interfaceClients {
+ if cm, ok := interfaceClients[i].(map[string]any); ok {
+ if _, ok2 := cm["created_at"]; !ok2 {
+ cm["created_at"] = nowTs
+ }
+ cm["updated_at"] = nowTs
+ interfaceClients[i] = cm
+ }
+ }
existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil {
return false, err
@@ -672,6 +754,25 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
return false, err
}
settingsClients := oldSettings["clients"].([]any)
+ // Preserve created_at and set updated_at for the replacing client
+ var preservedCreated any
+ if clientIndex >= 0 && clientIndex < len(settingsClients) {
+ if oldMap, ok := settingsClients[clientIndex].(map[string]any); ok {
+ if v, ok2 := oldMap["created_at"]; ok2 {
+ preservedCreated = v
+ }
+ }
+ }
+ if len(interfaceClients) > 0 {
+ if newMap, ok := interfaceClients[0].(map[string]any); ok {
+ if preservedCreated == nil {
+ preservedCreated = time.Now().Unix() * 1000
+ }
+ newMap["created_at"] = preservedCreated
+ newMap["updated_at"] = time.Now().Unix() * 1000
+ interfaceClients[0] = newMap
+ }
+ }
settingsClients[clientIndex] = interfaceClients[0]
oldSettings["clients"] = settingsClients
@@ -909,10 +1010,16 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
oldExpiryTime := c["expiryTime"].(float64)
newExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime)
c["expiryTime"] = newExpiryTime
+ c["updated_at"] = time.Now().Unix() * 1000
dbClientTraffics[traffic_index].ExpiryTime = newExpiryTime
break
}
}
+ // Backfill created_at and updated_at
+ if _, ok := c["created_at"]; !ok {
+ c["created_at"] = time.Now().Unix() * 1000
+ }
+ c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c))
}
settings["clients"] = newClients
@@ -1274,6 +1381,7 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (boo
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["tgId"] = tgId
+ c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c))
}
}
@@ -1360,6 +1468,7 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["enable"] = !clientOldEnabled
+ c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c))
}
}
@@ -1423,6 +1532,7 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["limitIp"] = count
+ c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c))
}
}
@@ -1481,6 +1591,7 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["expiryTime"] = expiry_time
+ c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c))
}
}
@@ -1542,6 +1653,7 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["totalGB"] = totalGB * 1024 * 1024 * 1024
+ c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c))
}
}
@@ -1962,6 +2074,11 @@ func (s *InboundService) MigrationRequirements() {
c["flow"] = ""
}
}
+ // Backfill created_at and updated_at
+ if _, ok := c["created_at"]; !ok {
+ c["created_at"] = time.Now().Unix() * 1000
+ }
+ c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c))
}
settings["clients"] = newClients
diff --git a/web/translation/translate.ar_EG.toml b/web/translation/translate.ar_EG.toml
index f1c1919e..4e4aac75 100644
--- a/web/translation/translate.ar_EG.toml
+++ b/web/translation/translate.ar_EG.toml
@@ -165,6 +165,8 @@
"details" = "تفاصيل"
"transportConfig" = "نقل"
"expireDate" = "المدة"
+"createdAt" = "تاريخ الإنشاء"
+"updatedAt" = "تاريخ التحديث"
"resetTraffic" = "إعادة ضبط الترافيك"
"addInbound" = "أضف إدخال"
"generalActions" = "إجراءات عامة"
diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml
index abff6bc7..4e27908d 100644
--- a/web/translation/translate.en_US.toml
+++ b/web/translation/translate.en_US.toml
@@ -165,6 +165,8 @@
"details" = "Details"
"transportConfig" = "Transport"
"expireDate" = "Duration"
+"createdAt" = "Created"
+"updatedAt" = "Updated"
"resetTraffic" = "Reset Traffic"
"addInbound" = "Add Inbound"
"generalActions" = "General Actions"
diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml
index 883f4e89..3ad93de2 100644
--- a/web/translation/translate.es_ES.toml
+++ b/web/translation/translate.es_ES.toml
@@ -165,6 +165,8 @@
"details" = "Detalles"
"transportConfig" = "Transporte"
"expireDate" = "Fecha de Expiración"
+"createdAt" = "Creado"
+"updatedAt" = "Actualizado"
"resetTraffic" = "Restablecer Tráfico"
"addInbound" = "Agregar Entrada"
"generalActions" = "Acciones Generales"
diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml
index 787a695a..5e810e62 100644
--- a/web/translation/translate.fa_IR.toml
+++ b/web/translation/translate.fa_IR.toml
@@ -165,6 +165,8 @@
"details" = "توضیحات"
"transportConfig" = "نحوه اتصال"
"expireDate" = "مدت زمان"
+"createdAt" = "ایجاد"
+"updatedAt" = "بهروزرسانی"
"resetTraffic" = "ریست ترافیک"
"addInbound" = "افزودن ورودی"
"generalActions" = "عملیات کلی"
diff --git a/web/translation/translate.id_ID.toml b/web/translation/translate.id_ID.toml
index 5f4c648d..dcfe68c0 100644
--- a/web/translation/translate.id_ID.toml
+++ b/web/translation/translate.id_ID.toml
@@ -165,6 +165,8 @@
"details" = "Rincian"
"transportConfig" = "Transport"
"expireDate" = "Durasi"
+"createdAt" = "Dibuat"
+"updatedAt" = "Diperbarui"
"resetTraffic" = "Reset Traffic"
"addInbound" = "Tambahkan Masuk"
"generalActions" = "Tindakan Umum"
diff --git a/web/translation/translate.ja_JP.toml b/web/translation/translate.ja_JP.toml
index 8efa9074..a8702cd8 100644
--- a/web/translation/translate.ja_JP.toml
+++ b/web/translation/translate.ja_JP.toml
@@ -165,6 +165,8 @@
"details" = "詳細情報"
"transportConfig" = "トランスポート設定"
"expireDate" = "有効期限"
+"createdAt" = "作成"
+"updatedAt" = "更新"
"resetTraffic" = "トラフィックリセット"
"addInbound" = "インバウンド追加"
"generalActions" = "一般操作"
diff --git a/web/translation/translate.pt_BR.toml b/web/translation/translate.pt_BR.toml
index 0d012591..813a4dde 100644
--- a/web/translation/translate.pt_BR.toml
+++ b/web/translation/translate.pt_BR.toml
@@ -165,6 +165,8 @@
"details" = "Detalhes"
"transportConfig" = "Transporte"
"expireDate" = "Duração"
+"createdAt" = "Criado"
+"updatedAt" = "Atualizado"
"resetTraffic" = "Redefinir Tráfego"
"addInbound" = "Adicionar Inbound"
"generalActions" = "Ações Gerais"
diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml
index 558d309b..be4a1ef3 100644
--- a/web/translation/translate.ru_RU.toml
+++ b/web/translation/translate.ru_RU.toml
@@ -165,6 +165,8 @@
"details" = "Подробнее"
"transportConfig" = "Транспорт"
"expireDate" = "Дата окончания"
+"createdAt" = "Создано"
+"updatedAt" = "Обновлено"
"resetTraffic" = "Сброс трафика"
"addInbound" = "Создать инбаунд"
"generalActions" = "Общие действия"
diff --git a/web/translation/translate.tr_TR.toml b/web/translation/translate.tr_TR.toml
index 32dca2ea..7159c9b5 100644
--- a/web/translation/translate.tr_TR.toml
+++ b/web/translation/translate.tr_TR.toml
@@ -165,6 +165,8 @@
"details" = "Detaylar"
"transportConfig" = "Taşıma"
"expireDate" = "Süre"
+"createdAt" = "Oluşturuldu"
+"updatedAt" = "Güncellendi"
"resetTraffic" = "Trafiği Sıfırla"
"addInbound" = "Gelen Ekle"
"generalActions" = "Genel Eylemler"
diff --git a/web/translation/translate.uk_UA.toml b/web/translation/translate.uk_UA.toml
index 8976b66a..95eca7a6 100644
--- a/web/translation/translate.uk_UA.toml
+++ b/web/translation/translate.uk_UA.toml
@@ -165,6 +165,8 @@
"details" = "Деталі"
"transportConfig" = "Транспорт"
"expireDate" = "Тривалість"
+"createdAt" = "Створено"
+"updatedAt" = "Оновлено"
"resetTraffic" = "Скинути трафік"
"addInbound" = "Додати вхідний"
"generalActions" = "Загальні дії"
diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml
index 6e28a0de..f8144a2f 100644
--- a/web/translation/translate.vi_VN.toml
+++ b/web/translation/translate.vi_VN.toml
@@ -165,6 +165,8 @@
"details" = "Chi tiết"
"transportConfig" = "Giao vận"
"expireDate" = "Ngày hết hạn"
+"createdAt" = "Tạo lúc"
+"updatedAt" = "Cập nhật"
"resetTraffic" = "Đặt lại lưu lượng"
"addInbound" = "Thêm điểm vào"
"generalActions" = "Hành động chung"
diff --git a/web/translation/translate.zh_CN.toml b/web/translation/translate.zh_CN.toml
index a2142bd1..6490372c 100644
--- a/web/translation/translate.zh_CN.toml
+++ b/web/translation/translate.zh_CN.toml
@@ -165,6 +165,8 @@
"details" = "详细信息"
"transportConfig" = "传输配置"
"expireDate" = "到期时间"
+"createdAt" = "创建时间"
+"updatedAt" = "更新时间"
"resetTraffic" = "重置流量"
"addInbound" = "添加入站"
"generalActions" = "通用操作"
diff --git a/web/translation/translate.zh_TW.toml b/web/translation/translate.zh_TW.toml
index db5b33ed..cd4f22e7 100644
--- a/web/translation/translate.zh_TW.toml
+++ b/web/translation/translate.zh_TW.toml
@@ -165,6 +165,8 @@
"details" = "詳細資訊"
"transportConfig" = "傳輸配置"
"expireDate" = "到期時間"
+"createdAt" = "建立時間"
+"updatedAt" = "更新時間"
"resetTraffic" = "重置流量"
"addInbound" = "新增入站"
"generalActions" = "通用操作"