diff --git a/backend/bot/app/web/webapp/guides.py b/backend/bot/app/web/webapp/guides.py index 071fbb3..a07f536 100644 --- a/backend/bot/app/web/webapp/guides.py +++ b/backend/bot/app/web/webapp/guides.py @@ -38,8 +38,21 @@ async def public_subscription_guides_route(request: web.Request) -> web.Response if not share_token: return web.json_response({"ok": False, "error": "invalid_share_token"}, status=404) - status = await _subscription_guides_status_shared(request.app) subscription = await _public_subscription_payload(request, share_token) + if not subscription.get("active"): + return web.json_response( + { + "ok": False, + "enabled": False, + "config": None, + "source": None, + "subscription": subscription, + "error": "subscription_unavailable", + }, + status=404, + ) + + status = await _subscription_guides_status_shared(request.app) payload = { "enabled": bool(status.get("enabled")), "config": status.get("config") if status.get("enabled") else None, diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index b86f5db..6216ecf 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -1064,6 +1064,15 @@ openExternalLink(url); } + function openPublicConnectLink() { + const url = publicInstallSubscription?.connect_url || publicInstallSubscription?.config_link; + if (!url) { + showToast(t("wa_connect_link_unavailable")); + return; + } + openExternalLink(url); + } + function openInstallOrConnect() { if (canUseInstallGuides()) { goInstall(); @@ -1363,7 +1372,7 @@ user={{}} subscription={publicInstallSubscription || { install_share_token: publicInstallToken }} {goHome} - {openConnectLink} + openConnectLink={openPublicConnectLink} {openExternalLink} {copyText} {t} diff --git a/frontend/src/lib/webapp/stores/installGuidesStore.js b/frontend/src/lib/webapp/stores/installGuidesStore.js index be91b5e..ef0b34c 100644 --- a/frontend/src/lib/webapp/stores/installGuidesStore.js +++ b/frontend/src/lib/webapp/stores/installGuidesStore.js @@ -21,7 +21,12 @@ export function createInstallGuidesStore({ api, t, showToast }) { }); if (!force && snapshot?.loaded) return snapshot; const promise = (async () => { - state.update((s) => ({ ...s, loading: true, error: "" })); + state.update((s) => ({ + ...s, + loading: true, + loaded: force ? false : s.loaded, + error: "", + })); try { const response = await api(path); const next = { diff --git a/tests/test_subscription_guides_route.py b/tests/test_subscription_guides_route.py index a6deab6..8c00085 100644 --- a/tests/test_subscription_guides_route.py +++ b/tests/test_subscription_guides_route.py @@ -206,6 +206,71 @@ class SubscriptionGuidesRouteTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(body["subscription"]["install_share_token"], share_token) panel_service.get_user_by_uuid.assert_awaited_once_with("panel-user") + async def test_public_route_rejects_unknown_share_token_without_loading_config(self): + share_token = "8f559061460e8fede78ef18dce887236" + panel_service = SimpleNamespace( + get_subscription_page_config_list=AsyncMock(), + get_subscription_page_config_by_uuid=AsyncMock(), + get_user_by_uuid=AsyncMock(), + ) + request = self._request( + self._settings(SUBSCRIPTION_MINI_APP_URL="https://app.example.test/app"), + panel_service, + match_info={"share_token": share_token}, + ) + + with patch.object( + guides.subscription_dal, + "get_subscription_by_install_share_token", + AsyncMock(return_value=None), + ): + response = await guides.public_subscription_guides_route(request) + + body = json.loads(response.text) + self.assertEqual(response.status, 404) + self.assertFalse(body["ok"]) + self.assertEqual(body["error"], "subscription_unavailable") + self.assertFalse(body["enabled"]) + self.assertIsNone(body["config"]) + self.assertEqual(body["subscription"]["install_share_token"], share_token) + self.assertFalse(body["subscription"]["active"]) + panel_service.get_subscription_page_config_list.assert_not_called() + panel_service.get_subscription_page_config_by_uuid.assert_not_called() + panel_service.get_user_by_uuid.assert_not_called() + + async def test_public_route_rejects_inactive_share_token_without_panel_user_lookup(self): + share_token = "8f559061460e8fede78ef18dce887236" + panel_service = SimpleNamespace( + get_subscription_page_config_list=AsyncMock(), + get_subscription_page_config_by_uuid=AsyncMock(), + get_user_by_uuid=AsyncMock(), + ) + request = self._request( + self._settings(SUBSCRIPTION_MINI_APP_URL="https://app.example.test/app"), + panel_service, + match_info={"share_token": share_token}, + ) + local_sub = SimpleNamespace( + panel_user_uuid="panel-user", + install_share_token=share_token, + is_active=False, + end_date=datetime.now(timezone.utc) + timedelta(days=3), + ) + + with patch.object( + guides.subscription_dal, + "get_subscription_by_install_share_token", + AsyncMock(return_value=local_sub), + ): + response = await guides.public_subscription_guides_route(request) + + body = json.loads(response.text) + self.assertEqual(response.status, 404) + self.assertFalse(body["ok"]) + self.assertEqual(body["error"], "subscription_unavailable") + self.assertFalse(body["subscription"]["active"]) + panel_service.get_user_by_uuid.assert_not_called() + if __name__ == "__main__": unittest.main()