Skip to content

feat(panel): HWID device limit (subscription, admin API, dashboard)#429

Open
Nemu-x wants to merge 14 commits intoPasarGuard:mainfrom
Nemu-x:main
Open

feat(panel): HWID device limit (subscription, admin API, dashboard)#429
Nemu-x wants to merge 14 commits intoPasarGuard:mainfrom
Nemu-x:main

Conversation

@Nemu-x
Copy link
Copy Markdown
Contributor

@Nemu-x Nemu-x commented May 1, 2026

📌 Summary

Adds an optional HWID-based device limit for PasarGuard (PGpanel), inspired by common subscription protection patterns.

  • Disabled by default
  • When disabled → no behavior changes
  • Clients do not need to send additional headers

📚 Documentation

  • docs/HWID_DEVELOPERS.md
    → Full operator & developer guide:

    • Subscription routes
    • Request/response headers
    • Settings model
    • Admin APIs
    • Concurrency notes
    • Smoke-test hints
  • docs/hwid_subscription_smoke.sh
    → Example curl flow:

    • Token login
    • Admin HWID endpoints
      (optional local smoke test)

⚙️ Behaviour (when enabled)

📥 Subscription Requests

  • Works with existing token-based subscription URLs
  • Headers are read case-insensitively

Key headers:

  • x-hwidrequired when enforcement applies
  • Optional:
    • x-device-os
    • x-ver-os
    • x-device-model
    • User-Agent

❌ Denials

  • Uses existing 404-style “invalid subscription” behavior
  • Includes diagnostic headers:
Header Description
x-hwid-active HWID enforcement active
x-hwid-not-supported Client does not support HWID
x-hwid-max-devices-reached Limit exceeded
x-hwid-limit Current device limit

🔒 Device Registration Logic

  • Not count → insert
  • Uses:
    • DB transaction
    • User row lock

➡ Prevents race conditions where parallel requests exceed limits


💾 Storage Model

  • HWID is stored as:
    HMAC-SHA256(HWID, HWID_HASH_SALT)
    
  • ❗ Raw x-hwid is never stored
  • 🔁 Changing HWID_HASH_SALT:
    • Invalidates all existing device records

⚙️ Settings & Data Model

🌍 Global Settings

Dashboard:

Settings → Subscriptions
  • Enable HWID enforcement
  • Set fallback device limit

👤 Per-user Overrides

Available in user edit / Groups tab:

  • Custom device limit
  • Skip HWID enforcement (bypass)

🗄️ Database

  • New table:
    • HWIDUserDevice (or equivalent)
  • Includes:
    • Alembic migration

Run migrations on deploy


🔧 Admin API

Endpoint:

/api/hwid/devices

Supports:

  • List devices
  • Stats
  • Add device
  • Delete device
  • Delete all devices
  • Per-user device list
  • Top users

➡ Uses existing admin auth patterns


🖥️ Dashboard Changes

Settings

  • Settings → Subscriptions

    • Enable HWID
    • Set fallback limit
  • Settings → HWID

    • Inspector
    • Search
    • Delete
    • Manual registration

Users

  • HWID section in user edit:
    • Limit override
    • Bypass toggle
    • Device list actions

🧪 Tests

Covered scenarios:

  • Enabled / disabled flows
  • Device limits
  • Per-user overrides
  • Admin API operations
  • Concurrent registrations

📁 See:

tests/api/test_user.py

🚀 Deploy / Ops Notes

  • 🔐 Set stable environment variable:

    HWID_HASH_SALT
    

    before production use

  • 🛠 Apply DB migration:

    alembic upgrade head
    

    (or your standard deploy pipeline)


✅ Result

  • Fully backward compatible
  • Secure (no raw HWID storage)
  • Race-condition safe
  • Flexible (global + per-user control)

Summary by CodeRabbit

  • New Features

    • HWID-based device limiting with per-user overrides and bypass; subscription endpoints accept device-identifying headers and surface diagnostic/active headers.
  • Admin APIs

    • List devices, stats, top users; add device, delete single/all devices for a user via new endpoints.
  • Dashboard

    • HWID management page, user-edit UI and settings to view/register/remove devices and set per-user limits.
  • Tests

    • Comprehensive tests including concurrent access and race-condition checks.
  • Documentation

    • Developer guide and smoke-test script.
  • Configuration

    • New HWID hash salt env var with production guard.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds HWID device-limiting: DB schema and models, CRUD and enforcement with HMAC-SHA256 hashing, subscription flow integration returning diagnostic headers on denial, admin HWID APIs and dashboard UI, tests, docs, and a runtime HWID hash salt production guard.

Changes

