diff --git a/backend/bot/app/web/admin_settings_manifest.py b/backend/bot/app/web/admin_settings_manifest.py index 86e0b26..006ce91 100644 --- a/backend/bot/app/web/admin_settings_manifest.py +++ b/backend/bot/app/web/admin_settings_manifest.py @@ -30,6 +30,11 @@ class SettingField: i18n_label_key: Optional[str] = None i18n_description_key: Optional[str] = None i18n_subsection_key: Optional[str] = None + webhook_path: Optional[str] = None + webhook_requires_base_url: bool = False + webhook_provider_id: Optional[str] = None + webhook_hint_i18n_key: Optional[str] = None + webhook_hint: str = "" SETTINGS_MANIFEST: List[SettingField] = [ @@ -84,44 +89,44 @@ SETTINGS_MANIFEST: List[SettingField] = [ SettingField( "PANEL_API_URL", "url", - "general", + "remnawave", "URL API Remnawave", "Например, https://panel.example.com/api.", - subsection="Remnawave", ), SettingField( "PANEL_API_KEY", "string", - "general", + "remnawave", "API-ключ Remnawave", "Секретный ключ API панели.", secret=True, - subsection="Remnawave", ), SettingField( "PANEL_WEBHOOK_SECRET", "string", - "general", + "remnawave", "Секрет вебхуков Remnawave", "Используется для проверки входящих вебхуков панели.", secret=True, - subsection="Remnawave", + webhook_path="/webhook/panel", + webhook_requires_base_url=True, + webhook_provider_id="remnawave", + webhook_hint_i18n_key="admin_settings_panel_webhook_url_hint", + webhook_hint="Use this URL as WEBHOOK_URL in Remnawave Panel.", ), SettingField( "USER_SQUAD_UUIDS", "string", - "general", + "remnawave", "Internal Squads по умолчанию", "UUID через запятую для legacy-режима без JSON-каталога тарифов.", - subsection="Remnawave", ), SettingField( "USER_EXTERNAL_SQUAD_UUID", "string", - "general", + "remnawave", "External Squad по умолчанию", "Необязательный UUID External Squad для новых пользователей.", - subsection="Remnawave", ), # ─── Web app appearance ──────────────────────────────────────── SettingField( @@ -707,6 +712,7 @@ def manifest_payload() -> List[dict]: sections_order = { "general": 1, "appearance": 2, + "remnawave": 3, "pricing": 11, "payments": 4, "trial": 5, @@ -773,6 +779,15 @@ def manifest_payload() -> List[dict]: item["default"] = default_value if webhook_metadata: item.update(webhook_metadata) + if field.webhook_path: + item["webhook_path"] = field.webhook_path + item["webhook_requires_base_url"] = field.webhook_requires_base_url + if field.webhook_provider_id: + item["provider_id"] = field.webhook_provider_id + if field.webhook_hint_i18n_key: + item["webhook_hint_i18n_key"] = field.webhook_hint_i18n_key + if field.webhook_hint: + item["webhook_hint"] = field.webhook_hint if field.choices: item["choices"] = [ { diff --git a/frontend/src/admin/sections/SettingsSection.svelte b/frontend/src/admin/sections/SettingsSection.svelte index 2c6ef95..d8b6683 100644 --- a/frontend/src/admin/sections/SettingsSection.svelte +++ b/frontend/src/admin/sections/SettingsSection.svelte @@ -198,6 +198,8 @@ url, requiresBaseUrl: Boolean(field.webhook_requires_base_url), baseConfigured: field.webhook_base_url_configured !== false, + hintI18nKey: field.webhook_hint_i18n_key || "", + hintFallback: field.webhook_hint || "", }; } @@ -333,6 +335,7 @@ function sectionTitle(id) { const map = { general: "Общие", + remnawave: "Remnawave Panel", appearance: "Внешний вид", pricing: "Тарифы и цены", payments: "Платёжные системы", @@ -415,9 +418,9 @@ {webhook.url ? at( - "settings_provider_webhook_url_hint", + adminLocaleKey(webhook.hintI18nKey || "settings_provider_webhook_url_hint"), {}, - "Use this URL in the provider webhook settings." + webhook.hintFallback || "Use this URL in the provider webhook settings." ) : at( "settings_provider_webhook_base_missing", diff --git a/locales/en.json b/locales/en.json index 427be32..bde5b29 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1017,6 +1017,7 @@ "admin_content": "Content", "admin_logs_empty": "No entries", "admin_settings_section_general": "General", + "admin_settings_section_remnawave": "Remnawave Panel", "admin_settings_section_appearance": "Appearance", "admin_settings_section_pricing": "Legacy tariff compatibility", "admin_settings_section_payments": "Payment systems", @@ -1039,6 +1040,7 @@ "admin_settings_subsection_heleket": "Heleket", "admin_settings_provider_webhook_url": "Webhook URL", "admin_settings_provider_webhook_url_hint": "Use this URL in the provider webhook settings.", + "admin_settings_panel_webhook_url_hint": "Use this URL as WEBHOOK_URL in Remnawave Panel.", "admin_settings_provider_webhook_base_missing": "Set WEBHOOK_BASE_URL in .env to show the full URL for {path}.", "admin_settings_provider_admin_only_label": "Only for admins", "admin_settings_provider_admin_only_description": "Shows this provider only to admins. Webhooks and payment status handling remain active for test payments.", @@ -1437,7 +1439,7 @@ "admin_settings_field_panel_api_key_label": "Remnawave API key", "admin_settings_field_panel_api_key_description": "Secret API key for the panel.", "admin_settings_field_panel_webhook_secret_label": "Remnawave webhook secret", - "admin_settings_field_panel_webhook_secret_description": "Used to verify incoming panel webhooks.", + "admin_settings_field_panel_webhook_secret_description": "Set the secret in Remnawave Panel and paste the same value here to verify incoming panel webhooks.", "admin_settings_field_user_squad_uuids_label": "Default Internal Squads", "admin_settings_field_user_squad_uuids_description": "Comma-separated UUIDs for the legacy mode without a JSON tariff catalog.", "admin_settings_field_user_external_squad_uuid_label": "Default External Squad", diff --git a/locales/ru.json b/locales/ru.json index aaf7564..15cda73 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1017,6 +1017,7 @@ "admin_content": "Контент", "admin_logs_empty": "Записей нет", "admin_settings_section_general": "Общие", + "admin_settings_section_remnawave": "Remnawave Panel", "admin_settings_section_appearance": "Внешний вид", "admin_settings_section_pricing": "Совместимость с legacy-тарифами", "admin_settings_section_payments": "Платёжные системы", @@ -1039,6 +1040,7 @@ "admin_settings_subsection_heleket": "Heleket", "admin_settings_provider_webhook_url": "Webhook URL", "admin_settings_provider_webhook_url_hint": "Укажите этот адрес в настройках вебхуков провайдера.", + "admin_settings_panel_webhook_url_hint": "Укажите этот адрес как WEBHOOK_URL в Remnawave Panel.", "admin_settings_provider_webhook_base_missing": "Укажите WEBHOOK_BASE_URL в .env, чтобы увидеть полный адрес для {path}.", "admin_settings_provider_admin_only_label": "Только для админов", "admin_settings_provider_admin_only_description": "Показывает провайдер только администраторам. Вебхуки и обработка статусов остаются активными для тестовых платежей.", @@ -1437,7 +1439,7 @@ "admin_settings_field_panel_api_key_label": "API-ключ Remnawave", "admin_settings_field_panel_api_key_description": "Секретный ключ API панели.", "admin_settings_field_panel_webhook_secret_label": "Секрет вебхуков Remnawave", - "admin_settings_field_panel_webhook_secret_description": "Используется для проверки входящих вебхуков панели.", + "admin_settings_field_panel_webhook_secret_description": "Задайте секрет в Remnawave Panel и вставьте то же значение здесь для проверки входящих вебхуков панели.", "admin_settings_field_user_squad_uuids_label": "Internal Squads по умолчанию", "admin_settings_field_user_squad_uuids_description": "UUID через запятую для legacy-режима без JSON-каталога тарифов.", "admin_settings_field_user_external_squad_uuid_label": "External Squad по умолчанию", diff --git a/tests/test_admin_settings_manifest_i18n.py b/tests/test_admin_settings_manifest_i18n.py index 5e58c76..2144100 100644 --- a/tests/test_admin_settings_manifest_i18n.py +++ b/tests/test_admin_settings_manifest_i18n.py @@ -230,6 +230,32 @@ def test_payment_provider_settings_include_webhook_metadata(): assert "webhook_path" not in manifest["PAYMENT_STARS_WEBAPP_LABEL_RU"] +def test_remnawave_settings_include_panel_webhook_metadata(): + manifest = _manifest_by_key() + field = manifest["PANEL_WEBHOOK_SECRET"] + remnawave_keys = ( + "PANEL_API_URL", + "PANEL_API_KEY", + "PANEL_WEBHOOK_SECRET", + "USER_SQUAD_UUIDS", + "USER_EXTERNAL_SQUAD_UUID", + ) + + assert field["webhook_path"] == "/webhook/panel" + assert field["webhook_requires_base_url"] is True + assert field["provider_id"] == "remnawave" + assert field["webhook_hint_i18n_key"] == "admin_settings_panel_webhook_url_hint" + for setting_key in remnawave_keys: + assert manifest[setting_key]["section"] == "remnawave" + assert manifest[setting_key]["section_order"] == 3 + assert manifest[setting_key]["subsection"] is None + + for language in ("ru", "en"): + messages = _locale(language) + assert "admin_settings_section_remnawave" in messages + assert field["webhook_hint_i18n_key"] in messages + + def test_payment_provider_admin_only_toggles_are_mutually_exclusive(): manifest = _manifest_by_key() diff --git a/tests/test_security.py b/tests/test_security.py index fa70525..a3548d3 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -760,10 +760,13 @@ class AdminSettingsSecurityTests(unittest.IsolatedAsyncioTestCase): response = await admin_api.admin_settings_get_route(request) payload = json.loads(response.text) + section_ids = [section["id"] for section in payload["sections"]] fields = { field["key"]: field for section in payload["sections"] for field in section["fields"] } + self.assertLess(section_ids.index("general"), section_ids.index("remnawave")) + self.assertLess(section_ids.index("remnawave"), section_ids.index("payments")) self.assertEqual( fields["FREEKASSA_ENABLED"]["webhook_url"], "https://web.tnnl.cc/webhook/freekassa", @@ -773,6 +776,11 @@ class AdminSettingsSecurityTests(unittest.IsolatedAsyncioTestCase): fields["PAYMENT_PLATEGA_CRYPTO_WEBAPP_LABEL_RU"]["webhook_url"], "https://web.tnnl.cc/webhook/platega", ) + self.assertEqual( + fields["PANEL_WEBHOOK_SECRET"]["webhook_url"], + "https://web.tnnl.cc/webhook/panel", + ) + self.assertTrue(fields["PANEL_WEBHOOK_SECRET"]["webhook_requires_base_url"]) self.assertNotIn("webhook_url", fields["PAYMENT_STARS_WEBAPP_LABEL_RU"])