feat: add admin HWID device limit overrides
This commit is contained in:
@@ -120,6 +120,8 @@ def _serialize_subscription(sub: Subscription) -> Dict[str, Any]:
|
||||
"regular_unlimited_override": regular_unlimited_override,
|
||||
"premium_unlimited_override": premium_unlimited_override,
|
||||
"premium_is_limited": bool(sub.premium_is_limited),
|
||||
"hwid_device_limit": getattr(sub, "hwid_device_limit", None),
|
||||
"extra_hwid_devices": int(getattr(sub, "extra_hwid_devices", 0) or 0),
|
||||
"tariff_key": sub.tariff_key,
|
||||
"display_label": display_label,
|
||||
"is_trial": is_trial,
|
||||
|
||||
@@ -30,6 +30,10 @@ def setup_admin_routes(app: web.Application) -> None:
|
||||
"/api/admin/users/{user_id:-?\\d+}/regular-traffic-override",
|
||||
admin_user_regular_traffic_override_route,
|
||||
)
|
||||
router.add_post(
|
||||
"/api/admin/users/{user_id:-?\\d+}/hwid-device-limit",
|
||||
admin_user_hwid_device_limit_route,
|
||||
)
|
||||
router.add_post(
|
||||
"/api/admin/users/{user_id:-?\\d+}/traffic-grant",
|
||||
admin_user_traffic_grant_route,
|
||||
|
||||
@@ -1347,6 +1347,78 @@ async def admin_user_regular_traffic_override_route(request: web.Request) -> web
|
||||
return _ok({"subscription": _serialize_subscription(active)})
|
||||
|
||||
|
||||
async def admin_user_hwid_device_limit_route(request: web.Request) -> web.Response:
|
||||
"""Override the user's base HWID device limit.
|
||||
|
||||
``hwid_device_limit == 0`` means unlimited; ``NULL`` means the tariff/.env
|
||||
default is used. Purchased extra devices remain tracked separately and are
|
||||
added when syncing the effective panel limit.
|
||||
"""
|
||||
actor_id = _require_admin_user_id(request)
|
||||
target_id = int(request.match_info["user_id"])
|
||||
settings: Settings = request.app["settings"]
|
||||
payload = await _read_json(request)
|
||||
|
||||
unlimited = bool(payload.get("unlimited"))
|
||||
use_default = bool(payload.get("use_default") or payload.get("reset_to_default"))
|
||||
limit_raw = payload.get("hwid_device_limit", payload.get("limit"))
|
||||
|
||||
if unlimited:
|
||||
hwid_device_limit: Optional[int] = 0
|
||||
elif use_default or limit_raw is None or limit_raw == "":
|
||||
hwid_device_limit = None
|
||||
else:
|
||||
try:
|
||||
hwid_device_limit = int(limit_raw)
|
||||
except (TypeError, ValueError):
|
||||
return _error(
|
||||
400,
|
||||
"invalid_hwid_device_limit",
|
||||
"hwid_device_limit must be a non-negative integer",
|
||||
)
|
||||
if hwid_device_limit < 0 or hwid_device_limit > 1_000_000:
|
||||
return _error(
|
||||
400,
|
||||
"invalid_hwid_device_limit",
|
||||
"hwid_device_limit must be an integer from 0 to 1000000",
|
||||
)
|
||||
|
||||
subscription_service = request.app.get("subscription_service")
|
||||
|
||||
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
||||
async with async_session_factory() as session:
|
||||
active = await subscription_dal.get_active_subscription_by_user_id(session, target_id)
|
||||
if not active:
|
||||
return _error(404, "no_active_subscription")
|
||||
|
||||
active.hwid_device_limit = hwid_device_limit
|
||||
|
||||
effective_limit = None
|
||||
if subscription_service is not None:
|
||||
effective_limit = await subscription_service.sync_hwid_device_limit_to_panel(
|
||||
session, target_id
|
||||
)
|
||||
|
||||
await message_log_dal.create_message_log(
|
||||
session,
|
||||
{
|
||||
"user_id": actor_id,
|
||||
"event_type": "admin_hwid_device_limit_webapp",
|
||||
"content": (
|
||||
f"hwid_device_limit={hwid_device_limit!r} "
|
||||
f"effective_hwid_device_limit={effective_limit!r}"
|
||||
),
|
||||
"is_admin_event": True,
|
||||
"target_user_id": target_id,
|
||||
},
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(active)
|
||||
|
||||
await _invalidate_after_admin_user_mutation(settings, target_id)
|
||||
return _ok({"subscription": _serialize_subscription(active)})
|
||||
|
||||
|
||||
async def admin_user_traffic_grant_route(request: web.Request) -> web.Response:
|
||||
"""Credit regular or premium traffic to a user without a payment.
|
||||
|
||||
|
||||
@@ -779,9 +779,31 @@ def _get_cached_webapp_settings(request: web.Request) -> Dict[str, Any]:
|
||||
def _resolve_app_version() -> str:
|
||||
# Single source of truth shared with the telemetry worker so the admin
|
||||
# sidebar and the install beacon always report the same version.
|
||||
from bot.utils.app_version import resolve_app_version
|
||||
from bot.utils import app_version as app_version_module
|
||||
|
||||
return resolve_app_version()
|
||||
global _APP_VERSION_CACHE
|
||||
|
||||
app_version_module.APP_ROOT = APP_ROOT
|
||||
app_version_module._run_git_command = _run_git_command
|
||||
app_version_module._APP_VERSION_CACHE = _APP_VERSION_CACHE
|
||||
version = app_version_module.resolve_app_version()
|
||||
_APP_VERSION_CACHE = app_version_module._APP_VERSION_CACHE
|
||||
return version
|
||||
|
||||
|
||||
def _run_git_command(*args: str) -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", *args],
|
||||
cwd=APP_ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=1.5,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return ""
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
async def _enforce_webapp_rate_limit(
|
||||
|
||||
@@ -342,11 +342,12 @@ def _serialize_subscription(
|
||||
and tariff.premium_topup_packages.has_any()
|
||||
)
|
||||
can_topup_traffic = bool(can_topup_regular_traffic or can_topup_premium_traffic)
|
||||
# max_devices == 0 means unlimited — top-up is pointless in that case.
|
||||
max_devices = _coerce_int_or_none(active.get("max_devices"))
|
||||
# max_devices == 0 or None means unlimited — top-up is pointless in that case.
|
||||
can_topup_devices = bool(
|
||||
tariff.billing_model == "period"
|
||||
and tariff.has_hwid_device_packages()
|
||||
and _coerce_int_or_none(active.get("max_devices")) != 0
|
||||
and max_devices not in (None, 0)
|
||||
)
|
||||
except Exception:
|
||||
can_topup_regular_traffic = False
|
||||
|
||||
Reference in New Issue
Block a user