Skip to content

feat: Implement HWID management models, API, and UI features#481

Merged
M03ED merged 9 commits into
devfrom
hwid
May 15, 2026
Merged

feat: Implement HWID management models, API, and UI features#481
M03ED merged 9 commits into
devfrom
hwid

Conversation

@M03ED
Copy link
Copy Markdown
Contributor

@M03ED M03ED commented May 15, 2026

Summary by CodeRabbit

  • New Features

    • Added hardware ID (HWID) registration and tracking to register and monitor user devices.
    • Implemented per-user device limits with configurable minimum and maximum thresholds.
    • Added admin endpoints to view, delete, and reset user hardware IDs.
    • Added configurable HWID policy settings (enable/force checks, device limits).
    • Extended dashboard with HWID management UI for administrators.
  • Chores

    • Database migration adding HWID storage and device metadata fields.

Review Change Stack

M03ED and others added 8 commits May 14, 2026 15:35
- Add HWID settings page with device registration policy configuration
- Implement hardware ID management modal for per-user HWID viewing and deletion
- Add HWID limit fields to user template and subscription forms
- Add comprehensive localization for HWID features across en, fa, ru, zh locales
- Add user-hwids-modal component for managing registered device IDs
- Add dedicated HWID settings route (_dashboard.settings.hwid.tsx)
- Update sidebar navigation to include HWID settings link
- Add HWID-related API endpoints to service layer
- Support device limit configuration with fallback, minimum, and maximum bounds
- Enable per-user HWID limit overrides with validation
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c2aab113-98c3-4db8-b52c-1c5dca803af9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR introduces a comprehensive hardware ID (HWID) tracking system enabling device registration and per-user device limits. The implementation spans database models, CRUD operations, admin APIs, subscription tracking integration, and full dashboard UI with multi-language support and configuration settings.

Changes

HWID Management System