Cohort / File(s) Summary
Core DB & CRUD
app/db/crud/hwid.py, app/db/models.py, app/db/migrations/versions/b2c3d4e5f6a7_add_hwid_device_limit.py
New hwid_user_devices table and HWIDUserDevice model; User adds hwid_device_limit and hwid_limit_disabled; HWID normalization and HMAC-SHA256 hashing; CRUD helpers: enforce/upsert/list/delete/stats/top-users with transactional device-limit enforcement.
Backend Operations & Routing
app/operation/hwid.py, app/operation/subscription.py, app/db/crud/user.py, app/routers/hwid.py, app/routers/subscription.py, app/routers/__init__.py
New HWIDOperation; subscription flow now accepts HWID/device headers, enforces per-user/subscription limits, emits diagnostic headers on denial; admin HWID endpoints added and router registered; modify_user handles HWID fields.
API Models & Config
app/models/hwid.py, app/models/user.py, app/models/settings.py, config.py
Pydantic request/response models for HWID APIs; user Pydantic model adds hwid_device_limit and hwid_limit_disabled; subscription settings add enable flag and fallback limit; new HWID_HASH_SALT and ENV production guard.
Frontend UI & i18n
dashboard/public/statics/locales/en.json, .../fa.json, .../ru.json, dashboard/src/...
Added localization keys for HWID UI and subscription controls; dashboard UI additions for HWID management and per-user settings.
Frontend wiring & components
dashboard/src/components/..., dashboard/src/pages/..., dashboard/src/router.tsx, dashboard/src/service/api/index.ts
New HWID settings page and router entry; user modal device inspector; subscription settings controls and schema updates; API client types extended; queries/mutations for device CRUD.
Docs, Scripts & Tests
docs/HWID_DEVELOPERS.md, docs/hwid_subscription_smoke.sh, tests/api/conftest.py, tests/api/test_user.py
Developer documentation and smoke script; tests covering enforcement, concurrency, admin listing/deletion; test fixtures adjusted to use test DB provider.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant API as Subscription API
    participant SubOp as SubscriptionOp
    participant HWIDOp as HWIDOp
    participant DB as Database

    Client->>API: GET subscription (X-HWID, headers...)
    API->>SubOp: user_subscription_with_client_type(hwid,...)
    SubOp->>HWIDOp: enforce_subscription_hwid(user, hwid, metadata...)
    HWIDOp->>DB: BEGIN TRANSACTION / LOCK user row
    DB-->>HWIDOp: locked
    HWIDOp->>DB: SELECT hwid_user_devices WHERE user_id AND hwid_hash
    DB-->>HWIDOp: existing? (row/none)
    alt existing
        HWIDOp->>DB: UPDATE last_seen_at, metadata
        DB-->>HWIDOp: updated
    else not existing
        HWIDOp->>DB: SELECT COUNT(*) WHERE user_id
        DB-->>HWIDOp: count
        alt count < limit
            HWIDOp->>DB: INSERT new hwid_user_devices
            DB-->>HWIDOp: created
        else limit reached
            HWIDOp-->>SubOp: HWIDDecision(allowed=false,max_devices_reached=true)
        end
    end
    HWIDOp->>DB: COMMIT
    HWIDOp-->>SubOp: HWIDDecision(allowed=true/false)
    SubOp-->>API: decision
    alt allowed
        API-->>Client: 200 subscription (+ active header if enabled)
    else denied
        API-->>Client: 404 + diagnostic headers (missing_hwid / limit reached)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 I hopped a hash across the ground,
I salted bytes where logs abound,
I counted paws and kept the gate,
I marked last-seen and checked the slate,
Now devices rest in tidy rows — hop safe, little code!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(panel): HWID device limit (subscription, admin API, dashboard)' clearly summarizes the main addition: an HWID-based device limiting feature across subscription handling, admin API, and dashboard UI components, which aligns with the substantial changeset adding HWID enforcement, CRUD operations, and UI management.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (6)
app/routers/hwid.py (1)

24-26: Add Query parameter constraints to offset/limit in lines 24-26 and line 71 to prevent negative/excessively large values.

Import Query from fastapi and apply bounds: offset with ge=0, limit with ge=1, le=200. This prevents invalid pagination inputs from reaching the database.

