Skip to content

feat: add per-host subscription template overrides#278

Open
ogerman wants to merge 2 commits intoPasarGuard:devfrom
ogerman:feature/host-subscription-templates
Open

feat: add per-host subscription template overrides#278
ogerman wants to merge 2 commits intoPasarGuard:devfrom
ogerman:feature/host-subscription-templates

Conversation

@ogerman
Copy link

@ogerman ogerman commented Feb 16, 2026

Allow hosts to override the Xray subscription template via a new subscription_templates field. Templates are loaded from CUSTOM_TEMPLATES_DIRECTORY and validated on create/update.

image

Backend:

  • Add subscription_templates JSON column to ProxyHost (with migration)
  • Add SubscriptionTemplates Pydantic model
  • Add GET /api/host/subscription-templates endpoint
  • Add shared get_subscription_templates() helper (DRY listing + validation)
  • Exclude default templates from the list (respects env overrides)
  • Wire template override into Xray subscription rendering

Frontend:

  • Add template select dropdown in host modal (hidden when no custom dir)
  • Add useGetSubscriptionTemplates hook and API types
  • Add i18n keys for en, ru, zh, fa

Tests:

  • Add 9 API tests covering list, auth, CRUD with templates, validation

Summary by CodeRabbit

  • New Features

    • Added per-host subscription template configuration for customizing Xray templates.
    • Introduced new API endpoint to list available subscription templates by format.
    • Added UI form field in host dialogs for selecting custom subscription templates.
    • Extended host creation and modification to support template selection.
  • Localization

    • Added subscription template-related translation strings (English, Persian, Russian, Chinese).

Allow hosts to override the Xray subscription template via a new
`subscription_templates` field. Templates are loaded from
CUSTOM_TEMPLATES_DIRECTORY and validated on create/update.

Backend:
- Add `subscription_templates` JSON column to ProxyHost (with migration)
- Add SubscriptionTemplates Pydantic model
- Add GET /api/host/subscription-templates endpoint
- Add shared get_subscription_templates() helper (DRY listing + validation)
- Exclude default templates from the list (respects env overrides)
- Wire template override into Xray subscription rendering

Frontend:
- Add template select dropdown in host modal (hidden when no custom dir)
- Add useGetSubscriptionTemplates hook and API types
- Add i18n keys for en, ru, zh, fa

Tests:
- Add 9 API tests covering list, auth, CRUD with templates, validation

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 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.

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 pull request adds per-host subscription template customization functionality. Users can now override default subscription templates (particularly for Xray) on a per-host basis. The implementation spans database schema changes, backend validation, template caching, API endpoints, and frontend UI components, with comprehensive test coverage.

Changes