Layer / File(s) Summary
HWID schema and data models
app/db/migrations/versions/f02194c811d6_add_hwid_models_and_fields.py, app/db/models.py, app/models/settings.py, app/models/subscription.py, app/models/user.py, app/models/user_template.py
Database migration creates user_hwids table with device metadata and timestamps, unique (user_id, hwid) constraint, and indexes. ORM models establish UserHWID, update User/Settings/UserTemplate with HWID fields, and Pydantic schemas define HWIDSettings, SubscriptionHeaders, and response types.
HWID CRUD operations and user persistence
app/db/crud/hwid.py, app/db/crud/user.py
HWID CRUD functions (list, fetch by value, count, register with device metadata truncation, update last-used timestamp, delete, reset). User creation/modification persist and validate hwid_limit from settings with min/max bounds.
HWID settings, caching, and configuration
app/settings/__init__.py, app/utils/wireguard.py
Cached async settings accessor for HWID configuration. WireGuard setup adds early return when disabled before key/peer validation.
HWID admin operations and user lifecycle management
app/operation/hwid.py, app/operation/user.py
HWIDOperation provides admin endpoints for list/delete/reset. User creation defaults hwid_limit from settings and enforces bounds. User modification validates HWID limit against current device count. Bulk user creation refactored to validate users and return subscription URLs.
Subscription HWID integration and validation
app/operation/subscription.py
New validate_and_register_hwid method gates on HWID enablement, enforces required headers when configured, updates last-used for existing HWIDs, and enforces per-user limits before registration. Three subscription endpoints extended with HWID/device header parameters and integration.
HWID API endpoints and header dependency injection
app/routers/dependencies/_common.py, app/routers/dependencies/subscription.py, app/routers/hwid.py, app/routers/subscription.py, app/routers/__init__.py
Header dependency factory coerces Pydantic models into FastAPI header parameters. Subscription endpoints injected with SubscriptionHeaders dependency. Three HWID admin endpoints (GET/DELETE/POST) wired with DB and admin auth. Router registration includes new HWID router.
Frontend HWID display and management modal
dashboard/src/features/users/dialogs/user-hwids-modal.tsx, dashboard/src/features/users/dialogs/user-modal.tsx, dashboard/src/features/users/dialogs/user-subscription-clients-modal.tsx, dashboard/src/features/users/components/action-buttons.tsx
Modal component fetches and displays registered HWIDs with device metadata, copy-to-clipboard, delete/reset confirmation dialogs, and success/error toasts. Integration into user edit dialog actions and subscription clients modal showing HWID per client.
Frontend user form integration with HWID limit
dashboard/src/features/users/forms/user-form.ts, dashboard/src/features/users/components/users-table.tsx
Form schema adds optional nullable hwid_limit field. State management tracks HWID limit in user form, table edit flow, and action buttons.
Frontend HWID settings page and configuration
dashboard/src/pages/_dashboard.settings.hwid.tsx, dashboard/src/pages/_dashboard.settings.tsx
Settings page with form schema (enable/forced toggles, fallback/min/max limit inputs, cross-field validation). Settings tab integration with Fingerprint icon and update flow reshaping HWID payload.
Frontend template HWID limit configuration and display
dashboard/src/features/templates/forms/user-template-form.ts, dashboard/src/features/templates/dialogs/user-template-modal.tsx, dashboard/src/features/templates/components/user-template.tsx, dashboard/src/features/templates/components/use-user-templates-list-columns.tsx, dashboard/src/pages/_dashboard.templates.user.tsx, dashboard/src/pages/_dashboard.bulk.create.tsx
Template form and modal support hwid_limit field with decimal input, normalization via Math.floor, and error handling. Template display and list columns conditionally render default/unlimited/numeric values with Infinity icon. Bulk creation page shows template HWID limit.
Frontend routing and navigation for HWID settings
dashboard/src/app/router.tsx, dashboard/src/components/layout/sidebar.tsx
Lazy-loaded HWID settings route under /settings/hwid with Suspense wrapper. Sidebar menu adds HWID settings entry with Fingerprint icon under sudo-admin settings submenu.
Generated API client and type definitions
dashboard/src/service/api/index.ts
Orval-generated types and React Query hooks for HWID endpoints (getUserHwids, deleteUserHwid, resetUserHwids), user/template/subscription HWID fields, and raw subscription endpoint. Type updates for XHTTP uplink_chunk_size to string-based format.
Internationalization for HWID features
dashboard/public/statics/locales/en.json, dashboard/public/statics/locales/fa.json, dashboard/public/statics/locales/ru.json, dashboard/public/statics/locales/zh.json
Localization strings across 4 languages for HWID settings (policy, enable/forced, device limits, validation), user HWID limit fields with placeholders, HWID management UI (copy/delete/reset actions, empty states, error messages), and timestamps.
Test configuration and integration tests
tests/api/conftest.py, tests/api/test_hwid.py
Global cache disabling in test suite via aiocache mock. Mocked HWID settings in fixtures. End-to-end integration test covering HWID registration via subscription headers, device limit enforcement (403 on limit exceeded), per-HWID deletion, and bulk reset with user cleanup.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SubRouter
  participant SubOp
  participant HWIDOp
  participant DB
  Client->>SubRouter: GET /sub/user_agent (X-HWID: device123, X-Device-OS: ios)
  SubRouter->>SubOp: user_subscription(..., x_hwid=device123, x_device_os=ios)
  SubOp->>SubOp: validate_and_register_hwid
  alt HWID enabled in settings
    alt existing HWID found
      SubOp->>HWIDOp: update_hwid_last_used(hwid_obj)
    else new HWID
      SubOp->>SubOp: check user limit (fallback_limit=3)
      alt limit exceeded
        SubOp-->>SubRouter: raise 403 Device limit reached
      else limit ok
        SubOp->>HWIDOp: register_user_hwid(user_id, device123, device_os=ios)
      end
    end
  end
  SubOp->>DB: user_sub_update(..., hwid=device123)
  SubRouter-->>Client: subscription config
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PasarGuard/panel#108: Modifies bulk user creation logic in app/db/crud/user.py and related user persistence flow, intersecting with HWID PR's additions to create_users_bulk for hwid_limit persistence.

Suggested labels

Backend, Frontend

Poem

🐰 A whisker-twitching tale of devices fine,
Each hardware ID in a perfect line,
Track them, limit them, with grace so true,
Now every bunny knows just what's who! 🪪✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.98% 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: Implement HWID management models, API, and UI features' accurately and comprehensively describes the main changes across backend models, API endpoints, and dashboard UI components for hardware ID 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
  • Commit unit tests in branch hwid

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.

@x0sina x0sina added the enhancement New feature or request label May 15, 2026
@M03ED
Copy link
Copy Markdown
Contributor Author

M03ED commented May 15, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 11

🧹 Nitpick comments (3)
dashboard/src/features/templates/dialogs/user-template-modal.tsx (1)

179-184: 💤 Low value

Consider simplifying redundant null check.

Line 179 already normalizes values.hwid_limit to either null or a number, so the null check on line 184 is redundant. The code is correct but could be more concise.