🔧 Suggested patch
-from fastapi import APIRouter, Depends
+from fastapi import APIRouter, Depends, Query
@@
 async def get_hwid_devices(
-    offset: int = 0,
-    limit: int = 50,
+    offset: int = Query(default=0, ge=0),
+    limit: int = Query(default=50, ge=1, le=200),
@@
-async def get_top_users(limit: int = 20, db: AsyncSession = Depends(get_db), admin=Depends(get_current)):
+async def get_top_users(
+    limit: int = Query(default=20, ge=1, le=200), db: AsyncSession = Depends(get_db), admin=Depends(get_current)
+):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routers/hwid.py` around lines 24 - 26, Add validation to the pagination
query params by importing Query from fastapi and changing the offset and limit
parameters to use Query defaults and bounds (offset: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200)) wherever they appear (the handler function
parameters that declare offset, limit, user_id around lines 24-26 and the
similar parameter set at line 71); ensure you add "from fastapi import Query" to
the imports so negative or excessively large values are rejected before reaching
the database.
app/models/hwid.py (1)

33-49: Add request-field constraints for stronger API contracts (lines 33-49).

user_id, hwid, and hwid_hash lack validation constraints. Adding bounds and length checks here prevents invalid payloads early and aligns with the codebase's existing Field validation patterns (e.g., data_limit: int = Field(ge=0), name: str = Field(min_length=3, max_length=64)). Field is already imported.

🔧 Suggested patch
 class HWIDDeleteRequest(BaseModel):
-    user_id: int
-    hwid_hash: str
+    user_id: int = Field(ge=1)
+    hwid_hash: str = Field(min_length=1, max_length=128)
@@
 class HWIDDeleteAllRequest(BaseModel):
-    user_id: int
+    user_id: int = Field(ge=1)
@@
 class HWIDAddRequest(BaseModel):
-    user_id: int
-    hwid: str
-    device_os: str | None = None
-    os_version: str | None = None
-    device_model: str | None = None
-    user_agent: str | None = None
-    request_ip: str | None = None
+    user_id: int = Field(ge=1)
+    hwid: str = Field(min_length=1, max_length=512)
+    device_os: str | None = Field(default=None, max_length=64)
+    os_version: str | None = Field(default=None, max_length=64)
+    device_model: str | None = Field(default=None, max_length=128)
+    user_agent: str | None = Field(default=None, max_length=512)
+    request_ip: str | None = Field(default=None, max_length=64)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/hwid.py` around lines 33 - 49, Add Pydantic Field validations to
strengthen the API contract: on HWIDDeleteRequest and HWIDDeleteAllRequest set
user_id: int = Field(ge=1) to require positive IDs; on HWIDDeleteRequest set
hwid_hash: str = Field(min_length=8, max_length=256) (or appropriate bounds) and
on HWIDAddRequest set user_id: int = Field(ge=1) and hwid: str =
Field(min_length=8, max_length=256); also optionally add reasonable max_length
constraints for device_os, os_version, device_model, user_agent and request_ip
to match project patterns — use the already-imported Field and update the
HWIDDeleteRequest, HWIDDeleteAllRequest, and HWIDAddRequest class attribute
definitions accordingly.
app/operation/subscription.py (2)

390-408: ⚖️ Poor tradeoff

Duplicated HWID enforcement logic.

The HWID enforcement block (lines 390-408) is nearly identical to lines 265-285 in user_subscription. Consider extracting a helper method like _enforce_hwid_or_deny() that returns either None (allowed) or a Response (denial) to reduce duplication.

Example refactor
async def _enforce_hwid_or_deny(
    self,
    db: AsyncSession,
    db_user: User,
    sub_settings: SubSettings,
    hwid: str | None,
    device_os: str | None,
    os_version: str | None,
    device_model: str | None,
    user_agent: str | None,
    request_ip: str | None,
) -> Response | None:
    hwid_operator = HWIDOperation(operator_type=self.operator_type)
    hwid_decision = await hwid_operator.enforce_subscription_hwid(
        db,
        user=db_user,
        subscription_settings=sub_settings,
        hwid=hwid,
        device_os=device_os,
        os_version=os_version,
        device_model=device_model,
        user_agent=user_agent,
        request_ip=request_ip,
    )
    if not hwid_decision.allowed:
        headers = (
            self._HWID_NOT_SUPPORTED_HEADERS
            if hwid_decision.missing_hwid
            else self._HWID_LIMIT_REACHED_HEADERS if hwid_decision.max_devices_reached else {}
        )
        return Response(status_code=404, headers=headers)
    return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/operation/subscription.py` around lines 390 - 408, The HWID enforcement
logic is duplicated between the block in subscription.py (current snippet) and
the user_subscription block; extract that logic into a private helper method
(e.g., _enforce_hwid_or_deny) that accepts (db: AsyncSession, db_user: User,
sub_settings: SubSettings, hwid: str | None, device_os: str | None, os_version:
str | None, device_model: str | None, user_agent: str | None, request_ip: str |
None) and returns Response | None; inside it instantiate
HWIDOperation(operator_type=self.operator_type), call
enforce_subscription_hwid(...) with the same args, build headers the same way
(use self._HWID_NOT_SUPPORTED_HEADERS / self._HWID_LIMIT_REACHED_HEADERS) and
return Response(status_code=404, headers=...) when not allowed or None when
allowed; then replace the duplicated blocks in both user_subscription and the
current function with a single call to _enforce_hwid_or_deny and immediately
return that Response if not None.

278-285: 💤 Low value

Consider logging or raising for unexpected denial state.

The else {} branch (line 282-283) handles a case where allowed=False but neither missing_hwid nor max_devices_reached is true. While defensive, this could silently mask unexpected denial reasons. Consider adding a log statement for debugging unexpected states.

Suggested improvement
         if not hwid_decision.allowed:
             headers = (
                 self._HWID_NOT_SUPPORTED_HEADERS
                 if hwid_decision.missing_hwid
                 else self._HWID_LIMIT_REACHED_HEADERS if hwid_decision.max_devices_reached else {}
             )
+            # Log unexpected denial state for debugging
+            if not hwid_decision.missing_hwid and not hwid_decision.max_devices_reached:
+                # Unexpected denial reason - consider logging
+                pass
             # Preserve existing safe denial behavior for invalid/missing subscription-like requests.
             return Response(status_code=404, headers=headers)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/operation/subscription.py` around lines 278 - 285, When
hwid_decision.allowed is False but neither hwid_decision.missing_hwid nor
hwid_decision.max_devices_reached is true, add a defensive log (e.g.
self._logger.warning or error) before returning the 404 so unexpected denial
reasons are visible; include the hwid_decision object (or its relevant fields),
any request/subscription identifiers available in the surrounding method, and
the chosen headers (_HWID_NOT_SUPPORTED_HEADERS/_HWID_LIMIT_REACHED_HEADERS) to
aid debugging — optionally consider escalating by raising an exception if this
state should never occur.
app/db/crud/hwid.py (2)

22-35: 💤 Low value

Minor redundancy: hash_hwid normalizes twice.

hash_hwid(hwid) calls normalize_hwid(hwid) internally (line 33), but callers like enforce_hwid_device_limit (line 64) and add_hwid_device (line 158) already pass a normalized HWID. This is harmless but slightly inefficient.

Consider removing the internal normalization since callers are responsible for it:

♻️ Suggested simplification
 def hash_hwid(hwid: str) -> str:
-    normalized = normalize_hwid(hwid)
-    digest = hmac.new(HWID_HASH_SALT.encode("utf-8"), normalized.encode("utf-8"), hashlib.sha256).hexdigest()
+    digest = hmac.new(HWID_HASH_SALT.encode("utf-8"), hwid.encode("utf-8"), hashlib.sha256).hexdigest()
     return digest
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/db/crud/hwid.py` around lines 22 - 35, hash_hwid currently calls
normalize_hwid internally which is redundant because callers like
enforce_hwid_device_limit and add_hwid_device already supply a normalized HWID;
remove the internal normalization in hash_hwid (replace the call to
normalize_hwid(hwid) with using the hwid parameter directly), update hash_hwid's
docstring or add a short comment stating it expects a normalized HWID, and
verify callers (enforce_hwid_device_limit, add_hwid_device) continue to pass
normalized values.

220-229: ⚡ Quick win

Direct commit() is inconsistent with other methods and may cause issues if called within a transaction.

delete_hwid_device and delete_all_hwid_devices call db.commit() directly, whereas add_hwid_device and enforce_hwid_device_limit use the begin_nested()/begin() pattern. If a caller wraps these delete calls in a transaction, the inner commit() would commit the outer transaction prematurely.

Consider aligning with the transaction-safe pattern used elsewhere:

♻️ Suggested fix for transaction safety
 async def delete_hwid_device(db: AsyncSession, *, user_id: int, hwid_hash: str) -> int:
+    tx_ctx = db.begin_nested() if db.in_transaction() else db.begin()
+    async with tx_ctx:
-    result = await db.execute(delete(HWIDUserDevice).where(HWIDUserDevice.user_id == user_id, HWIDUserDevice.hwid_hash == hwid_hash))
-    await db.commit()
+        result = await db.execute(delete(HWIDUserDevice).where(HWIDUserDevice.user_id == user_id, HWIDUserDevice.hwid_hash == hwid_hash))
     return int(result.rowcount or 0)


 async def delete_all_hwid_devices(db: AsyncSession, *, user_id: int) -> int:
+    tx_ctx = db.begin_nested() if db.in_transaction() else db.begin()
+    async with tx_ctx:
-    result = await db.execute(delete(HWIDUserDevice).where(HWIDUserDevice.user_id == user_id))
-    await db.commit()
+        result = await db.execute(delete(HWIDUserDevice).where(HWIDUserDevice.user_id == user_id))
     return int(result.rowcount or 0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/db/crud/hwid.py` around lines 220 - 229, The two delete helpers
delete_hwid_device and delete_all_hwid_devices call db.commit() directly which
can prematurely commit an outer transaction; change them to use the AsyncSession
transaction context like other functions (e.g. async with db.begin() or async
with db.begin_nested() as appropriate) and execute the delete inside that
context, remove the direct db.commit() call, capture the result from
db.execute(...) and return int(result.rowcount or 0) after the context completes
so transaction semantics match add_hwid_device/enforce_hwid_device_limit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/db/migrations/versions/b2c3d4e5f6a7_add_hwid_device_limit.py`:
- Around line 22-23: Add a DB-level CHECK to prevent negative values for
hwid_device_limit: update the migration that adds the hwid_device_limit column
so the column is created with a non-negative constraint (e.g., include
sa.CheckConstraint("hwid_device_limit >= 0") or call
batch_op.create_check_constraint with a name like
"ck_users_hwid_device_limit_nonnegative") referencing the hwid_device_limit
column; keep the existing hwid_limit_disabled addition unchanged. Ensure the
constraint name is unique and compatible with batch_op usage so the DB enforces
hwid_device_limit >= 0.

In `@config.py`:
- Line 71: Replace the predictable default salt by making HWID_HASH_SALT
required and fail fast in production: change HWID_HASH_SALT to not silently
default to "pasarguard-hwid" (use config("HWID_HASH_SALT", default=None) or
remove the default) and add a startup check that if running in production (e.g.,
ENV=="production" or not DEBUG) and HWID_HASH_SALT is missing or equals
"pasarguard-hwid" then raise an explicit error (RuntimeError) so the process
fails to start; reference the HWID_HASH_SALT symbol for the check and the
production flag you already use in configuration.

In `@dashboard/public/statics/locales/ru.json`:
- Line 370: The locale value for key "globalFallback" mixes English and Russian;
replace the current value ("Fallback лимит устройств") with a fully Russian
string—e.g., "Резервный лимит устройств"—so the JSON entry for globalFallback is
fully localized and consistent with other Russian translations.

In `@dashboard/src/components/dialogs/user-modal.tsx`:
- Around line 604-609: The hwidDevicesQuery currently calls fetcher with a path
parameter `/api/hwid/devices/${resolvedEditingUserId}` which doesn't match the
backend signature expecting a query parameter; update the queryFn in
hwidDevicesQuery to call fetcher with
`/api/hwid/devices?user_id=${resolvedEditingUserId}` (keep the same
resolvedEditingUserId, useQuery key, enabled condition and fetcher wrapper) so
the backend receives user_id as a query parameter; ensure no other behavior
changes to resolvedEditingUserId or enabled logic in the useQuery call.

In `@dashboard/src/components/forms/user-form.ts`:
- Line 61: The hwid_device_limit schema currently allows non-integer values;
update the validation in user-form.ts for hwid_device_limit (the zod schema
entry named hwid_device_limit) to enforce integers by adding .int() to the chain
(e.g., z.number().int().min(0).optional()) so only non-negative whole numbers
are accepted; adjust any related tests or UI error messages that expect
integer-only input if present.

In `@dashboard/src/components/users/users-table.tsx`:
- Around line 232-233: The object assignment is coercing a valid 0 to undefined
by using || undefined; update the expressions that set hwid_device_limit to use
nullish coalescing instead of || so a 0 value is preserved (replace occurrences
around hwid_device_limit in this file, including the second occurrence near
lines 258-259), and keep hwid_limit_disabled behavior as intended (it can remain
defaulting to false if needed); locate the assignments that reference
selectedUser.hwid_device_limit and change the fallback to use ?? rather than ||
undefined.

In `@dashboard/src/pages/_dashboard.settings.hwid.tsx`:
- Around line 263-265: The Delete-all button currently falls back to user_id: 0
when userIdFilter is non-numeric; change the onClick to use the same validation
pattern used elsewhere: parse the filter into a number (e.g. const parsed =
Number.parseInt(userIdFilter, 10)) and only call deleteAllMutation.mutate({
user_id: parsed }) when parsed is a valid positive integer (not NaN and > 0);
also keep the Button disabled when userIdFilter is empty or when the parsed
value is invalid so you never send user_id: 0. Reference: Button onClick,
userIdFilter and deleteAllMutation.
- Around line 240-246: The click handler currently coerces invalid user input
into user_id: 0 (via Number.parseInt(... ) || 0) which is unsafe; update the UI
to validate newDeviceUserId as a positive integer before calling
addDeviceMutation.mutate (and before enabling the button). Specifically, replace
the fallback parse logic in the onClick and adjust the disabled prop to require
a valid positive integer (use a helper or regex) so addDeviceMutation.mutate
only receives a validated Number(parseInt) value; if invalid, keep the button
disabled or show a validation message instead of sending 0.

---

Nitpick comments:
In `@app/db/crud/hwid.py`:
- Around line 22-35: hash_hwid currently calls normalize_hwid internally which
is redundant because callers like enforce_hwid_device_limit and add_hwid_device
already supply a normalized HWID; remove the internal normalization in hash_hwid
(replace the call to normalize_hwid(hwid) with using the hwid parameter
directly), update hash_hwid's docstring or add a short comment stating it
expects a normalized HWID, and verify callers (enforce_hwid_device_limit,
add_hwid_device) continue to pass normalized values.
- Around line 220-229: The two delete helpers delete_hwid_device and
delete_all_hwid_devices call db.commit() directly which can prematurely commit
an outer transaction; change them to use the AsyncSession transaction context
like other functions (e.g. async with db.begin() or async with db.begin_nested()
as appropriate) and execute the delete inside that context, remove the direct
db.commit() call, capture the result from db.execute(...) and return
int(result.rowcount or 0) after the context completes so transaction semantics
match add_hwid_device/enforce_hwid_device_limit.

In `@app/models/hwid.py`:
- Around line 33-49: Add Pydantic Field validations to strengthen the API
contract: on HWIDDeleteRequest and HWIDDeleteAllRequest set user_id: int =
Field(ge=1) to require positive IDs; on HWIDDeleteRequest set hwid_hash: str =
Field(min_length=8, max_length=256) (or appropriate bounds) and on
HWIDAddRequest set user_id: int = Field(ge=1) and hwid: str =
Field(min_length=8, max_length=256); also optionally add reasonable max_length
constraints for device_os, os_version, device_model, user_agent and request_ip
to match project patterns — use the already-imported Field and update the
HWIDDeleteRequest, HWIDDeleteAllRequest, and HWIDAddRequest class attribute
definitions accordingly.

In `@app/operation/subscription.py`:
- Around line 390-408: The HWID enforcement logic is duplicated between the
block in subscription.py (current snippet) and the user_subscription block;
extract that logic into a private helper method (e.g., _enforce_hwid_or_deny)
that accepts (db: AsyncSession, db_user: User, sub_settings: SubSettings, hwid:
str | None, device_os: str | None, os_version: str | None, device_model: str |
None, user_agent: str | None, request_ip: str | None) and returns Response |
None; inside it instantiate HWIDOperation(operator_type=self.operator_type),
call enforce_subscription_hwid(...) with the same args, build headers the same
way (use self._HWID_NOT_SUPPORTED_HEADERS / self._HWID_LIMIT_REACHED_HEADERS)
and return Response(status_code=404, headers=...) when not allowed or None when
allowed; then replace the duplicated blocks in both user_subscription and the
current function with a single call to _enforce_hwid_or_deny and immediately
return that Response if not None.
- Around line 278-285: When hwid_decision.allowed is False but neither
hwid_decision.missing_hwid nor hwid_decision.max_devices_reached is true, add a
defensive log (e.g. self._logger.warning or error) before returning the 404 so
unexpected denial reasons are visible; include the hwid_decision object (or its
relevant fields), any request/subscription identifiers available in the
surrounding method, and the chosen headers
(_HWID_NOT_SUPPORTED_HEADERS/_HWID_LIMIT_REACHED_HEADERS) to aid debugging —
optionally consider escalating by raising an exception if this state should
never occur.

In `@app/routers/hwid.py`:
- Around line 24-26: Add validation to the pagination query params by importing
Query from fastapi and changing the offset and limit parameters to use Query
defaults and bounds (offset: int = Query(0, ge=0), limit: int = Query(50, ge=1,
le=200)) wherever they appear (the handler function parameters that declare
offset, limit, user_id around lines 24-26 and the similar parameter set at line
71); ensure you add "from fastapi import Query" to the imports so negative or
excessively large values are rejected before reaching the database.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ef43ae33-e0a2-445e-ba5d-b58c3ff6402c

📥 Commits

Reviewing files that changed from the base of the PR and between e634249 and 19a5002.

📒 Files selected for processing (31)
  • app/db/crud/hwid.py
  • app/db/crud/user.py
  • app/db/migrations/versions/b2c3d4e5f6a7_add_hwid_device_limit.py
  • app/db/models.py
  • app/models/hwid.py
  • app/models/settings.py
  • app/models/user.py
  • app/operation/hwid.py
  • app/operation/subscription.py
  • app/routers/__init__.py
  • app/routers/hwid.py
  • app/routers/subscription.py
  • config.py
  • dashboard/public/statics/locales/en.json
  • dashboard/public/statics/locales/fa.json
  • dashboard/public/statics/locales/ru.json
  • dashboard/src/components/dialogs/user-modal.tsx
  • dashboard/src/components/forms/user-form.ts
  • dashboard/src/components/layout/sidebar.tsx
  • dashboard/src/components/subscriptions/subscription-general-settings-section.tsx
  • dashboard/src/components/subscriptions/subscription-settings-schema.ts
  • dashboard/src/components/users/users-table.tsx
  • dashboard/src/pages/_dashboard.settings.hwid.tsx
  • dashboard/src/pages/_dashboard.settings.subscriptions.tsx
  • dashboard/src/pages/_dashboard.settings.tsx
  • dashboard/src/router.tsx
  • dashboard/src/service/api/index.ts
  • docs/HWID_DEVELOPERS.md
  • docs/hwid_subscription_smoke.sh
  • tests/api/conftest.py
  • tests/api/test_user.py

Comment thread app/db/migrations/versions/b2c3d4e5f6a7_add_hwid_device_limit.py
Comment thread config.py Outdated
Comment thread dashboard/public/statics/locales/ru.json Outdated
Comment thread dashboard/src/components/dialogs/user-modal.tsx
Comment thread dashboard/src/components/forms/user-form.ts Outdated
Comment thread dashboard/src/components/users/users-table.tsx Outdated
Comment thread dashboard/src/pages/_dashboard.settings.hwid.tsx Outdated
Comment thread dashboard/src/pages/_dashboard.settings.hwid.tsx Outdated
"resetToDefaultSuccess": "Правила подписки сброшены к умолчанию",
"hwidEnabled": "Включить лимит устройств HWID",
"hwidEnabledDescription": "Требовать заголовок x-hwid при запросе подписки и применять лимиты устройств.",
"hwidFallbackLimit": "Резервный лимит HWID",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

лучше так и написать Fallback лимит - непонятно что за "резервный"

Comment thread dashboard/public/statics/locales/ru.json Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dashboard/src/components/dialogs/user-modal.tsx`:
- Around line 2440-2457: The onChange handler for the Input currently uses
Number.parseInt(... ) || 0 which silently truncates decimals and coerces invalid
input to 0; replace that parsing with a proper numeric validation: parse the raw
string with Number(raw), check Number.isFinite(parsed) and
Number.isInteger(parsed) and parsed >= 0, and only call field.onChange(parsed)
and handleFieldChange('hwid_device_limit', parsed) when those checks pass; if
raw === '' keep the existing undefined behavior, and for non-integer or
non-finite input set the field to undefined (or leave previous value) instead of
coercing to 0 so decimals/scientific notation and invalid input are not silently
truncated.

In `@dashboard/src/pages/_dashboard.settings.hwid.tsx`:
- Around line 83-89: The code uses Number.parseInt(userIdFilter, 10) (e.g., in
listQuery's queryFn calling fetcher<HWIDListResponse>) which accepts strings
with trailing non-digit chars or decimals; instead validate and strictly parse
the userIdFilter before sending it to the API: ensure userIdFilter matches a
whole-integer regex (e.g., /^\d+$/) and only then convert to a Number (or
parseInt) otherwise treat it as undefined or reject the action; apply the same
strict parsing/validation logic to the other HWID-related places referenced (the
registration and deletion handlers) so no malformed inputs like "12abc" or "1.5"
are converted into wrong user IDs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 706215ca-8646-48cb-b496-01a3bf147d26

📥 Commits

Reviewing files that changed from the base of the PR and between 19a5002 and 8e9fe63.

📒 Files selected for processing (13)
  • app/db/crud/hwid.py
  • app/db/migrations/versions/b2c3d4e5f6a7_add_hwid_device_limit.py
  • app/db/models.py
  • app/models/hwid.py
  • app/operation/subscription.py
  • app/routers/hwid.py
  • config.py
  • dashboard/public/statics/locales/ru.json
  • dashboard/src/components/dialogs/user-modal.tsx
  • dashboard/src/components/forms/user-form.ts
  • dashboard/src/components/users/users-table.tsx
  • dashboard/src/pages/_dashboard.settings.hwid.tsx
  • tests/api/conftest.py
✅ Files skipped from review due to trivial changes (3)
  • dashboard/src/components/forms/user-form.ts
  • app/db/migrations/versions/b2c3d4e5f6a7_add_hwid_device_limit.py
  • dashboard/public/statics/locales/ru.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • config.py
  • app/db/models.py
  • app/routers/hwid.py
  • app/models/hwid.py
  • tests/api/conftest.py

Comment thread dashboard/src/components/dialogs/user-modal.tsx
Comment thread dashboard/src/pages/_dashboard.settings.hwid.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/db/crud/hwid.py`:
- Around line 67-68: Remove manual commit calls that break outer transactions:
in add_hwid_device, delete_hwid_device, and delete_all_hwid_devices remove the
explicit await db.commit() calls (previously at lines referenced in the review)
and rely on the transaction context manager created via tx_ctx =
db.begin_nested() if db.in_transaction() else db.begin() to commit or rollback;
likewise, in enforce_hwid_device_limit remove the conditional await db.commit()
after the nested/regular transaction—do not add any new commits, simply let the
context manager finalize the savepoint/transaction and keep the existing
tx_ctx/with usage and error handling intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 36f491fe-86f9-4f6e-bbfe-c1de2334cd09

📥 Commits

Reviewing files that changed from the base of the PR and between 8e9fe63 and a97777e.

📒 Files selected for processing (2)
  • app/db/crud/hwid.py
  • app/db/migrations/versions/b2c3d4e5f6a7_add_hwid_device_limit.py

Comment thread app/db/crud/hwid.py Outdated
Handle MySQL unsigned FK compatibility, avoid committing inside outer transactions, and enforce strict integer parsing for HWID user/device IDs.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dashboard/src/components/dialogs/user-modal.tsx`:
- Around line 1524-1538: The delete button rendered as an icon-only Button
(component Button wrapping Trash2) is missing an accessible name; update the
Button that calls deleteHwidDeviceMutation.mutate to include an aria-label (or
title) that describes the action and the target (e.g., "Delete device
{item.hwid_hash}" or include user_id/item identifier) so screen readers can
identify which device will be deleted, ensuring the label is unique/contextual
per item.
- Around line 1485-1494: The "Delete all" Button immediately calls
deleteAllHwidDevicesMutation.mutate which can irreversibly remove all HWID
devices; update the Button handler to require an explicit confirmation step
(e.g., open a confirmation modal or call window.confirm) before invoking
deleteAllHwidDevicesMutation.mutate({ user_id: resolvedEditingUserId }), and
only proceed when the user confirms; keep the disabled logic using
hwidItems.length and deleteAllHwidDevicesMutation.isPending intact and ensure
the confirmation UI clearly indicates the destructive action.
- Around line 1497-1504: The current ternary rendering conflates a failed
hwidDevicesQuery with an empty hwidItems list; update the conditional in
user-modal.tsx to handle hwidDevicesQuery.isError before checking
hwidItems.length—render a clear error message (using
hwidDevicesQuery.error?.message when available) when isError, keep the existing
loading UI for hwidDevicesQuery.isLoading, and only show the "No HWID
devices..." empty state when the query succeeded and hwidItems.length === 0;
locate and modify the JSX branch that currently checks
hwidDevicesQuery.isLoading ? ... : hwidItems.length === 0 ? ... to include an
isError branch referencing hwidDevicesQuery and hwidItems.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6c1f3a9-ad78-4398-953b-1b7bfe09a6bb

📥 Commits

Reviewing files that changed from the base of the PR and between a97777e and afd7421.

📒 Files selected for processing (4)
  • app/db/crud/hwid.py
  • app/db/migrations/versions/b2c3d4e5f6a7_add_hwid_device_limit.py
  • dashboard/src/components/dialogs/user-modal.tsx
  • dashboard/src/pages/_dashboard.settings.hwid.tsx
✅ Files skipped from review due to trivial changes (1)
  • app/db/migrations/versions/b2c3d4e5f6a7_add_hwid_device_limit.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • dashboard/src/pages/_dashboard.settings.hwid.tsx

Comment thread dashboard/src/components/dialogs/user-modal.tsx
Comment thread dashboard/src/components/dialogs/user-modal.tsx
Comment thread dashboard/src/components/dialogs/user-modal.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
app/db/crud/hwid.py (1)

67-68: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Manual commits after transaction context can break outer atomicity.

At Line 112, Line 208, Line 235, and Line 243, calling await db.commit() after db.begin()/db.begin_nested() risks committing a caller-owned outer transaction. Let the context manager finalize the transaction/savepoint.

🔧 Suggested patch
@@
-    should_commit = False
     decision = HWIDDecision(allowed=True)
@@
-            should_commit = True
             decision = HWIDDecision(allowed=True)
@@
-                should_commit = True
                 decision = HWIDDecision(allowed=True)
 
-    if should_commit:
-        await db.commit()
     return decision
@@
-    await db.commit()
     return (
@@
-    await db.commit()
     return int(result.rowcount or 0)
@@
-    await db.commit()
     return int(result.rowcount or 0)
#!/bin/bash
# Read-only verification: find explicit commits following tx_ctx usage in HWID CRUD.
rg -n -C2 'tx_ctx = db\.begin_nested\(\) if db\.in_transaction\(\) else db\.begin\(\)|await db\.commit\(\)' app/db/crud/hwid.py

Also applies to: 112-113, 169-170, 208-208, 230-231, 235-235, 240-241, 243-243

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/db/crud/hwid.py` around lines 67 - 68, The code starts a transactional
context using tx_ctx = db.begin_nested() if db.in_transaction() else db.begin()
but later calls await db.commit(), which can prematurely commit an outer
transaction; remove the explicit await db.commit() calls and any should_commit
flag logic and instead rely on the context manager (tx_ctx) to commit/rollback
the savepoint or transaction; search for uses of tx_ctx and replace manual
commits (await db.commit()) with letting the async context manager exit handle
finalization so outer atomicity is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/db/crud/hwid.py`:
- Around line 67-68: The code starts a transactional context using tx_ctx =
db.begin_nested() if db.in_transaction() else db.begin() but later calls await
db.commit(), which can prematurely commit an outer transaction; remove the
explicit await db.commit() calls and any should_commit flag logic and instead
rely on the context manager (tx_ctx) to commit/rollback the savepoint or
transaction; search for uses of tx_ctx and replace manual commits (await
db.commit()) with letting the async context manager exit handle finalization so
outer atomicity is preserved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b30124fc-9cce-467f-8f37-bd360155fbe8

📥 Commits

Reviewing files that changed from the base of the PR and between afd7421 and 9cac4e2.

📒 Files selected for processing (4)
  • app/db/crud/hwid.py
  • app/db/migrations/versions/b2c3d4e5f6a7_add_hwid_device_limit.py
  • app/db/models.py
  • tests/api/conftest.py

@Nemu-x
Copy link
Copy Markdown
Contributor Author

Nemu-x commented May 3, 2026

@ImMohammad20000 @M03ED could you skim this PR and say what you’d change before merge? CI is green on latest main; happy to adjust quickly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants