From 79e6c778a151e247d63cd05776f55f05bc2bdd1d Mon Sep 17 00:00:00 2001 From: kazeyukiro <177553227+kazeyukiro@users.noreply.github.com> Date: Mon, 21 Sep 2026 00:00:00 +0800 Subject: [PATCH] chore: remove HWID feature (subscription device binding) --- README.ar.md | 2 +- README.de.md | 2 +- README.en.md | 4 +- README.es.md | 2 +- README.fr.md | 2 +- README.hi.md | 2 +- README.id.md | 2 +- README.ja.md | 2 +- README.ko.md | 2 +- README.md | 4 +- README.pl.md | 2 +- README.pt-BR.md | 2 +- README.ru.md | 2 +- README.th.md | 2 +- README.tr.md | 2 +- README.uk.md | 2 +- README.vi.md | 2 +- README.zh-CN.md | 4 +- README.zh-TW.md | 2 +- backend/docs/openapi.yaml | 2 - backend/internal/database/database.go | 63 +--------- .../internal/database/models/proxy_user.go | 2 - backend/internal/docs/openapi.yaml | 2 - backend/internal/router/subscription.go | 24 ---- backend/internal/user/api.go | 48 -------- backend/internal/user/service.go | 2 - backend/internal/user/service_crud.go | 3 - backend/internal/user/service_util.go | 2 - docs/users-limits.md | 36 +----- frontend/src/api/users.ts | 18 --- frontend/src/pages/Users.tsx | 108 ++---------------- 31 files changed, 35 insertions(+), 319 deletions(-) diff --git a/README.ar.md b/README.ar.md index 4951863..05b5fd0 100644 --- a/README.ar.md +++ b/README.ar.md @@ -25,7 +25,7 @@ | المجال | القدرات | |------|------| | **العُقد** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY target scan; TLS self-signed certs; transport field validation | -| **المستخدمون** | Bind nodes, traffic limits, expiry, IP/HWID limits, batch ops, subscription tokens | +| **المستخدمون** | Bind nodes, traffic limits, expiry, IP limits, batch ops, subscription tokens | | **الاشتراكات** | UA routing for Clash/Mihomo YAML, v2ray Base64, sing-box JSON; optional `?target=`; HTML info page | | **الإعدادات** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **التشغيل** | Core start/stop/update, logs, dashboard metrics, Geo files, panel SSL/ACME, backup/restore | diff --git a/README.de.md b/README.de.md index 93cb7b0..1e7d143 100644 --- a/README.de.md +++ b/README.de.md @@ -25,7 +25,7 @@ Leichtgewichtig und selbst gehostet. Verwalten Sie [Mihomo](https://github.com/M | Bereich | Fähigkeiten | |------|------| | **Knoten** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY target scan; TLS self-signed certs; transport field validation | -| **Benutzer** | Bind nodes, traffic limits, expiry, IP/HWID limits, batch ops, subscription tokens | +| **Benutzer** | Bind nodes, traffic limits, expiry, IP limits, batch ops, subscription tokens | | **Abonnements** | UA routing for Clash/Mihomo YAML, v2ray Base64, sing-box JSON; optional `?target=`; HTML info page | | **Konfiguration** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **Betrieb** | Core start/stop/update, logs, dashboard metrics, Geo files, panel SSL/ACME, backup/restore | diff --git a/README.en.md b/README.en.md index 16e4cf0..179acff 100644 --- a/README.en.md +++ b/README.en.md @@ -25,7 +25,7 @@ Lightweight and self-hosted. Manage [Mihomo](https://github.com/MetaCubeX/mihomo | Area | Capabilities | |------|------| | **Nodes** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY; TLS self-signed; batch certificate; **traffic multiplier**; transport validation | -| **Users** | Bind nodes, traffic/expiry, **per-node usage** (billed = raw × node multiplier), IP/HWID/sub-pull limits, start-on-first-use, cycle renew/reset, groups/tags, external sub merge, batch ops, subscription tokens | +| **Users** | Bind nodes, traffic/expiry, **per-node usage** (billed = raw × node multiplier), IP/sub-pull limits, start-on-first-use, cycle renew/reset, groups/tags, external sub merge, batch ops, subscription tokens | | **Subscriptions** | UA routing for Clash/Mihomo YAML, v2ray Base64 (`?target=v2ray` always Base64), sing-box JSON; optional `?target=`; HTML info page; TUIC/HY2 share TLS params | | **Config** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **Ops** | Core start/stop/update, logs, dashboard, online connections, Geo, panel SSL/ACME, WARP one-click, backup/restore | @@ -93,7 +93,7 @@ Static Linux binaries for multiple architectures are on [Releases](https://githu ### Extra docs (limits / certs / subscriptions) -- [User limits: IP · HWID · sub pulls](docs/users-limits.md) +- [User limits: IP · sub pulls](docs/users-limits.md) - [Per-node traffic & multiplier](docs/node-traffic.md) - [Batch node certificates](docs/batch-certificate.md) - [Subscription formats](docs/subscription-formats.md) diff --git a/README.es.md b/README.es.md index 563bb7e..2c9811e 100644 --- a/README.es.md +++ b/README.es.md @@ -25,7 +25,7 @@ Ligero y autoalojado. Gestiona listeners, usuarios, suscripciones y estado de [M | Área | Capacidades | |------|------| | **Nodos** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY target scan; TLS self-signed certs; transport field validation | -| **Usuarios** | Bind nodes, traffic limits, expiry, IP/HWID limits, batch ops, subscription tokens | +| **Usuarios** | Bind nodes, traffic limits, expiry, IP limits, batch ops, subscription tokens | | **Suscripciones** | UA routing for Clash/Mihomo YAML, v2ray Base64, sing-box JSON; optional `?target=`; HTML info page | | **Configuración** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **Operaciones** | Core start/stop/update, logs, dashboard metrics, Geo files, panel SSL/ACME, backup/restore | diff --git a/README.fr.md b/README.fr.md index 15b2495..edb11d8 100644 --- a/README.fr.md +++ b/README.fr.md @@ -25,7 +25,7 @@ Léger et auto-hébergé. Gérez les listeners, utilisateurs, abonnements et l | Domaine | Capacités | |------|------| | **Nœuds** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY target scan; TLS self-signed certs; transport field validation | -| **Utilisateurs** | Bind nodes, traffic limits, expiry, IP/HWID limits, batch ops, subscription tokens | +| **Utilisateurs** | Bind nodes, traffic limits, expiry, IP limits, batch ops, subscription tokens | | **Abonnements** | UA routing for Clash/Mihomo YAML, v2ray Base64, sing-box JSON; optional `?target=`; HTML info page | | **Configuration** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **Exploitation** | Core start/stop/update, logs, dashboard metrics, Geo files, panel SSL/ACME, backup/restore | diff --git a/README.hi.md b/README.hi.md index bd1afcc..91ef74a 100644 --- a/README.hi.md +++ b/README.hi.md @@ -25,7 +25,7 @@ | क्षेत्र | क्षमताएँ | |------|------| | **नोड्स** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY target scan; TLS self-signed certs; transport field validation | -| **उपयोगकर्ता** | Bind nodes, traffic limits, expiry, IP/HWID limits, batch ops, subscription tokens | +| **उपयोगकर्ता** | Bind nodes, traffic limits, expiry, IP limits, batch ops, subscription tokens | | **सब्सक्रिप्शन** | UA routing for Clash/Mihomo YAML, v2ray Base64, sing-box JSON; optional `?target=`; HTML info page | | **कॉन्फ़िग** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **ऑप्स** | Core start/stop/update, logs, dashboard metrics, Geo files, panel SSL/ACME, backup/restore | diff --git a/README.id.md b/README.id.md index 4b51fa9..41d9012 100644 --- a/README.id.md +++ b/README.id.md @@ -25,7 +25,7 @@ Ringan dan self-hosted. Kelola listener, pengguna, langganan, dan status [Mihomo | Area | Kemampuan | |------|------| | **Node** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY target scan; TLS self-signed certs; transport field validation | -| **Pengguna** | Bind nodes, traffic limits, expiry, IP/HWID limits, batch ops, subscription tokens | +| **Pengguna** | Bind nodes, traffic limits, expiry, IP limits, batch ops, subscription tokens | | **Langganan** | UA routing for Clash/Mihomo YAML, v2ray Base64, sing-box JSON; optional `?target=`; HTML info page | | **Konfigurasi** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **Operasi** | Core start/stop/update, logs, dashboard metrics, Geo files, panel SSL/ACME, backup/restore | diff --git a/README.ja.md b/README.ja.md index 7e06dd1..2a181e3 100644 --- a/README.ja.md +++ b/README.ja.md @@ -25,7 +25,7 @@ | 区分 | 内容 | |------|------| | **ノード** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY target scan; TLS self-signed certs; transport field validation | -| **ユーザー** | Bind nodes, traffic limits, expiry, IP/HWID limits, batch ops, subscription tokens | +| **ユーザー** | Bind nodes, traffic limits, expiry, IP limits, batch ops, subscription tokens | | **購読** | UA routing for Clash/Mihomo YAML, v2ray Base64, sing-box JSON; optional `?target=`; HTML info page | | **設定** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **運用** | Core start/stop/update, logs, dashboard metrics, Geo files, panel SSL/ACME, backup/restore | diff --git a/README.ko.md b/README.ko.md index 87fee8f..ffbd4b1 100644 --- a/README.ko.md +++ b/README.ko.md @@ -25,7 +25,7 @@ | 구분 | 기능 | |------|------| | **노드** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY target scan; TLS self-signed certs; transport field validation | -| **사용자** | Bind nodes, traffic limits, expiry, IP/HWID limits, batch ops, subscription tokens | +| **사용자** | Bind nodes, traffic limits, expiry, IP limits, batch ops, subscription tokens | | **구독** | UA routing for Clash/Mihomo YAML, v2ray Base64, sing-box JSON; optional `?target=`; HTML info page | | **설정** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **운영** | Core start/stop/update, logs, dashboard metrics, Geo files, panel SSL/ACME, backup/restore | diff --git a/README.md b/README.md index 05ddf4f..254409f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ | 类别 | 能力 | |------|------| | **节点** | 协议注册表驱动的 Listener:VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC 等;Reality、TLS 自签、一键批量证书、**流量倍率**、传输层字段互斥校验 | -| **用户** | 绑定节点、流量限额、**按节点分别计量**(计费 = 实际 × 节点倍率)、到期、IP/HWID/订阅拉取限制、首次使用起算、周期续期/重置、分组标签、外部订阅合并、批量操作、订阅 Token | +| **用户** | 绑定节点、流量限额、**按节点分别计量**(计费 = 实际 × 节点倍率)、到期、IP/订阅拉取限制、首次使用起算、周期续期/重置、分组标签、外部订阅合并、批量操作、订阅 Token | | **订阅** | UA 自动识别 Clash/Mihomo YAML、v2ray Base64(`?target=v2ray` 始终 Base64)、sing-box JSON;可选 `?target=`;HTML 订阅页;TUIC/HY2 分享链含 SNI/`allow_insecure` 等客户端参数 | | **配置** | 生成 → 校验 → 应用 分离;失败回滚上一份 `config.yaml` | | **运维** | 核心启停/更新、日志、仪表盘、在线连接、Geo、面板 SSL/ACME、WARP 一键、备份恢复 | @@ -39,7 +39,7 @@ ### 补充说明(用户限制 / 证书 / 订阅) -- [用户限制:IP · HWID · 订阅拉取](docs/users-limits.md) +- [用户限制:IP · 订阅拉取](docs/users-limits.md) - [按节点流量与倍率](docs/node-traffic.md) - [批量应用节点证书](docs/batch-certificate.md) - [订阅格式与 target](docs/subscription-formats.md) diff --git a/README.pl.md b/README.pl.md index d0f39a0..8b06ab0 100644 --- a/README.pl.md +++ b/README.pl.md @@ -25,7 +25,7 @@ Lekki, self-hosted. Zarządzaj listenerami, użytkownikami, subskrypcjami i stat | Obszar | Możliwości | |------|------| | **Węzły** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY target scan; TLS self-signed certs; transport field validation | -| **Użytkownicy** | Bind nodes, traffic limits, expiry, IP/HWID limits, batch ops, subscription tokens | +| **Użytkownicy** | Bind nodes, traffic limits, expiry, IP limits, batch ops, subscription tokens | | **Subskrypcje** | UA routing for Clash/Mihomo YAML, v2ray Base64, sing-box JSON; optional `?target=`; HTML info page | | **Konfiguracja** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **Administracja** | Core start/stop/update, logs, dashboard metrics, Geo files, panel SSL/ACME, backup/restore | diff --git a/README.pt-BR.md b/README.pt-BR.md index 2428f58..e693c5f 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -25,7 +25,7 @@ Leve e auto-hospedado. Gerencie listeners, usuários, assinaturas e status do [M | Área | Capacidades | |------|------| | **Nós** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY target scan; TLS self-signed certs; transport field validation | -| **Usuários** | Bind nodes, traffic limits, expiry, IP/HWID limits, batch ops, subscription tokens | +| **Usuários** | Bind nodes, traffic limits, expiry, IP limits, batch ops, subscription tokens | | **Assinaturas** | UA routing for Clash/Mihomo YAML, v2ray Base64, sing-box JSON; optional `?target=`; HTML info page | | **Configuração** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **Operações** | Core start/stop/update, logs, dashboard metrics, Geo files, panel SSL/ACME, backup/restore | diff --git a/README.ru.md b/README.ru.md index 8956e7f..8a161bb 100644 --- a/README.ru.md +++ b/README.ru.md @@ -25,7 +25,7 @@ | Раздел | Описание | |------|------| | **Узлы** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY target scan; TLS self-signed certs; transport field validation | -| **Пользователи** | Bind nodes, traffic limits, expiry, IP/HWID limits, batch ops, subscription tokens | +| **Пользователи** | Bind nodes, traffic limits, expiry, IP limits, batch ops, subscription tokens | | **Подписки** | UA routing for Clash/Mihomo YAML, v2ray Base64, sing-box JSON; optional `?target=`; HTML info page | | **Конфиг** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **Администрирование** | Core start/stop/update, logs, dashboard metrics, Geo files, panel SSL/ACME, backup/restore | diff --git a/README.th.md b/README.th.md index 856d0df..2571d6d 100644 --- a/README.th.md +++ b/README.th.md @@ -25,7 +25,7 @@ | หมวด | ความสามารถ | |------|------| | **โหนด** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY target scan; TLS self-signed certs; transport field validation | -| **ผู้ใช้** | Bind nodes, traffic limits, expiry, IP/HWID limits, batch ops, subscription tokens | +| **ผู้ใช้** | Bind nodes, traffic limits, expiry, IP limits, batch ops, subscription tokens | | **สับสไครบ์** | UA routing for Clash/Mihomo YAML, v2ray Base64, sing-box JSON; optional `?target=`; HTML info page | | **คอนฟิก** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **ดูแลระบบ** | Core start/stop/update, logs, dashboard metrics, Geo files, panel SSL/ACME, backup/restore | diff --git a/README.tr.md b/README.tr.md index 9a4af51..fdfa6e3 100644 --- a/README.tr.md +++ b/README.tr.md @@ -25,7 +25,7 @@ Hafif ve self-hosted. Linux üzerinde [Mihomo](https://github.com/MetaCubeX/miho | Alan | Yetenekler | |------|------| | **Düğümler** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY target scan; TLS self-signed certs; transport field validation | -| **Kullanıcılar** | Bind nodes, traffic limits, expiry, IP/HWID limits, batch ops, subscription tokens | +| **Kullanıcılar** | Bind nodes, traffic limits, expiry, IP limits, batch ops, subscription tokens | | **Abonelikler** | UA routing for Clash/Mihomo YAML, v2ray Base64, sing-box JSON; optional `?target=`; HTML info page | | **Yapılandırma** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **İşletim** | Core start/stop/update, logs, dashboard metrics, Geo files, panel SSL/ACME, backup/restore | diff --git a/README.uk.md b/README.uk.md index 00edb30..f3a8265 100644 --- a/README.uk.md +++ b/README.uk.md @@ -25,7 +25,7 @@ | Розділ | Опис | |------|------| | **Вузли** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY target scan; TLS self-signed certs; transport field validation | -| **Користувачі** | Bind nodes, traffic limits, expiry, IP/HWID limits, batch ops, subscription tokens | +| **Користувачі** | Bind nodes, traffic limits, expiry, IP limits, batch ops, subscription tokens | | **Підписки** | UA routing for Clash/Mihomo YAML, v2ray Base64, sing-box JSON; optional `?target=`; HTML info page | | **Конфіг** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **Адміністрування** | Core start/stop/update, logs, dashboard metrics, Geo files, panel SSL/ACME, backup/restore | diff --git a/README.vi.md b/README.vi.md index a8a78ca..1130022 100644 --- a/README.vi.md +++ b/README.vi.md @@ -25,7 +25,7 @@ Nhẹ, tự lưu trữ. Quản lý listener, người dùng, subscription và tr | Hạng mục | Khả năng | |------|------| | **Node** | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY target scan; TLS self-signed certs; transport field validation | -| **Người dùng** | Bind nodes, traffic limits, expiry, IP/HWID limits, batch ops, subscription tokens | +| **Người dùng** | Bind nodes, traffic limits, expiry, IP limits, batch ops, subscription tokens | | **Subscription** | UA routing for Clash/Mihomo YAML, v2ray Base64, sing-box JSON; optional `?target=`; HTML info page | | **Cấu hình** | Generate → validate → apply; rollback previous `config.yaml` on failure | | **Vận hành** | Core start/stop/update, logs, dashboard metrics, Geo files, panel SSL/ACME, backup/restore | diff --git a/README.zh-CN.md b/README.zh-CN.md index 072cde7..fec19e7 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -25,7 +25,7 @@ | 类别 | 能力 | |------|------| | **节点** | 协议注册表驱动的 Listener:VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC 等;REALITY、TLS 自签、一键批量证书、**流量倍率**、传输层字段校验 | -| **用户** | 绑定节点、流量限额、**按节点分别计量**(计费 = 实际 × 节点倍率)、到期、IP/HWID/订阅拉取限制、首次使用起算、周期续期/重置、分组标签、外部订阅合并、批量操作、订阅 Token | +| **用户** | 绑定节点、流量限额、**按节点分别计量**(计费 = 实际 × 节点倍率)、到期、IP/订阅拉取限制、首次使用起算、周期续期/重置、分组标签、外部订阅合并、批量操作、订阅 Token | | **订阅** | UA 识别 Clash/Mihomo YAML、v2ray Base64(`?target=v2ray` 始终 Base64)、sing-box JSON;可选 `?target=`;HTML 订阅页;TUIC/HY2 分享链含客户端 TLS 参数 | | **配置** | 生成 → 校验 → 应用;失败回滚上一份 `config.yaml` | | **运维** | 核心启停/更新、日志、仪表盘、在线连接、Geo、面板 SSL/ACME、WARP 一键、备份恢复 | @@ -36,7 +36,7 @@ ### 补充说明 -- [用户限制:IP · HWID · 订阅拉取](docs/users-limits.md) +- [用户限制:IP · 订阅拉取](docs/users-limits.md) - [按节点流量与倍率](docs/node-traffic.md) - [批量应用节点证书](docs/batch-certificate.md) - [订阅格式与 target](docs/subscription-formats.md) diff --git a/README.zh-TW.md b/README.zh-TW.md index 868c6a9..394bfe7 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -25,7 +25,7 @@ | 類別 | 能力 | |------|------| | **節點** | 協定登錄表驅動的 Listener(VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC 等);REALITY 目標掃描、TLS 自簽、傳輸層欄位校驗 | -| **使用者** | 綁定節點、流量限額、到期、IP/HWID 限制、批次操作、訂閱 Token | +| **使用者** | 綁定節點、流量限額、到期、IP 限制、批次操作、訂閱 Token | | **訂閱** | UA 識別 Clash/Mihomo YAML、v2ray Base64、sing-box JSON;可選 `?target=`;HTML 訂閱頁 | | **設定** | 產生 → 校驗 → 套用;失敗回滾上一份 `config.yaml` | | **維運** | 核心啟停/更新、日誌、儀表板、Geo、面板 SSL/ACME、備份還原 | diff --git a/backend/docs/openapi.yaml b/backend/docs/openapi.yaml index f695494..e3939bd 100644 --- a/backend/docs/openapi.yaml +++ b/backend/docs/openapi.yaml @@ -1210,7 +1210,6 @@ components: online: { type: boolean } blocked: { type: boolean } ip_limit: { type: integer, description: "0 = unlimited concurrent source IPs; enforced by ~5s connection poll (disconnect excess)" } - hwid_limit: { type: integer, description: "0 = unlimited (still records x-hwid when sent); >0 requires x-hwid on sub fetch and caps devices" } sub_pull_limit: { type: integer, description: "0 = unlimited; max successful subscription pulls per rolling 24h" } remark: { type: string } group: { type: string, description: Admin group label } @@ -1230,7 +1229,6 @@ components: uuid: { type: string, description: Empty = auto-generate } traffic_limit: { type: integer, format: int64 } ip_limit: { type: integer, description: "0 = unlimited concurrent source IPs" } - hwid_limit: { type: integer, description: "0 = unlimited; >0 requires x-hwid" } sub_pull_limit: { type: integer, description: "0 = unlimited pulls per rolling 24h" } remark: { type: string } group: { type: string } diff --git a/backend/internal/database/database.go b/backend/internal/database/database.go index 33087ff..c07a50a 100644 --- a/backend/internal/database/database.go +++ b/backend/internal/database/database.go @@ -46,14 +46,11 @@ func InitDB(dbPath string) (*gorm.DB, error) { return nil, fmt.Errorf("ensure unique sub tokens: %w", err) } - if err := prepareHWIDDeviceSchema(db); err != nil { - return nil, fmt.Errorf("prepare hwid_devices schema: %w", err) - } err = db.AutoMigrate( &models.User{}, &models.Listener{}, &models.ListenerUser{}, &models.ListenerVersion{}, &models.ListenerTemplate{}, &models.Subscription{}, &models.AccessToken{}, &models.Config{}, - &models.ProxyUser{}, &models.UserNodeTraffic{}, &models.HWIDDevice{}, &models.TrafficRecord{}, &models.PanelSetting{}, &models.RemoteServer{}, &models.RemoteNodeMirror{}, &models.ProxyUserRemoteNode{}, + &models.ProxyUser{}, &models.UserNodeTraffic{}, &models.TrafficRecord{}, &models.PanelSetting{}, &models.RemoteServer{}, &models.RemoteNodeMirror{}, &models.ProxyUserRemoteNode{}, ) if err != nil { // One more cleanup pass then retry — soft-deleted collisions are the usual cause. @@ -62,7 +59,7 @@ func InitDB(dbPath string) (*gorm.DB, error) { if retryErr := db.AutoMigrate( &models.User{}, &models.Listener{}, &models.ListenerUser{}, &models.ListenerVersion{}, &models.ListenerTemplate{}, &models.Subscription{}, &models.AccessToken{}, &models.Config{}, - &models.ProxyUser{}, &models.UserNodeTraffic{}, &models.HWIDDevice{}, &models.TrafficRecord{}, &models.PanelSetting{}, &models.RemoteServer{}, &models.RemoteNodeMirror{}, &models.ProxyUserRemoteNode{}, + &models.ProxyUser{}, &models.UserNodeTraffic{}, &models.TrafficRecord{}, &models.PanelSetting{}, &models.RemoteServer{}, &models.RemoteNodeMirror{}, &models.ProxyUserRemoteNode{}, ); retryErr != nil { return nil, fmt.Errorf("failed to run database auto-migration: %w (after dedupe retry)", retryErr) } @@ -154,59 +151,3 @@ func randomHexToken(n int) (string, error) { return hex.EncodeToString(b), nil } -// prepareHWIDDeviceSchema runs before AutoMigrate. -// SQLite index names are global: a leftover uidx_user_hwid from hw_id_devices -// causes "index uidx_user_hwid already exists" when creating hwid_devices. -func prepareHWIDDeviceSchema(db *gorm.DB) error { - if db == nil { - return nil - } - // Drop legacy unique index names (SQLite does not scope indexes per table). - for _, idx := range []string{ - "uidx_user_hwid", - "idx_hw_id_devices_proxy_user_id", - "idx_hwid_devices_proxy_user_id", - } { - if err := db.Exec("DROP INDEX IF EXISTS `" + idx + "`").Error; err != nil { - log.Printf("database: drop index %s: %v", idx, err) - } - } - for _, name := range []string{"hw_id_devices", "hwid_devices"} { - if !db.Migrator().HasTable(name) { - continue - } - rows, err := db.Raw("PRAGMA table_info(`" + name + "`)").Rows() - if err != nil { - // Non-SQLite: drop legacy table name only. - if name == "hw_id_devices" { - _ = db.Migrator().DropTable(name) - } - continue - } - hasHWID := false - colCount := 0 - for rows.Next() { - var cid int - var cname, ctype string - var notnull, pk int - var dflt interface{} - if scanErr := rows.Scan(&cid, &cname, &ctype, ¬null, &dflt, &pk); scanErr != nil { - _ = rows.Close() - return scanErr - } - colCount++ - if cname == "hwid" { - hasHWID = true - } - } - _ = rows.Close() - // Drop legacy table always; drop current if missing hwid column. - if name == "hw_id_devices" || (colCount > 0 && !hasHWID) { - log.Printf("database: dropping HWID table %s for clean migrate (hasHWID=%v)", name, hasHWID) - if err := db.Exec("DROP TABLE IF EXISTS `" + name + "`").Error; err != nil { - return err - } - } - } - return nil -} diff --git a/backend/internal/database/models/proxy_user.go b/backend/internal/database/models/proxy_user.go index 946ae58..0c4111a 100644 --- a/backend/internal/database/models/proxy_user.go +++ b/backend/internal/database/models/proxy_user.go @@ -21,8 +21,6 @@ type ProxyUser struct { Enabled bool `gorm:"not null;default:true" json:"enabled"` // IPLimit is max concurrent client IPs (0 = unlimited). Max concurrent client IPs. IPLimit int `gorm:"not null;default:0" json:"ip_limit"` - // HWIDLimit is max devices allowed via subscription HWID headers (0 = unlimited / tracking only when seen). - HWIDLimit int `gorm:"not null;default:0" json:"hwid_limit"` // SubPullLimit is max subscription fetches per rolling 24h (0 = unlimited). SubPullLimit int `gorm:"not null;default:0" json:"sub_pull_limit"` // SubPullCount is successful pulls in the current window (server-side counter). diff --git a/backend/internal/docs/openapi.yaml b/backend/internal/docs/openapi.yaml index d0825dc..16989b5 100644 --- a/backend/internal/docs/openapi.yaml +++ b/backend/internal/docs/openapi.yaml @@ -1259,7 +1259,6 @@ components: online: { type: boolean } blocked: { type: boolean } ip_limit: { type: integer, description: "0 = unlimited concurrent source IPs; enforced by ~5s connection poll (disconnect excess)" } - hwid_limit: { type: integer, description: "0 = unlimited (still records x-hwid when sent); >0 requires x-hwid on sub fetch and caps devices" } sub_pull_limit: { type: integer, description: "0 = unlimited; max successful subscription pulls per rolling 24h" } remark: { type: string } group: { type: string, description: Admin group label } @@ -1279,7 +1278,6 @@ components: uuid: { type: string, description: Empty = auto-generate } traffic_limit: { type: integer, format: int64 } ip_limit: { type: integer, description: "0 = unlimited concurrent source IPs" } - hwid_limit: { type: integer, description: "0 = unlimited; >0 requires x-hwid" } sub_pull_limit: { type: integer, description: "0 = unlimited pulls per rolling 24h" } remark: { type: string } group: { type: string } diff --git a/backend/internal/router/subscription.go b/backend/internal/router/subscription.go index 8c6bde5..51dbe06 100644 --- a/backend/internal/router/subscription.go +++ b/backend/internal/router/subscription.go @@ -14,7 +14,6 @@ import ( "github.com/kazeyukiro/3m-ui/backend/internal/config" "github.com/kazeyukiro/3m-ui/backend/internal/converter" "github.com/kazeyukiro/3m-ui/backend/internal/database/models" - "github.com/kazeyukiro/3m-ui/backend/internal/hwid" "github.com/kazeyukiro/3m-ui/backend/internal/node" "github.com/kazeyukiro/3m-ui/backend/internal/subpage" "github.com/kazeyukiro/3m-ui/backend/internal/subpull" @@ -75,29 +74,6 @@ func subscriptionHandler(db *gorm.DB, cfg *config.Config) gin.HandlerFunc { } isProxyUser = true user.TouchFirstUse(db, pu.ID) - // HWID device registration (Happ / Remnawave-compatible headers). - info := hwid.ParseRequest(c.Request) - if err := hwid.Enforce(db, pu.ID, pu.HWIDLimit, info, c.Writer.Header()); err != nil { - if errors.Is(err, hwid.ErrMaxDevices) { - c.Header(hwid.HeaderMaxDevicesReached, "true") - if pu.HWIDLimit > 0 { - c.Header(hwid.HeaderActive, "true") - } - c.JSON(http.StatusForbidden, gin.H{"error": "hwid device limit reached"}) - return - } - if errors.Is(err, hwid.ErrRequired) { - c.Header(hwid.HeaderNotSupported, "true") - if pu.HWIDLimit > 0 { - c.Header(hwid.HeaderActive, "true") - } - c.JSON(http.StatusForbidden, gin.H{"error": "hwid required: enable device binding in a compatible client (x-hwid)"}) - return - } - // Unexpected errors: log only. Never 500 a subscription for HWID - // bookkeeping — hwid_limit=0 must always remain fetchable. - log.Printf("hwid enforce (ignored): %v", err) - } if err := subpull.Enforce(db, pu.ID, pu.SubPullLimit); err != nil { if errors.Is(err, subpull.ErrLimitReached) { c.Header("Retry-After", "3600") diff --git a/backend/internal/user/api.go b/backend/internal/user/api.go index 0e35da9..d616e27 100644 --- a/backend/internal/user/api.go +++ b/backend/internal/user/api.go @@ -10,7 +10,6 @@ import ( "github.com/gin-gonic/gin" "github.com/kazeyukiro/3m-ui/backend/internal/database/models" - "github.com/kazeyukiro/3m-ui/backend/internal/hwid" "gorm.io/gorm" ) @@ -46,9 +45,6 @@ func (h *Handler) RegisterRoutes(rg *gin.RouterGroup) { rg.GET("/:id/node-traffic", h.ListNodeTraffic) rg.GET("/:id/subscription", h.GetSubscription) rg.POST("/:id/subscription/rotate", h.RotateSubscription) - rg.GET("/:id/hwid-devices", h.ListHWIDDevices) - rg.DELETE("/:id/hwid-devices", h.ClearHWIDDevices) - rg.DELETE("/:id/hwid-devices/:deviceId", h.DeleteHWIDDevice) } func parseID(c *gin.Context) (uint, bool) { @@ -412,51 +408,7 @@ func (h *Handler) ListRemoteNodes(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"mirror_ids": ids}) } -func (h *Handler) ListHWIDDevices(c *gin.Context) { - id, ok := parseID(c) - if !ok { - return - } - if h.svc == nil || h.svc.DB() == nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "service unavailable"}) - return - } - rows, err := hwid.List(h.svc.DB(), id) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, gin.H{"items": rows}) -} -func (h *Handler) DeleteHWIDDevice(c *gin.Context) { - id, ok := parseID(c) - if !ok { - return - } - did, err := strconv.ParseUint(c.Param("deviceId"), 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid device id"}) - return - } - if err := hwid.Delete(h.svc.DB(), id, uint(did)); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, gin.H{"ok": true}) -} - -func (h *Handler) ClearHWIDDevices(c *gin.Context) { - id, ok := parseID(c) - if !ok { - return - } - if err := hwid.DeleteAll(h.svc.DB(), id); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, gin.H{"ok": true}) -} func (h *Handler) ListNodeTraffic(c *gin.Context) { id, ok := parseID(c) diff --git a/backend/internal/user/service.go b/backend/internal/user/service.go index de7065d..d0e624a 100644 --- a/backend/internal/user/service.go +++ b/backend/internal/user/service.go @@ -72,7 +72,6 @@ type CreateInput struct { UUID string `json:"uuid"` TrafficLimit int64 `json:"traffic_limit"` IPLimit int `json:"ip_limit"` - HWIDLimit int `json:"hwid_limit"` SubPullLimit int `json:"sub_pull_limit"` Remark string `json:"remark"` Group string `json:"group"` @@ -94,7 +93,6 @@ type UpdateInput struct { UUID string `json:"uuid"` TrafficLimit *int64 `json:"traffic_limit"` IPLimit *int `json:"ip_limit"` - HWIDLimit *int `json:"hwid_limit"` SubPullLimit *int `json:"sub_pull_limit"` Remark *string `json:"remark"` Group *string `json:"group"` diff --git a/backend/internal/user/service_crud.go b/backend/internal/user/service_crud.go index 9e59d51..7b06c8e 100644 --- a/backend/internal/user/service_crud.go +++ b/backend/internal/user/service_crud.go @@ -103,7 +103,6 @@ func (s *Service) Create(in CreateInput) (*models.ProxyUser, error) { UUID: uuid, TrafficLimit: in.TrafficLimit, IPLimit: max0(in.IPLimit), - HWIDLimit: max0(in.HWIDLimit), SubPullLimit: max0(in.SubPullLimit), Remark: strings.TrimSpace(in.Remark), Group: strings.TrimSpace(in.Group), @@ -170,8 +169,6 @@ func (s *Service) Update(id uint, in UpdateInput) (*models.ProxyUser, error) { if in.IPLimit != nil { u.IPLimit = max0(*in.IPLimit) } - if in.HWIDLimit != nil { - u.HWIDLimit = max0(*in.HWIDLimit) } if in.SubPullLimit != nil { u.SubPullLimit = max0(*in.SubPullLimit) diff --git a/backend/internal/user/service_util.go b/backend/internal/user/service_util.go index c799192..6383ec0 100644 --- a/backend/internal/user/service_util.go +++ b/backend/internal/user/service_util.go @@ -31,7 +31,6 @@ type SafeUser struct { Enabled bool `json:"enabled"` Blocked bool `json:"blocked"` IPLimit int `json:"ip_limit"` - HWIDLimit int `json:"hwid_limit"` SubPullLimit int `json:"sub_pull_limit"` Remark string `json:"remark"` Group string `json:"group"` @@ -63,7 +62,6 @@ func ToSafeUser(u *models.ProxyUser) SafeUser { Enabled: u.Enabled, Blocked: !IsCredentialActive(*u), IPLimit: u.IPLimit, - HWIDLimit: u.HWIDLimit, SubPullLimit: u.SubPullLimit, Remark: u.Remark, Group: u.Group, diff --git a/docs/users-limits.md b/docs/users-limits.md index 2e906b4..014e44d 100644 --- a/docs/users-limits.md +++ b/docs/users-limits.md @@ -1,4 +1,4 @@ -# User limits: IP, HWID, subscription pulls +# User limits: IP and subscription pulls Panel fields on each **ProxyUser** (Users page / API). @@ -11,17 +11,6 @@ Panel fields on each **ProxyUser** (Users page / API). **How it works:** the traffic collector polls Mihomo connections about every **5 seconds**. Connections that cannot be attributed to a user are ignored. When a user exceeds `N` source IPs, excess IPs’ connections are closed (not blocked at handshake time). -## HWID limit (`hwid_limit`) - -Compatible with Happ / Remnawave-style headers: `x-hwid`, `x-device-os`, `x-ver-os`, `x-device-model`. - -| Value | Meaning | -|-------|---------| -| `0` | No device cap (devices still recorded when `x-hwid` is sent; DB errors never block the subscription) | -| `N ≥ 1` | At most **N** devices; **subscription requests without a valid `x-hwid` are rejected (403)** | - -Device list: `GET /api/v1/users/{id}/hwid-devices`. Delete one or clear all via the Users UI / DELETE APIs. - ## Subscription pull limit (`sub_pull_limit`) | Value | Meaning | @@ -35,30 +24,9 @@ HTML subscription pages and client downloads both count. ## Related APIs -- User CRUD: `ip_limit`, `hwid_limit`, `sub_pull_limit` on create/update +- User CRUD: `ip_limit`, `sub_pull_limit` on create/update - Subscription: public `/api/v1/client/sub/{token}` (and aliases) - -## Why is the device list empty? - -HWID is recorded **only when a client fetches the user subscription URL** and sends a device id header (typically `x-hwid`). - -| Client | Usually sends `x-hwid`? | -|--------|-------------------------| -| Happ | Yes (on subscription update) | -| v2RayTun | Yes (when HWID feature enabled) | -| FlClashX / some Remnawave forks | Yes | -| Clash Meta / mihomo / v2rayNG / NekoBox (stock) | **No** — will never appear in device list | - -Also check: - -1. You opened the **user** subscription link (`/api/v1/client/sub/{token}`), not an access-token or single-node URI. -2. After enabling HWID support in the app, **force update** the subscription (not only connect). -3. Reverse proxies must forward `x-hwid` / `x-device-os` / `x-device-model` (do not strip unknown headers). -4. Panel log lines `hwid: registered` / `hwid: seen` confirm the server received the header. - - - ## Related - [Per-node traffic & multiplier](node-traffic.md) diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index e74fb29..0ad8c98 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -15,7 +15,6 @@ export interface ProxyUser { expire_time?: string; blocked?: boolean; ip_limit?: number; - hwid_limit?: number; sub_pull_limit?: number; remark?: string; group?: string; @@ -88,23 +87,6 @@ export const exportUserLinks = (params?: { q?: string; group?: string }) => .then((r) => r.data); -export interface HWIDDevice { - id: number; - proxy_user_id: number; - hwid: string; - device_os?: string; - ver_os?: string; - device_model?: string; - user_agent?: string; - last_seen_at?: string; -} - -export const fetchUserHWIDDevices = (userId: number) => - client.get<{ items: HWIDDevice[] }>(`/users/${userId}/hwid-devices`).then((r) => r.data.items || []); -export const deleteUserHWIDDevice = (userId: number, deviceId: number) => - client.delete(`/users/${userId}/hwid-devices/${deviceId}`); -export const clearUserHWIDDevices = (userId: number) => - client.delete(`/users/${userId}/hwid-devices`); export interface UserNodeTrafficItem { diff --git a/frontend/src/pages/Users.tsx b/frontend/src/pages/Users.tsx index 6708916..edd3d08 100644 --- a/frontend/src/pages/Users.tsx +++ b/frontend/src/pages/Users.tsx @@ -3,12 +3,12 @@ import { Card, Table, Button, Space, Modal, Form, Input, Switch, message, Popconfirm, Select, Tag, InputNumber, DatePicker, Progress, Tooltip, Dropdown, Checkbox, Spin, } from 'antd'; -import { PlusOutlined, DeleteOutlined, EditOutlined, LinkOutlined, ClearOutlined, ShareAltOutlined, CopyOutlined, MoreOutlined, TabletOutlined, FundOutlined } from '@ant-design/icons'; +import { PlusOutlined, DeleteOutlined, EditOutlined, LinkOutlined, ClearOutlined, ShareAltOutlined, CopyOutlined, MoreOutlined, FundOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import { fetchUsers, createUser, updateUser, deleteUser, resetUserTraffic, deleteDepletedUsers, batchUsers, fetchUserNodes, bindUserNodes, fetchUserRemoteNodes, bindUserRemoteNodes, ProxyUser, - fetchUserHWIDDevices, deleteUserHWIDDevice, clearUserHWIDDevices, fetchUserNodeTraffic, type HWIDDevice, type UserNodeTrafficItem, + fetchUserNodeTraffic, type UserNodeTrafficItem, } from '../api/users'; import { fetchListeners, Listener } from '../api/nodes'; import { fetchMirroredNodes, RemoteNodeMirror } from '../api/cluster'; @@ -28,10 +28,6 @@ const Users: React.FC = () => { const [modalOpen, setModalOpen] = useState(false); const [submitting, setSubmitting] = useState(false); const submittingRef = useRef(false); - const [hwidOpen, setHwidOpen] = useState(false); - const [hwidUser, setHwidUser] = useState(null); - const [hwidRows, setHwidRows] = useState([]); - const [hwidLoading, setHwidLoading] = useState(false); const [nodeTrafficOpen, setNodeTrafficOpen] = useState(false); const [nodeTrafficUser, setNodeTrafficUser] = useState(null); const [nodeTrafficRows, setNodeTrafficRows] = useState([]); @@ -86,8 +82,7 @@ const Users: React.FC = () => { enabled: !!values.enabled, traffic_limit: trafficGB && trafficGB > 0 ? Math.round(Number(trafficGB) * 1024 * 1024 * 1024) : 0, ip_limit: values.ip_limit != null ? Number(values.ip_limit) : 0, - hwid_limit: values.hwid_limit != null ? Number(values.hwid_limit) : 0, - sub_pull_limit: values.sub_pull_limit != null ? Number(values.sub_pull_limit) : 0, + sub_pull_limit: values.sub_pull_limit != null ? Number(values.sub_pull_limit) : 0, remark: values.remark || '', group: values.group || '', tags: values.tags || '', @@ -143,18 +138,6 @@ const Users: React.FC = () => { } }; - const openHwid = async (record: ProxyUser) => { - setHwidUser(record); - setHwidOpen(true); - setHwidLoading(true); - try { - setHwidRows(await fetchUserHWIDDevices(record.id)); - } catch (e: any) { - message.error(e?.message || t('common.error')); - } finally { - setHwidLoading(false); - } - }; const openNodeTraffic = async (record: ProxyUser) => { setNodeTrafficUser(record); @@ -188,8 +171,7 @@ const Users: React.FC = () => { expire_time: exp, password: undefined, ip_limit: record.ip_limit || 0, - hwid_limit: record.hwid_limit || 0, - sub_pull_limit: record.sub_pull_limit || 0, + sub_pull_limit: record.sub_pull_limit || 0, remark: record.remark || '', group: record.group || '', tags: record.tags || '', @@ -363,7 +345,6 @@ const Users: React.FC = () => { , - , - ]} - width={isMobile ? '100%' : 720} - > - v ? new Date(v).toLocaleString() : '-' }, - { title: t('common.actions'), key: 'a', width: 90, render: (_: any, r: HWIDDevice) => ( - - )}, - ]} - /> -