Cohort / File(s) Summary
Database Schema & Models
app/db/migrations/versions/f1c2d3e4b5a6_..., app/db/models.py, app/db/crud/general.py
Added JSON column subscription_templates to hosts table via migration; added ProxyHost.subscription_templates field storing Dict[str, Any]; removed unused SQLAlchemy imports.
Backend Data Models
app/models/host.py, app/models/subscription.py
Introduced SubscriptionTemplates model with optional xray field and extra properties support; extended BaseHost and SubscriptionInboundData with subscription_templates field.
Template Management
app/templates/__init__.py
Implemented get_subscription_templates() function to scan custom template directories by format (xray, clash, singbox) and return available templates excluding defaults; added DEFAULT_TEMPLATES constant.
Host Core Logic
app/core/hosts.py, app/operation/host.py
Propagated subscription_templates through inbound data assembly; added validate_subscription_templates() method to verify selected templates exist before host creation/modification.
Subscription Configuration
app/subscription/xray.py
Replaced static template rendering with per-path caching via _get_template() and _template_cache dictionary; extended add_config() to accept optional template_path parameter for per-host template overrides.
API & Routing
app/routers/host.py, dashboard/src/service/api/index.ts
Added /subscription-templates GET endpoint (admin-only) returning template dict; extended API types with subscription_templates field in CreateHost and BaseHost; added React Query hook useGetSubscriptionTemplates().
Frontend Forms & Components
dashboard/src/components/forms/host-form.ts, dashboard/src/components/dialogs/host-modal.tsx, dashboard/src/components/hosts/hosts-list.tsx
Extended HostFormValues schema with subscription_templates object; added conditional Select field for Xray template selection in host modal; propagated field through host creation/editing/duplication flows.
Localization
dashboard/public/statics/locales/{en,fa,ru,zh}.json
Added three translation keys per language: subscriptionTemplates, subscriptionTemplates.xray, subscriptionTemplates.xray.info.
Test Coverage
tests/api/test_subscription_templates.py
Comprehensive test module covering: template listing endpoint, custom/built-in template differentiation, host creation with templates, validation of invalid templates, update scenarios, and authentication enforcement.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Frontend
    participant API as API Endpoint
    participant DB as Database
    participant Validator as Host Validator
    participant Templates as Template Manager
    participant Xray as Xray Config

    User->>API: POST /host (subscription_templates.xray = "custom.json")
    API->>Validator: validate_subscription_templates()
    Validator->>Templates: get_subscription_templates()
    Templates->>DB: scan custom templates dir
    DB-->>Templates: list of available templates
    Templates-->>Validator: {xray: ["custom.json", ...]}
    Validator->>Validator: verify "custom.json" in list
    alt Template exists
        Validator-->>API: ✓ validation passed
        API->>DB: CREATE host + store subscription_templates
        DB-->>API: host created
        API-->>User: 201 Created
    else Template not found
        Validator-->>API: ✗ 400 Bad Request
        API-->>User: Template not found error
    end

    User->>API: GET /subscription/xray?host_id=123
    API->>Xray: build subscription
    Xray->>DB: fetch host.subscription_templates
    DB-->>Xray: {xray: "custom.json"}
    Xray->>Xray: _get_template("custom.json")
    Xray->>Templates: load custom.json from cache/disk
    Templates-->>Xray: rendered template
    Xray->>Xray: apply config with custom template
    Xray-->>API: subscription data
    API-->>User: subscription response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

enhancement

Poem

🐰 A template for every host, custom and fine,
Xray configs now bending to your design,
Subscription templates, cached with great care,
Frontend and backend, validation so fair!
Hop-hop through features, the feature's complete,
From database to dashboard, the integration's neat! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add per-host subscription template overrides' clearly and concisely summarizes the main change: adding per-host subscription template override functionality.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into dev

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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.

@ImMohammad20000
Copy link
Contributor

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 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.

@coderabbitai coderabbitai bot added the enhancement New feature or request label Feb 17, 2026
Copy link

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
dashboard/src/components/hosts/hosts-list.tsx (1)

359-502: ⚠️ Potential issue | 🟠 Major

subscription_templates is missing from drag-and-drop reorder payloads — reordering will silently drop template overrides.

The hostsToUpdate array built in handleDragEnd constructs full CreateHost objects for each host but never includes subscription_templates. When a user reorders hosts via drag-and-drop, the modifyHosts call will overwrite all hosts without their template settings, effectively clearing any per-host subscription template overrides.

Proposed fix

Add subscription_templates to each host payload, e.g. after line 501:

         http_headers: host.http_headers || {},
+        subscription_templates: (host as any).subscription_templates,
       }))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dashboard/src/components/hosts/hosts-list.tsx` around lines 359 - 502, The
drag-and-drop reorder builds hostsToUpdate in handleDragEnd and omits
subscription_templates, so calling modifyHosts overwrites per-host template
overrides; update the hostsToUpdate mapping (the CreateHost objects constructed
in the hostsToUpdate array inside handleDragEnd) to include
subscription_templates: host.subscription_templates || undefined (or
host.subscription_templates ?? undefined) when present so subscription template
overrides are preserved before calling modifyHosts.
🧹 Nitpick comments (2)
dashboard/src/components/hosts/hosts-list.tsx (1)

90-90: Remove unnecessary as any casts — BaseHost already includes subscription_templates.

The subscription_templates property is defined in the BaseHost interface (type BaseHostSubscriptionTemplates), so the (host as any) casts on lines 90 and 276 are unnecessary. Replace (host as any).subscription_templates with host.subscription_templates to preserve type safety.

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

In `@dashboard/src/components/hosts/hosts-list.tsx` at line 90, The code is using
unnecessary casts to any for the subscription_templates property; update the
object construction in hosts-list.tsx (where subscription_templates is assigned)
to use host.subscription_templates directly (remove the `(host as any)` cast)
and do the same for the other occurrence later in the file (previously `(host as
any).subscription_templates` around the second occurrence), relying on the
BaseHost type for correct typing.
tests/api/test_subscription_templates.py (1)

106-123: Host cleanup is not in finally — assertion failure leaks the host.

If the assertion on line 119 fails, client.delete(f"/api/host/{data['id']}") on line 121 is never reached. The core is cleaned up via finally, but the host may remain orphaned (depending on cascade behavior). This pattern repeats in all CRUD tests.

Consider restructuring so host deletion is also guaranteed:

Example
     try:
         ...
         response = client.post("/api/host", headers=auth_headers(access_token), json=payload)
         assert response.status_code == status.HTTP_201_CREATED
-
         data = response.json()
-        assert data["subscription_templates"] == {"xray": "xray/custom.json"}
-
-        client.delete(f"/api/host/{data['id']}", headers=auth_headers(access_token))
+        try:
+            assert data["subscription_templates"] == {"xray": "xray/custom.json"}
+        finally:
+            client.delete(f"/api/host/{data['id']}", headers=auth_headers(access_token))
     finally:
         delete_core(access_token, core["id"])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/api/test_subscription_templates.py` around lines 106 - 123, The host