♻️ Simplified version
-const normalizedHwidLimit = values.hwid_limit == null ? null : Number(values.hwid_limit)
+const normalizedHwidLimit = values.hwid_limit == null ? null : Number(values.hwid_limit)
 // Build payload according to UserTemplateCreate interface
 const submitData = {
   name: values.name,
   data_limit: hasDataLimit ? gbToBytes(normalizedDataLimitGb as any) : 0,
-  hwid_limit: normalizedHwidLimit == null ? null : Number.isFinite(normalizedHwidLimit) ? Math.floor(normalizedHwidLimit) : null,
+  hwid_limit: normalizedHwidLimit === null || !Number.isFinite(normalizedHwidLimit) ? null : Math.floor(normalizedHwidLimit),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/features/templates/dialogs/user-template-modal.tsx` around
lines 179 - 184, The hwid_limit assignment in the submitData object is doing a
redundant null check because normalizedHwidLimit has already been normalized to
null or a number; simplify the expression in the submitData construction (in
user-template-modal.tsx) to directly test Number.isFinite(normalizedHwidLimit) ?
Math.floor(normalizedHwidLimit) : null (or equivalent) so the code uses the
existing normalizedHwidLimit variable and removes the extra "== null" branch
while preserving the current semantics.
dashboard/src/components/layout/sidebar.tsx (1)

257-261: 💤 Low value

Add trailing comma for consistency.

The closing brace should have a trailing comma to match the style of all other items in the items array.

📝 Proposed fix
               {
                 title: 'settings.hwid.title',
                 url: '/settings/hwid',
                 icon: Fingerprint
-              },
+              },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/components/layout/sidebar.tsx` around lines 257 - 261, The
object entry with title 'settings.hwid.title' in the items array (the object
containing url '/settings/hwid' and icon Fingerprint) is missing a trailing
comma; update that object literal to include a trailing comma after its closing
brace so it matches the other entries in the items array and the project's
consistent comma style.
app/models/settings.py (1)

288-294: ⚡ Quick win

Add cross-field validation for HWID limits.

HWIDSettings currently validates each field independently, so inconsistent combinations can still pass (e.g., min_limit > max_limit or fallback outside bounds). Add a model-level invariant check to reject invalid configurations early.

♻️ Proposed fix
 class HWIDSettings(BaseModel):
     enabled: bool = Field(default=False)
     forced: bool = Field(default=False)
     fallback_limit: int = Field(default=0, ge=0)
     min_limit: int = Field(default=0, ge=0)
     max_limit: int = Field(default=0, ge=0)
+
+    `@model_validator`(mode="after")
+    def validate_limit_bounds(self):
+        if self.min_limit > self.max_limit:
+            raise ValueError("min_limit cannot be greater than max_limit")
+        if not (self.min_limit <= self.fallback_limit <= self.max_limit):
+            raise ValueError("fallback_limit must be between min_limit and max_limit")
+        return self
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/settings.py` around lines 288 - 294, HWIDSettings currently only
validates fields individually, so invalid combos like min_limit > max_limit or
fallback_limit outside [min_limit, max_limit] can slip through; add a
model-level validator (e.g., a Pydantic `@root_validator` or model validator) on
class HWIDSettings that checks invariants: ensure min_limit <= max_limit, ensure
fallback_limit is between min_limit and max_limit (or equal to a defined
sentinel behavior if disabled), and raise a ValueError with a clear message if
any invariant fails; implement this in the HWIDSettings class so invalid
configurations are rejected at model creation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/db/crud/hwid.py`:
- Around line 44-47: Wrap the db.add(new_hwid) / await db.commit() / await
db.refresh(new_hwid) block in a try/except that catches
sqlalchemy.exc.IntegrityError, call await db.rollback() inside the except, then
query the DB for the existing record using the same unique key(s) (e.g. by
hardware_id or whatever unique column the model uses) and return that existing
instance instead of raising; ensure IntegrityError is imported and that you only
suppress duplicates (re-raise other DB errors). Keep references to the current
variables/new_hwid and use the model's unique lookup (e.g. get_by_hardware_id or
a query on the model) to return the already-registered object.

In `@app/db/crud/user.py`:
- Around line 969-970: The modify logic for hwid_limit only assigns when
modify.hwid_limit is not None, preventing clients from clearing an existing
db_user.hwid_limit back to NULL; update the user-modify handler (where
modify.hwid_limit and db_user.hwid_limit are referenced) to distinguish "no
change" from "explicit clear" by adding an explicit flag or using a sentinel
(e.g., a separate boolean on the request or an Optional wrapper) so that when
the client signals clear you set db_user.hwid_limit = None, otherwise leave it
unchanged; locate the assignment involving modify.hwid_limit and replace the
conditional to apply the clear behavior when the explicit-clear indicator is
present.

In `@app/operation/subscription.py`:
- Around line 280-289: The HWID limit check is non-atomic (uses
get_user_hwid_count then register_user_hwid) and can be bypassed by concurrent
requests; fix by performing the check+insert inside a DB transaction or by
moving the enforcement into register_user_hwid so it does the count-and-insert
atomically (e.g., start a transaction, SELECT ... FOR UPDATE on the user's row
or use a DB constraint and attempt the insert, then raise the same 403 error if
the count >= limit). Update the code around get_user_hwid_count and
register_user_hwid to use this transactional/atomic approach so concurrent
requests cannot exceed the limit.

In `@app/routers/dependencies/_common.py`:
- Around line 96-100: The code currently only wraps non-required defaults in
Header(...) so required parameters (default == Parameter.empty) remain unbound
and become query params; update the logic around default/field_info/field_name
in the block that checks isinstance(default, (HeaderParam, ParameterOverride))
to also wrap required parameters by calling Header(..., alias=alias) when
default is Parameter.empty (use the same alias calculation using
field_info.alias or field_name.replace("_", "-").title()), so both required and
optional header fields are explicitly bound as Header with the correct alias.

