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 @@ + + {{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" = "通用操作"