From a7a7218afec40ee85e4d7708a3ef32137c0eafa3 Mon Sep 17 00:00:00 2001 From: dadachann <185672915+dadachann@users.noreply.github.com> Date: Sun, 2 Aug 2026 09:08:49 +0000 Subject: [PATCH] fix(cloud): accept invitations with current account --- .../pkg/api/http/controller/groups/user.py | 4 +- tests/integration/api/test_smoke.py | 24 ++++++ web/src/app/infra/http/BackendClient.ts | 1 + web/src/app/invitations/accept/page.tsx | 20 ++++- web/tests/e2e/invitations.spec.ts | 77 +++++++++++++++++++ 5 files changed, 124 insertions(+), 2 deletions(-) diff --git a/src/langbot/pkg/api/http/controller/groups/user.py b/src/langbot/pkg/api/http/controller/groups/user.py index 560f5988c..49b59906b 100644 --- a/src/langbot/pkg/api/http/controller/groups/user.py +++ b/src/langbot/pkg/api/http/controller/groups/user.py @@ -311,8 +311,10 @@ class UserRouterGroup(group.RouterGroup): return self.success(data={'initialized': False}) capabilities = await self.ap.user_service.get_login_capabilities() - if getattr(getattr(self.ap, 'deployment', None), 'mode', 'oss') == 'cloud': + cloud_mode = getattr(getattr(self.ap, 'deployment', None), 'mode', 'oss') == 'cloud' + if cloud_mode: capabilities['password_login_enabled'] = False + capabilities['authenticated_invitation_acceptance_enabled'] = cloud_mode return self.success(data={'initialized': True, **capabilities}) @self.route('/set-password', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) diff --git a/tests/integration/api/test_smoke.py b/tests/integration/api/test_smoke.py index 9c2927503..dfd5054f2 100644 --- a/tests/integration/api/test_smoke.py +++ b/tests/integration/api/test_smoke.py @@ -9,6 +9,8 @@ Run: uv run pytest tests/integration/api/test_smoke.py -q from __future__ import annotations +from types import SimpleNamespace + import pytest from unittest.mock import MagicMock, AsyncMock, Mock @@ -304,12 +306,34 @@ class TestUserInitEndpoint: data = await response.get_json() assert data['data'] == { 'initialized': True, + 'authenticated_invitation_acceptance_enabled': False, 'password_login_enabled': True, 'space_login_enabled': False, } fake_api_app.user_service.get_login_capabilities.assert_awaited_once_with() fake_api_app.user_service.get_first_user.assert_not_awaited() + @pytest.mark.asyncio + async def test_account_info_enables_authenticated_invitation_acceptance_in_cloud( + self, quart_test_client, fake_api_app + ): + fake_api_app.deployment = SimpleNamespace(mode='cloud') + fake_api_app.user_service.is_initialized.return_value = True + fake_api_app.user_service.get_login_capabilities = AsyncMock( + return_value={'password_login_enabled': True, 'space_login_enabled': True} + ) + + response = await quart_test_client.get('/api/v1/user/account-info') + + assert response.status_code == 200 + data = await response.get_json() + assert data['data'] == { + 'initialized': True, + 'authenticated_invitation_acceptance_enabled': True, + 'password_login_enabled': False, + 'space_login_enabled': True, + } + @pytest.mark.asyncio async def test_recovery_key_resets_any_existing_account(self, quart_test_client, fake_api_app, monkeypatch): fake_api_app.user_service.is_initialized.return_value = True diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index 63d6f37c8..03e0899f6 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -1179,6 +1179,7 @@ export class BackendClient extends BaseHttpClient { public getAccountInfo(): Promise<{ initialized: boolean; + authenticated_invitation_acceptance_enabled?: boolean; password_login_enabled?: boolean; space_login_enabled?: boolean; }> { diff --git a/web/src/app/invitations/accept/page.tsx b/web/src/app/invitations/accept/page.tsx index e8d412ecc..0d83331b5 100644 --- a/web/src/app/invitations/accept/page.tsx +++ b/web/src/app/invitations/accept/page.tsx @@ -93,6 +93,10 @@ export default function AcceptInvitationPage() { const [confirmPassword, setConfirmPassword] = useState(''); const [passwordRegistrationEnabled, setPasswordRegistrationEnabled] = useState(false); + const [ + authenticatedInvitationAcceptanceEnabled, + setAuthenticatedInvitationAcceptanceEnabled, + ] = useState(false); useEffect(() => { const handleHashChange = () => setInvitationHash(window.location.hash); @@ -113,6 +117,9 @@ export default function AcceptInvitationPage() { .getAccountInfo() .then((info) => { setPasswordRegistrationEnabled(info.password_login_enabled !== false); + setAuthenticatedInvitationAcceptanceEnabled( + info.authenticated_invitation_acceptance_enabled === true, + ); }) .catch(() => setPasswordRegistrationEnabled(false)); if (!invitationToken) { @@ -304,7 +311,18 @@ export default function AcceptInvitationPage() { )} - {hasLoginToken ? ( + {hasLoginToken && authenticatedInvitationAcceptanceEnabled ? ( + + ) : hasLoginToken ? (