In `@dashboard/public/statics/locales/ru.json`:
- Around line 1052-1053: Replace the inconsistent placeholder text "Резерв, 0 =
без лимита" with the same phrasing used in the template ("По умолчанию, 0 = без
лимита") for the key userDialog.hwidLimitPlaceholder and the other occurrences
flagged (the entries around lines 2401-2402); update the JSON values for
userDialog.hwidLimitPlaceholder (and the matching keys at the other location) to
"По умолчанию, 0 = без лимита" so all HWID limit placeholders use identical
wording.

In `@dashboard/public/statics/locales/zh.json`:
- Around line 1406-1407: Update the HWID placeholder translation to match the
template wording: replace the value of "userDialog.hwidLimitPlaceholder" (and
the duplicate entry at the other occurrence) from the current phrase using "备用"
to use "默认,0 = 无限制" so the placeholder text is consistent with the template copy
referenced by the key "userDialog.hwidLimitPlaceholder".

In `@dashboard/src/features/users/dialogs/user-hwids-modal.tsx`:
- Around line 139-141: The icon-only remove Button in user-hwids-modal.tsx lacks
an accessible name; update the Button (the element rendering <Trash2 /> and
calling setHwidToDelete(item.hwid)) to provide one—either add an explicit
aria-label (e.g., aria-label={`Remove HWID ${item.hwid}`} or aria-label="Remove
hardware ID") or include visually hidden text (a <span
className="sr-only">Remove hardware ID</span>) inside the Button so screen
readers can identify the action.

In `@dashboard/src/features/users/forms/user-form.ts`:
- Line 54: The hwid_limit schema currently allows fractional numbers; update the
validation for hwid_limit in user-form.ts to require non-negative integers by
adding integer enforcement (use the z.number().int().min(0) form) while
preserving nullable() and optional() so the field can still be null/omitted;
update the hwid_limit definition to use the integer-constrained validator to
prevent fractional device counts.

In `@dashboard/src/pages/_dashboard.settings.hwid.tsx`:
- Around line 79-82: The cancel handler currently shows a success toast which is
non-standard; update the handleCancel function so it no longer calls
toast.success(...) — either remove the toast call entirely or replace it with a
neutral/info toast (e.g., toast.info) and/or a less-assertive message key
(t('settings.hwid.cancelSuccess', ...) can be replaced or removed); keep the
existing form.reset(formValues) behavior and adjust the toast usage accordingly
in handleCancel to avoid showing a success state for a cancel action.

In `@dashboard/src/service/api/index.ts`:
- Around line 11577-11579: The URL path parameters are interpolated without
encoding in userSubscriptionRaw (token) and the similar hwid-based fetcher
around the other block; update both to call encodeURIComponent on the path
variables before building the URL (e.g., use encodeURIComponent(token) and
encodeURIComponent(hwid)) so reserved characters are safely escaped when
constructing `/sub/{token}/raw` and the hwid path, keeping the same orvalFetcher
call and argument names.

In `@tests/api/test_hwid.py`:
- Around line 45-46: The two intermediate calls using client.get(sub_url,
headers={"X-HWID": "device-2"}) and device-3 do not assert the response status;
update those calls to capture the Response objects (e.g., resp =
client.get(...)) and assert resp.status_code == 200 (or the expected HTTP
status) before proceeding to the later count/assertion; ensure you reference the
same variables (client, sub_url) and the X-HWID header when making the assertion
so failures surface as clear HTTP errors.

---

Nitpick comments:
In `@app/models/settings.py`:
- Around line 288-294: HWIDSettings currently only validates fields
individually, so invalid combos like min_limit > max_limit or fallback_limit
outside [min_limit, max_limit] can slip through; add a model-level validator
(e.g., a Pydantic `@root_validator` or model validator) on class HWIDSettings that
checks invariants: ensure min_limit <= max_limit, ensure fallback_limit is
between min_limit and max_limit (or equal to a defined sentinel behavior if
disabled), and raise a ValueError with a clear message if any invariant fails;
implement this in the HWIDSettings class so invalid configurations are rejected
at model creation.

In `@dashboard/src/components/layout/sidebar.tsx`:
- Around line 257-261: The object entry with title 'settings.hwid.title' in the
items array (the object containing url '/settings/hwid' and icon Fingerprint) is
missing a trailing comma; update that object literal to include a trailing comma
after its closing brace so it matches the other entries in the items array and
the project's consistent comma style.

In `@dashboard/src/features/templates/dialogs/user-template-modal.tsx`:
- Around line 179-184: The hwid_limit assignment in the submitData object is
doing a redundant null check because normalizedHwidLimit has already been
normalized to null or a number; simplify the expression in the submitData
construction (in user-template-modal.tsx) to directly test
Number.isFinite(normalizedHwidLimit) ? Math.floor(normalizedHwidLimit) : null
(or equivalent) so the code uses the existing normalizedHwidLimit variable and
removes the extra "== null" branch while preserving the current semantics.
🪄 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: 4e1b99be-1efe-47ac-b530-732773d13d71