deletion is inside the try block so a failing assertion can leak the created
host; change the test to always delete the host in the finally block by
capturing the created host id (e.g., set host_id = None before the try, assign
host_id = data["id"] after successful creation from client.post) and then call
client.delete(f"/api/host/{host_id}", headers=auth_headers(access_token)) in the
finally before calling delete_core(access_token, core["id"]); update other CRUD
tests with the same pattern (use client.post, assign data["id"] to host_id, and
ensure client.delete runs in finally).
🤖 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/f1c2d3e4b5a6_add_subscription_templates_to_hosts.py`:
- Around line 1-7: The module-level docstring's "Revises: ee97c01bfbaf" is
inconsistent with the migration's down_revision variable; update the docstring
so the "Revises:" line matches down_revision = "20e2a5cf1e40" (i.e., change
ee97c01bfbaf to 20e2a5cf1e40) so the docstring and the migration's down_revision
are identical.

In `@app/routers/host.py`:
- Around line 17-23: The docstring on list_subscription_templates is inaccurate:
get_subscription_templates() only scans the CUSTOM_TEMPLATES_DIRECTORY and does
not include built-in templates. Update the docstring in the
list_subscription_templates handler to state that it returns subscription
templates found only in the custom templates directory (referencing
CUSTOM_TEMPLATES_DIRECTORY) and that built-in/default templates are excluded, or
alternatively modify get_subscription_templates to include built-ins if you
prefer that behavior; ensure the docstring and the behavior of
get_subscription_templates stay consistent.

In `@dashboard/public/statics/locales/fa.json`:
- Line 944: The Persian locale entry for the key "vlessRoute.info" is still in
English; replace the English string with an appropriate Persian translation for
"Optional route value for VLESS hosts. Must be exactly 4 hexadecimal characters
(0-9, a-f, A-F). Leave empty to use default routing." so the fa.json locale uses
Persian text for the vlessRoute.info key; ensure you preserve punctuation and
the requirement about 4 hexadecimal characters (0-9, a-f, A-F) in the translated
string.

In `@dashboard/public/statics/locales/ru.json`:
- Around line 1233-1238: The string for vlessRoute.info is still in English;
replace its value with a Russian translation while preserving the original
meaning and constraints. Update the vlessRoute.info entry so it reads something
like: "Необязательное значение route для хостов VLESS. Должно содержать ровно 4
шестнадцатеричных символа (0-9, a-f, A-F). Оставьте пустым, чтобы использовать
маршрутизацию по умолчанию." Ensure you only change the value for the
vlessRoute.info key and keep surrounding JSON structure intact.

In `@dashboard/public/statics/locales/zh.json`:
- Line 1050: The key "vlessRoute.info" currently contains an English message;
replace its value with a Chinese translation: "VLESS 主机的可选路由值。必须恰好为 4
个十六进制字符(0-9、a-f、A-F)。留空以使用默认路由。" so the Chinese locale entry matches the rest of
the file and conveys the same constraints and guidance.

In `@dashboard/src/service/api/index.ts`:
- Around line 1922-1925: The CreateHostSubscriptionTemplates type currently
allows arbitrary values via [key: string]: any; change that index signature to
[key: string]: string | null | undefined so additional template properties are
constrained to template filenames (strings) or nullable/undefined, keeping the
overall union with null intact; apply the same tightening to the analogous
subscription-template type declared around the other location (lines 2170-2173)
to ensure consistency with the Zod .passthrough() schema.

In `@tests/api/test_subscription_templates.py`:
- Around line 36-41: The test directly reassigns app.templates.env which isn't
restored; change that assignment to use monkeypatch.setattr so the original
Environment is auto-restored after the fixture. Specifically, replace the direct
write to app.templates.env with a monkeypatch.setattr call that sets
app.templates.env to a new app.templates.jinja2.Environment (with loader set to
app.templates.jinja2.FileSystemLoader([tmpdir, "app/templates"])) and ensure
filters are updated from app.templates.CUSTOM_FILTERS, so the monkeypatch
controls cleanup.

---

Outside diff comments:
In `@dashboard/src/components/hosts/hosts-list.tsx`:
- Around line 359-502: The drag-and-drop reorder builds hostsToUpdate in
handleDragEnd and omits subscription_templates, so calling modifyHosts
overwrites per-host template overrides; update the hostsToUpdate mapping (the
CreateHost objects constructed in the hostsToUpdate array inside handleDragEnd)
to include subscription_templates: host.subscription_templates || undefined (or
host.subscription_templates ?? undefined) when present so subscription template
overrides are preserved before calling modifyHosts.

---

Nitpick comments:
In `@dashboard/src/components/hosts/hosts-list.tsx`:
- Line 90: The code is using unnecessary casts to any for the
subscription_templates property; update the object construction in
hosts-list.tsx (where subscription_templates is assigned) to use
host.subscription_templates directly (remove the `(host as any)` cast) and do
the same for the other occurrence later in the file (previously `(host as
any).subscription_templates` around the second occurrence), relying on the
BaseHost type for correct typing.

In `@tests/api/test_subscription_templates.py`:
- Around line 106-123: The host deletion is inside the try block so a failing
assertion can leak the created host; change the test to always delete the host
in the finally block by capturing the created host id (e.g., set host_id = None
before the try, assign host_id = data["id"] after successful creation from
client.post) and then call client.delete(f"/api/host/{host_id}",
headers=auth_headers(access_token)) in the finally before calling
delete_core(access_token, core["id"]); update other CRUD tests with the same
pattern (use client.post, assign data["id"] to host_id, and ensure client.delete
runs in finally).

- Fix subscription_templates silently cleared on drag-and-drop reorder
- Narrow index signature type from `any` to `string | null`
- Fix migration docstring Revises field mismatch
- Fix inaccurate router docstring for list endpoint
- Translate vlessRoute.info for ru, zh, fa locales
- Remove unnecessary `as any` casts in hosts-list
- Move host cleanup into finally blocks in tests
@ImMohammad20000
Copy link
Contributor

this consepet is good but it not what i want to implement
templates should move from env to database like core-configs

@ogerman
Copy link
Author

ogerman commented Feb 17, 2026

Looks like translation issues was from the original code, but I've fixed them anyway

@ogerman
Copy link
Author

ogerman commented Feb 17, 2026

@ImMohammad20000 if we'll do so, how do we plan to migrate existing custom templates files (in current installations)? Import to db from JSON files via migrations?

@ImMohammad20000
Copy link
Contributor

Yes migrate existing files to database

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants