Bläddra i källkod

feat(clients): show short HWID fingerprint in admin device list (#6464)

* feat(clients): show short HWID fingerprint in admin device list

Expose a 12-char prefix of the stored hwid_hash in the admin HWID list API and UI so admins can distinguish devices without querying the database. Full hashes and raw HWIDs remain unexposed.

Fixes #6359

* ci: retrigger release matrix after 386 dependency download flake

---------

Co-authored-by: mrchatam <[email protected]>
Co-authored-by: mrchatam <[email protected]>
mrchatam 3 timmar sedan
förälder
incheckning
8082ab4d74

+ 4 - 4
docs/content/docs/en/reference/api/clients.mdx

@@ -227,8 +227,8 @@ _openapi:
       title: Reset the recorded IP list for a client.
       url: '#reset-the-recorded-ip-list-for-a-client'
     - depth: 2
-      title: List registered HWID devices for a client. Hashes are not exposed.
-      url: '#list-registered-hwid-devices-for-a-client-hashes-are-not-exposed'
+      title: List registered HWID devices for a client with a short fingerprint. Full hashes are not exposed.
+      url: '#list-registered-hwid-devices-for-a-client-with-a-short-fingerprint-full-hashes-are-not-exposed'
     - depth: 2
       title: Clear all registered HWID devices for a client so new devices can
         register again.
@@ -473,8 +473,8 @@ _openapi:
         id: list-source-ips-that-have-connected-with-the-given-clients-credentials-returns-an-array-of-ip-timestamp-strings
       - content: Reset the recorded IP list for a client.
         id: reset-the-recorded-ip-list-for-a-client
-      - content: List registered HWID devices for a client. Hashes are not exposed.
-        id: list-registered-hwid-devices-for-a-client-hashes-are-not-exposed
+      - content: List registered HWID devices for a client with a short fingerprint. Full hashes are not exposed.
+        id: list-registered-hwid-devices-for-a-client-with-a-short-fingerprint-full-hashes-are-not-exposed
       - content: Clear all registered HWID devices for a client so new devices can
           register again.
         id: clear-all-registered-hwid-devices-for-a-client-so-new-devices-can-register-again

+ 3 - 2
docs/public/openapi.json

@@ -9604,7 +9604,7 @@
         "tags": [
           "Clients"
         ],
-        "summary": "List registered HWID devices for a client. Hashes are not exposed.",
+        "summary": "List registered HWID devices for a client with a short fingerprint. Full hashes are not exposed.",
         "operationId": "post_panel_api_clients_hwids_email",
         "parameters": [
           {
@@ -9644,7 +9644,8 @@
                       "userAgent": "Happ/1.0",
                       "deviceOs": "android",
                       "osVersion": "15",
-                      "deviceModel": "Pixel 9"
+                      "deviceModel": "Pixel 9",
+                      "fingerprint": "6ad17c93e821"
                     }
                   ]
                 }

+ 3 - 2
frontend/public/openapi.json

@@ -9604,7 +9604,7 @@
         "tags": [
           "Clients"
         ],
-        "summary": "List registered HWID devices for a client. Hashes are not exposed.",
+        "summary": "List registered HWID devices for a client with a short fingerprint. Full hashes are not exposed.",
         "operationId": "post_panel_api_clients_hwids_email",
         "parameters": [
           {
@@ -9644,7 +9644,8 @@
                       "userAgent": "Happ/1.0",
                       "deviceOs": "android",
                       "osVersion": "15",
-                      "deviceModel": "Pixel 9"
+                      "deviceModel": "Pixel 9",
+                      "fingerprint": "6ad17c93e821"
                     }
                   ]
                 }

+ 8 - 0
frontend/src/components/clients/ClientHwidList.tsx

@@ -101,6 +101,14 @@ export default function ClientHwidListModal({
                     </Typography.Text>
                   </>
                 )}
+                {entry.fingerprint && (
+                  <>
+                    <br />
+                    <Typography.Text type="secondary" code>
+                      {t('pages.clients.hwidFingerprint')}: {entry.fingerprint}
+                    </Typography.Text>
+                  </>
+                )}
               </div>
               <Popconfirm
                 title={t('pages.clients.deleteHwidConfirm')}

+ 1 - 0
frontend/src/lib/clients/hwid-log.ts

@@ -8,6 +8,7 @@ export type ClientHwidInfo = {
   deviceOs: string;
   osVersion: string;
   deviceModel: string;
+  fingerprint: string;
 };
 
 // normalizeClientHwids accepts the API payload and returns typed entries,

+ 3 - 2
frontend/src/pages/api-docs/endpoints.ts

@@ -1483,10 +1483,11 @@ export const sections: readonly Section[] = [
       {
         method: 'POST',
         path: '/panel/api/clients/hwids/:email',
-        summary: 'List registered HWID devices for a client. Hashes are not exposed.',
+        summary:
+          'List registered HWID devices for a client with a short fingerprint. Full hashes are not exposed.',
         params: [{ name: 'email', in: 'path', type: 'string', desc: 'Client email.' }],
         response:
-          '{\n  "success": true,\n  "obj": [\n    {\n      "id": 1,\n      "firstSeen": 1735000000000,\n      "lastSeen": 1735100000000,\n      "userAgent": "Happ/1.0",\n      "deviceOs": "android",\n      "osVersion": "15",\n      "deviceModel": "Pixel 9"\n    }\n  ]\n}',
+          '{\n  "success": true,\n  "obj": [\n    {\n      "id": 1,\n      "firstSeen": 1735000000000,\n      "lastSeen": 1735100000000,\n      "userAgent": "Happ/1.0",\n      "deviceOs": "android",\n      "osVersion": "15",\n      "deviceModel": "Pixel 9",\n      "fingerprint": "6ad17c93e821"\n    }\n  ]\n}',
       },
       {
         method: 'DELETE',

+ 13 - 1
internal/web/service/client_hwid.go

@@ -41,7 +41,10 @@ type HwidSlotStatus struct {
 	Full       bool `json:"full" example:"false"`
 }
 
-const minHwidLength = 6
+const (
+	minHwidLength         = 6
+	hwidFingerprintLength = 12
+)
 
 type ClientHwidInfo struct {
 	Id          int    `json:"id"`
@@ -51,6 +54,7 @@ type ClientHwidInfo struct {
 	DeviceOS    string `json:"deviceOs"`
 	OsVersion   string `json:"osVersion"`
 	DeviceModel string `json:"deviceModel"`
+	Fingerprint string `json:"fingerprint"`
 }
 
 func hashHwid(raw string) string {
@@ -58,6 +62,13 @@ func hashHwid(raw string) string {
 	return hex.EncodeToString(sum[:])
 }
 
+func shortHwidFingerprint(hash string) string {
+	if len(hash) <= hwidFingerprintLength {
+		return hash
+	}
+	return hash[:hwidFingerprintLength]
+}
+
 func trimHwidMeta(s string) string {
 	s = strings.TrimSpace(s)
 	r := []rune(s)
@@ -232,6 +243,7 @@ func (s *ClientService) ListClientHwids(email string) ([]ClientHwidInfo, error)
 			DeviceOS:    r.DeviceOS,
 			OsVersion:   r.OsVersion,
 			DeviceModel: r.DeviceModel,
+			Fingerprint: shortHwidFingerprint(r.HwidHash),
 		})
 	}
 	return out, nil

+ 7 - 0
internal/web/service/client_hwid_test.go

@@ -121,8 +121,15 @@ func TestClientHwidGateRegistersAndBlocks(t *testing.T) {
 	}
 	foundUpdated := false
 	for _, row := range list {
+		if len(row.Fingerprint) != hwidFingerprintLength {
+			t.Fatalf("fingerprint length = %d, want %d: %q", len(row.Fingerprint), hwidFingerprintLength, row.Fingerprint)
+		}
 		if row.DeviceModel == "updated-model" && row.UserAgent == "Karing/2.0" && row.DeviceOS == "ios" && row.OsVersion == "18" {
 			foundUpdated = true
+			want := hashHwid(firstRaw)[:hwidFingerprintLength]
+			if row.Fingerprint != want {
+				t.Fatalf("fingerprint = %q, want %q", row.Fingerprint, want)
+			}
 		}
 	}
 	if !foundUpdated {

+ 1 - 0
internal/web/translation/ar-EG.json

@@ -762,6 +762,7 @@
       "noHwids": "لا توجد أجهزة HWID بعد",
       "firstSeen": "أول ظهور",
       "lastSeen": "آخر ظهور",
+      "hwidFingerprint": "بصمة HWID",
       "deleteHwid": "إزالة الجهاز",
       "deleteHwidConfirm": "إزالة هذا الجهاز؟ سيحتاج إلى إعادة التسجيل عند جلب الاشتراك التالي.",
       "hwidDeleted": "تمت إزالة الجهاز.",

+ 1 - 0
internal/web/translation/en-US.json

@@ -762,6 +762,7 @@
       "noHwids": "No HWID devices yet",
       "firstSeen": "First seen",
       "lastSeen": "Last seen",
+      "hwidFingerprint": "HWID fingerprint",
       "deleteHwid": "Remove device",
       "deleteHwidConfirm": "Remove this device? It will need to re-register on its next subscription fetch.",
       "hwidDeleted": "Device removed.",

+ 1 - 0
internal/web/translation/es-ES.json

@@ -762,6 +762,7 @@
       "noHwids": "Aún no hay dispositivos HWID",
       "firstSeen": "Visto por primera vez",
       "lastSeen": "Visto por última vez",
+      "hwidFingerprint": "Huella HWID",
       "deleteHwid": "Eliminar dispositivo",
       "deleteHwidConfirm": "¿Eliminar este dispositivo? Deberá volver a registrarse en la próxima obtención de la suscripción.",
       "hwidDeleted": "Dispositivo eliminado.",

+ 1 - 0
internal/web/translation/fa-IR.json

@@ -762,6 +762,7 @@
       "noHwids": "هنوز دستگاه HWID ثبت نشده است",
       "firstSeen": "اولین مشاهده",
       "lastSeen": "آخرین مشاهده",
+      "hwidFingerprint": "اثر انگشت HWID",
       "deleteHwid": "حذف دستگاه",
       "deleteHwidConfirm": "این دستگاه حذف شود؟ در دریافت بعدی اشتراک باید دوباره ثبت‌نام شود.",
       "hwidDeleted": "دستگاه حذف شد.",

+ 1 - 0
internal/web/translation/id-ID.json

@@ -762,6 +762,7 @@
       "noHwids": "Belum ada perangkat HWID",
       "firstSeen": "Pertama terlihat",
       "lastSeen": "Terakhir terlihat",
+      "hwidFingerprint": "Sidik jari HWID",
       "deleteHwid": "Hapus perangkat",
       "deleteHwidConfirm": "Hapus perangkat ini? Perangkat perlu mendaftar ulang pada pengambilan langganan berikutnya.",
       "hwidDeleted": "Perangkat dihapus.",

+ 1 - 0
internal/web/translation/ja-JP.json

@@ -762,6 +762,7 @@
       "noHwids": "HWID デバイスはまだありません",
       "firstSeen": "初回確認",
       "lastSeen": "最終確認",
+      "hwidFingerprint": "HWID フィンガープリント",
       "deleteHwid": "デバイスを削除",
       "deleteHwidConfirm": "このデバイスを削除しますか?次回のサブスクリプション取得時に再登録が必要になります。",
       "hwidDeleted": "デバイスを削除しました。",

+ 1 - 0
internal/web/translation/pt-BR.json

@@ -762,6 +762,7 @@
       "noHwids": "Ainda não há dispositivos HWID",
       "firstSeen": "Visto primeiro",
       "lastSeen": "Visto por último",
+      "hwidFingerprint": "Impressão digital HWID",
       "deleteHwid": "Remover dispositivo",
       "deleteHwidConfirm": "Remover este dispositivo? Ele precisará se registrar novamente na próxima busca da assinatura.",
       "hwidDeleted": "Dispositivo removido.",

+ 1 - 0
internal/web/translation/ru-RU.json

@@ -762,6 +762,7 @@
       "noHwids": "Устройств HWID пока нет",
       "firstSeen": "Первое появление",
       "lastSeen": "Последнее появление",
+      "hwidFingerprint": "Отпечаток HWID",
       "deleteHwid": "Удалить устройство",
       "deleteHwidConfirm": "Удалить это устройство? При следующем запросе подписки оно зарегистрируется заново.",
       "hwidDeleted": "Устройство удалено.",

+ 1 - 0
internal/web/translation/tr-TR.json

@@ -762,6 +762,7 @@
       "noHwids": "Henüz HWID cihazı yok",
       "firstSeen": "İlk görülme",
       "lastSeen": "Son görülme",
+      "hwidFingerprint": "HWID parmak izi",
       "deleteHwid": "Cihazı kaldır",
       "deleteHwidConfirm": "Bu cihaz kaldırılsın mı? Bir sonraki abonelik alımında yeniden kaydolması gerekecek.",
       "hwidDeleted": "Cihaz kaldırıldı.",

+ 1 - 0
internal/web/translation/uk-UA.json

@@ -762,6 +762,7 @@
       "noHwids": "Пристроїв HWID ще немає",
       "firstSeen": "Перша поява",
       "lastSeen": "Остання поява",
+      "hwidFingerprint": "Відбиток HWID",
       "deleteHwid": "Видалити пристрій",
       "deleteHwidConfirm": "Видалити цей пристрій? Йому потрібно буде зареєструватися знову під час наступного отримання підписки.",
       "hwidDeleted": "Пристрій видалено.",

+ 1 - 0
internal/web/translation/vi-VN.json

@@ -762,6 +762,7 @@
       "noHwids": "Chưa có thiết bị HWID",
       "firstSeen": "Lần đầu thấy",
       "lastSeen": "Lần cuối thấy",
+      "hwidFingerprint": "Dấu vân tay HWID",
       "deleteHwid": "Xóa thiết bị",
       "deleteHwidConfirm": "Xóa thiết bị này? Thiết bị sẽ cần đăng ký lại vào lần lấy gói đăng ký tiếp theo.",
       "hwidDeleted": "Đã xóa thiết bị.",

+ 1 - 0
internal/web/translation/zh-CN.json

@@ -762,6 +762,7 @@
       "noHwids": "暂无 HWID 设备",
       "firstSeen": "首次出现",
       "lastSeen": "最后出现",
+      "hwidFingerprint": "HWID 指纹",
       "deleteHwid": "移除设备",
       "deleteHwidConfirm": "移除此设备?下次获取订阅时它将需要重新注册。",
       "hwidDeleted": "设备已移除。",

+ 1 - 0
internal/web/translation/zh-TW.json

@@ -762,6 +762,7 @@
       "noHwids": "尚無 HWID 裝置",
       "firstSeen": "首次出現",
       "lastSeen": "最後出現",
+      "hwidFingerprint": "HWID 指紋",
       "deleteHwid": "移除裝置",
       "deleteHwidConfirm": "移除此裝置?下次取得訂閱時它將需要重新註冊。",
       "hwidDeleted": "裝置已移除。",