📥 Commits

Reviewing files that changed from the base of the PR and between 4b70d34 and 7130c16.

📒 Files selected for processing (42)
  • app/db/crud/hwid.py
  • app/db/crud/user.py
  • app/db/migrations/versions/f02194c811d6_add_hwid_models_and_fields.py
  • app/db/models.py
  • app/models/settings.py
  • app/models/subscription.py
  • app/models/user.py
  • app/models/user_template.py
  • app/operation/hwid.py
  • app/operation/subscription.py
  • app/operation/user.py
  • app/routers/__init__.py
  • app/routers/dependencies/__init__.py
  • app/routers/dependencies/_common.py
  • app/routers/dependencies/subscription.py
  • app/routers/hwid.py
  • app/routers/subscription.py
  • app/settings/__init__.py
  • app/utils/wireguard.py
  • dashboard/public/statics/locales/en.json
  • dashboard/public/statics/locales/fa.json
  • dashboard/public/statics/locales/ru.json
  • dashboard/public/statics/locales/zh.json
  • dashboard/src/app/router.tsx
  • dashboard/src/components/layout/sidebar.tsx
  • dashboard/src/features/templates/components/use-user-templates-list-columns.tsx
  • dashboard/src/features/templates/components/user-template.tsx
  • dashboard/src/features/templates/dialogs/user-template-modal.tsx
  • dashboard/src/features/templates/forms/user-template-form.ts
  • dashboard/src/features/users/components/action-buttons.tsx
  • dashboard/src/features/users/components/users-table.tsx
  • dashboard/src/features/users/dialogs/user-hwids-modal.tsx
  • dashboard/src/features/users/dialogs/user-modal.tsx
  • dashboard/src/features/users/dialogs/user-subscription-clients-modal.tsx
  • dashboard/src/features/users/forms/user-form.ts
  • dashboard/src/pages/_dashboard.bulk.create.tsx
  • dashboard/src/pages/_dashboard.settings.hwid.tsx
  • dashboard/src/pages/_dashboard.settings.tsx
  • dashboard/src/pages/_dashboard.templates.user.tsx
  • dashboard/src/service/api/index.ts
  • tests/api/conftest.py
  • tests/api/test_hwid.py

Comment thread app/db/crud/hwid.py
Comment on lines +44 to +47
db.add(new_hwid)
await db.commit()
await db.refresh(new_hwid)
return new_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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make HWID registration idempotent under concurrent requests.

Concurrent calls can hit the unique constraint and raise an integrity error; this should be handled as a normal duplicate-registration case instead of surfacing a failure.

🛡️ Proposed fix
 from sqlalchemy import delete, func, select
+from sqlalchemy.exc import IntegrityError
 from sqlalchemy.ext.asyncio import AsyncSession
@@
     db.add(new_hwid)
-    await db.commit()
+    try:
+        await db.commit()
+    except IntegrityError:
+        await db.rollback()
+        existing = await get_user_hwid_by_value(db, user_id=user_id, hwid_str=hwid)
+        if existing is not None:
+            return existing
+        raise
     await db.refresh(new_hwid)
     return new_hwid
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
db.add(new_hwid)
await db.commit()
await db.refresh(new_hwid)
return new_hwid
from sqlalchemy import delete, func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
db.add(new_hwid)
try:
await db.commit()
except IntegrityError:
await db.rollback()
existing = await get_user_hwid_by_value(db, user_id=user_id, hwid_str=hwid)
if existing is not None:
return existing
raise
await db.refresh(new_hwid)
return new_hwid
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/db/crud/hwid.py` around lines 44 - 47, Wrap the db.add(new_hwid) / await
db.commit() / await db.refresh(new_hwid) block in a try/except that catches
sqlalchemy.exc.IntegrityError, call await db.rollback() inside the except, then
query the DB for the existing record using the same unique key(s) (e.g. by
hardware_id or whatever unique column the model uses) and return that existing
instance instead of raising; ensure IntegrityError is imported and that you only
suppress duplicates (re-raise other DB errors). Keep references to the current
variables/new_hwid and use the model's unique lookup (e.g. get_by_hardware_id or
a query on the model) to return the already-registered object.

Comment thread app/db/crud/user.py
Comment on lines +969 to +970
if modify.hwid_limit is not None:
db_user.hwid_limit = modify.hwid_limit
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Allow explicit clearing of hwid_limit during user modify.

This branch only runs for non-None values, so clients cannot reset an existing override to NULL (inherit/default behavior).

🐛 Proposed fix
-    if modify.hwid_limit is not None:
-        db_user.hwid_limit = modify.hwid_limit
+    if "hwid_limit" in modify.model_fields_set:
+        db_user.hwid_limit = modify.hwid_limit
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/db/crud/user.py` around lines 969 - 970, The modify logic for hwid_limit
only assigns when modify.hwid_limit is not None, preventing clients from
clearing an existing db_user.hwid_limit back to NULL; update the user-modify
handler (where modify.hwid_limit and db_user.hwid_limit are referenced) to
distinguish "no change" from "explicit clear" by adding an explicit flag or
using a sentinel (e.g., a separate boolean on the request or an Optional
wrapper) so that when the client signals clear you set db_user.hwid_limit =
None, otherwise leave it unchanged; locate the assignment involving
modify.hwid_limit and replace the conditional to apply the clear behavior when
the explicit-clear indicator is present.

Comment on lines +280 to +289
# It's a new HWID, check limit
limit = user_hwid_limit if user_hwid_limit is not None else hwid_conf.fallback_limit
if limit == 0:
pass # unlimited
else:
current_count = await get_user_hwid_count(db, user_id)
if current_count >= limit:
await self.raise_error(message="Device limit reached", code=403)

await register_user_hwid(db, user_id, x_hwid, x_device_os, x_ver_os, x_device_model)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Enforce HWID limit atomically to prevent over-registration under concurrency.

Line 285–289 performs a read (get_user_hwid_count) then write (register_user_hwid) without a lock/atomic guard. Concurrent requests can bypass the cap and insert extra devices.

Suggested direction
-        else:
-            current_count = await get_user_hwid_count(db, user_id)
-            if current_count >= limit:
-                await self.raise_error(message="Device limit reached", code=403)
-
-        await register_user_hwid(db, user_id, x_hwid, x_device_os, x_ver_os, x_device_model)
+        else:
+            # Move count+insert into one transactional CRUD function
+            # (e.g., SELECT ... FOR UPDATE on user/hwid rows, then insert).
+            created = await register_user_hwid_with_limit(
+                db=db,
+                user_id=user_id,
+                hwid=x_hwid,
+                limit=limit,
+                device_os=x_device_os,
+                ver_os=x_ver_os,
+                device_model=x_device_model,
+            )
+            if not created:
+                await self.raise_error(message="Device limit reached", code=403)
+            return
+
+        await register_user_hwid(db, user_id, x_hwid, x_device_os, x_ver_os, x_device_model)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/operation/subscription.py` around lines 280 - 289, The HWID limit check
is non-atomic (uses get_user_hwid_count then register_user_hwid) and can be
bypassed by concurrent requests; fix by performing the check+insert inside a DB
transaction or by moving the enforcement into register_user_hwid so it does the
count-and-insert atomically (e.g., start a transaction, SELECT ... FOR UPDATE on
the user's row or use a DB constraint and attempt the insert, then raise the
same 403 error if the count >= limit). Update the code around
get_user_hwid_count and register_user_hwid to use this transactional/atomic
approach so concurrent requests cannot exceed the limit.

Comment on lines +96 to +100
# Ensure everything is explicitly a Header parameter
if not isinstance(default, (HeaderParam, ParameterOverride)) and default is not Parameter.empty:
alias = field_info.alias if field_info.alias else field_name.replace("_", "-").title()
default = Header(default, alias=alias)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In FastAPI dependencies, if a function parameter is required (no default) and not wrapped in Header(...), is it treated as a query parameter by default?

💡 Result:

Yes, in FastAPI, if a function parameter (whether in a path operation function or a dependency function) is declared without a default value and is not explicitly wrapped in a parameter class like Header, Path, or Body, it is treated as a query parameter by default [1][2][3]. FastAPI determines the source of a parameter based on the following rules: 1. Path Parameters: If the parameter name is declared in the path string (e.g., /items/{item_id}), it is automatically treated as a path parameter [4]. 2. Special Types: Certain types, such as Request, Response, or BackgroundTasks, are automatically recognized and injected by FastAPI without needing any special declaration [2]. 3. Query Parameters: If a parameter is not a path parameter and is not one of the special types, it is treated as a query parameter by default [1][2]. Because you did not specify a default value, FastAPI will require this parameter to be present in the request's query string [1][3]. If the client fails to provide it, FastAPI will return a 422 Unprocessable Entity error.

Citations:


🏁 Script executed:

find . -type f -name "_common.py" | grep dependencies

Repository: PasarGuard/panel

Length of output: 98


🏁 Script executed:

cat -n app/routers/dependencies/_common.py | head -120 | tail -50

Repository: PasarGuard/panel

Length of output: 2398


🏁 Script executed:

# Get more context around the function to understand the full logic
sed -n '80,110p' app/routers/dependencies/_common.py

Repository: PasarGuard/panel

Length of output: 1307


🏁 Script executed:

sed -n '40,72p' app/routers/dependencies/_common.py

Repository: PasarGuard/panel

Length of output: 1342


Required header fields are not bound as headers.

At lines 97–100, only non-Parameter.empty defaults are wrapped with Header(...). Required fields (Parameter.empty) will be interpreted as query parameters instead of headers, since FastAPI defaults to query parameters for undecorated required parameters.

Use Header(..., alias=alias) for required fields to ensure they are correctly sourced from request headers:

Proposed fix
-        if not isinstance(default, (HeaderParam, ParameterOverride)) and default is not Parameter.empty:
-            alias = field_info.alias if field_info.alias else field_name.replace("_", "-").title()
-            default = Header(default, alias=alias)
+        alias = field_info.alias if field_info.alias else field_name.replace("_", "-").title()
+        if not isinstance(default, HeaderParam):
+            if default is Parameter.empty:
+                default = Header(..., alias=alias)
+            else:
+                default = Header(default, alias=alias)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/routers/dependencies/_common.py` around lines 96 - 100, The code
currently only wraps non-required defaults in Header(...) so required parameters
(default == Parameter.empty) remain unbound and become query params; update the
logic around default/field_info/field_name in the block that checks
isinstance(default, (HeaderParam, ParameterOverride)) to also wrap required
parameters by calling Header(..., alias=alias) when default is Parameter.empty
(use the same alias calculation using field_info.alias or
field_name.replace("_", "-").title()), so both required and optional header
fields are explicitly bound as Header with the correct alias.

Comment thread dashboard/public/statics/locales/ru.json Outdated
Comment on lines +139 to +141
<Button type="button" variant="ghost" size="icon" className="h-8 w-8 text-destructive hover:text-destructive" onClick={() => setHwidToDelete(item.hwid)}>
<Trash2 className="h-4 w-4" />
</Button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an accessible name to the icon-only remove action.

Line 139 renders a destructive icon-only button without an accessible label, so screen-reader users can’t reliably identify the action.

♿ Proposed fix
-            <Button type="button" variant="ghost" size="icon" className="h-8 w-8 text-destructive hover:text-destructive" onClick={() => setHwidToDelete(item.hwid)}>
+            <Button
+              type="button"
+              variant="ghost"
+              size="icon"
+              className="h-8 w-8 text-destructive hover:text-destructive"
+              aria-label={t('remove', { defaultValue: 'Remove' })}
+              title={t('remove', { defaultValue: 'Remove' })}
+              onClick={() => setHwidToDelete(item.hwid)}
+            >
               <Trash2 className="h-4 w-4" />
             </Button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button type="button" variant="ghost" size="icon" className="h-8 w-8 text-destructive hover:text-destructive" onClick={() => setHwidToDelete(item.hwid)}>
<Trash2 className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive hover:text-destructive"
aria-label={t('remove', { defaultValue: 'Remove' })}
title={t('remove', { defaultValue: 'Remove' })}
onClick={() => setHwidToDelete(item.hwid)}
>
<Trash2 className="h-4 w-4" />
</Button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/features/users/dialogs/user-hwids-modal.tsx` around lines 139 -
141, The icon-only remove Button in user-hwids-modal.tsx lacks an accessible
name; update the Button (the element rendering <Trash2 /> and calling
setHwidToDelete(item.hwid)) to provide one—either add an explicit aria-label
(e.g., aria-label={`Remove HWID ${item.hwid}`} or aria-label="Remove hardware
ID") or include visually hidden text (a <span className="sr-only">Remove
hardware ID</span>) inside the Button so screen readers can identify the action.

username: z.string().min(3, 'validation.minLength').max(128, 'validation.maxLength'),
group_ids: z.array(z.number()).min(1, { message: 'validation.required' }),
data_limit: z.number().min(0),
hwid_limit: z.number().min(0).nullable().optional(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce integer validation for hwid_limit.

Line 54 accepts fractional values for a device-count field. This can lead to unintended policy outcomes downstream during submission normalization.

🛡️ Proposed fix
-  hwid_limit: z.number().min(0).nullable().optional(),
+  hwid_limit: z.number().int().min(0).nullable().optional(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
hwid_limit: z.number().min(0).nullable().optional(),
hwid_limit: z.number().int().min(0).nullable().optional(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/features/users/forms/user-form.ts` at line 54, The hwid_limit
schema currently allows fractional numbers; update the validation for hwid_limit
in user-form.ts to require non-negative integers by adding integer enforcement
(use the z.number().int().min(0) form) while preserving nullable() and
optional() so the field can still be null/omitted; update the hwid_limit
definition to use the integer-constrained validator to prevent fractional device
counts.

Comment on lines +79 to +82
const handleCancel = () => {
form.reset(formValues)
toast.success(t('settings.hwid.cancelSuccess', { defaultValue: 'Changes cancelled and original HWID settings restored' }))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unusual UX: success toast on cancel action.

Showing a success toast when canceling changes is non-standard. Users typically expect success feedback only for completed actions (like saving). Consider either removing the toast entirely or using a neutral message instead of toast.success.

Suggested alternatives

Option 1: Remove the toast entirely

 const handleCancel = () => {
   form.reset(formValues)
-  toast.success(t('settings.hwid.cancelSuccess', { defaultValue: 'Changes cancelled and original HWID settings restored' }))
 }

Option 2: Use neutral info toast

 const handleCancel = () => {
   form.reset(formValues)
-  toast.success(t('settings.hwid.cancelSuccess', { defaultValue: 'Changes cancelled and original HWID settings restored' }))
+  toast.info(t('settings.hwid.cancelInfo', { defaultValue: 'Changes discarded' }))
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleCancel = () => {
form.reset(formValues)
toast.success(t('settings.hwid.cancelSuccess', { defaultValue: 'Changes cancelled and original HWID settings restored' }))
}
const handleCancel = () => {
form.reset(formValues)
}
Suggested change
const handleCancel = () => {
form.reset(formValues)
toast.success(t('settings.hwid.cancelSuccess', { defaultValue: 'Changes cancelled and original HWID settings restored' }))
}
const handleCancel = () => {
form.reset(formValues)
toast.info(t('settings.hwid.cancelInfo', { defaultValue: 'Changes discarded' }))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/pages/_dashboard.settings.hwid.tsx` around lines 79 - 82, The
cancel handler currently shows a success toast which is non-standard; update the
handleCancel function so it no longer calls toast.success(...) — either remove
the toast call entirely or replace it with a neutral/info toast (e.g.,
toast.info) and/or a less-assertive message key
(t('settings.hwid.cancelSuccess', ...) can be replaced or removed); keep the
existing form.reset(formValues) behavior and adjust the toast usage accordingly
in handleCancel to avoid showing a success state for a cancel action.

Comment on lines +11577 to +11579
export const userSubscriptionRaw = (token: string, signal?: AbortSignal) => {
return orvalFetcher<unknown>({ url: `/sub/${token}/raw`, method: 'GET', signal })
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

URL path params should be URI-encoded before interpolation

Line 11578 (token) and Line 12391 (hwid) are inserted into URL paths without encoding. If they contain reserved characters (/, #, %, ?), requests can hit the wrong route or fail.

🔧 Proposed fix
 export const userSubscriptionRaw = (token: string, signal?: AbortSignal) => {
-  return orvalFetcher<unknown>({ url: `/sub/${token}/raw`, method: 'GET', signal })
+  return orvalFetcher<unknown>({ url: `/sub/${encodeURIComponent(token)}/raw`, method: 'GET', signal })
 }

 export const deleteUserHwid = (userId: number, hwid: string) => {
-  return orvalFetcher<unknown>({ url: `/api/user/${userId}/hwids/${hwid}`, method: 'DELETE' })
+  return orvalFetcher<unknown>({ url: `/api/user/${userId}/hwids/${encodeURIComponent(hwid)}`, method: 'DELETE' })
 }

Also applies to: 12390-12392

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/service/api/index.ts` around lines 11577 - 11579, The URL path
parameters are interpolated without encoding in userSubscriptionRaw (token) and
the similar hwid-based fetcher around the other block; update both to call
encodeURIComponent on the path variables before building the URL (e.g., use
encodeURIComponent(token) and encodeURIComponent(hwid)) so reserved characters
are safely escaped when constructing `/sub/{token}/raw` and the hwid path,
keeping the same orvalFetcher call and argument names.

Comment thread tests/api/test_hwid.py Outdated
Comment on lines +45 to +46
client.get(sub_url, headers={"X-HWID": "device-2"})
client.get(sub_url, headers={"X-HWID": "device-3"})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate response status for intermediate HWID registrations.

The subscription requests at lines 45-46 don't check the response status. If either registration fails, the test will fail at line 49 with a confusing count mismatch instead of a clear HTTP error.

✅ Proposed fix to add status code assertions
-        client.get(sub_url, headers={"X-HWID": "device-2"})
-        client.get(sub_url, headers={"X-HWID": "device-3"})
+        response = client.get(sub_url, headers={"X-HWID": "device-2"})
+        assert response.status_code == status.HTTP_200_OK
+        response = client.get(sub_url, headers={"X-HWID": "device-3"})
+        assert response.status_code == status.HTTP_200_OK
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
client.get(sub_url, headers={"X-HWID": "device-2"})
client.get(sub_url, headers={"X-HWID": "device-3"})
response = client.get(sub_url, headers={"X-HWID": "device-2"})
assert response.status_code == status.HTTP_200_OK
response = client.get(sub_url, headers={"X-HWID": "device-3"})
assert response.status_code == status.HTTP_200_OK
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/api/test_hwid.py` around lines 45 - 46, The two intermediate calls
using client.get(sub_url, headers={"X-HWID": "device-2"}) and device-3 do not
assert the response status; update those calls to capture the Response objects
(e.g., resp = client.get(...)) and assert resp.status_code == 200 (or the
expected HTTP status) before proceeding to the later count/assertion; ensure you
reference the same variables (client, sub_url) and the X-HWID header when making
the assertion so failures surface as clear HTTP errors.

@M03ED M03ED merged commit bef60c4 into dev May 15, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants