diff --git a/src/langbot/pkg/api/http/controller/groups/user.py b/src/langbot/pkg/api/http/controller/groups/user.py index af37172db..6ca6b9ca6 100644 --- a/src/langbot/pkg/api/http/controller/groups/user.py +++ b/src/langbot/pkg/api/http/controller/groups/user.py @@ -144,13 +144,6 @@ class UserRouterGroup(group.RouterGroup): try: redirect_uri = self._validate_space_redirect_uri(redirect_uri, bind=False) launch_workspace_uuid = quart.request.args.get('launch_workspace_uuid') - cloud_entry = quart.request.args.get('cloud_entry') == '1' - if ( - cloud_entry - and not launch_workspace_uuid - and getattr(getattr(self.ap, 'deployment', None), 'mode', 'oss') == 'cloud' - ): - return self.success(data={'authorize_url': self.ap.space_service.get_cloud_entry_url()}) if launch_workspace_uuid: if not getattr(getattr(self.ap, 'deployment', None), 'multi_workspace_enabled', False): return self.fail(1, 'Space launch requires Cloud mode') diff --git a/src/langbot/pkg/api/http/service/space.py b/src/langbot/pkg/api/http/service/space.py index 174cc965c..5be09a860 100644 --- a/src/langbot/pkg/api/http/service/space.py +++ b/src/langbot/pkg/api/http/service/space.py @@ -124,11 +124,6 @@ class SpaceService: params['state'] = state return f'{authorize_url}?{urlencode(params)}' - def get_cloud_entry_url(self) -> str: - """Return the Space-owned Cloud selector for a Cloud Account login.""" - - return f'{self._get_space_config()["url"].rstrip("/")}/cloud?environment=beta' - async def exchange_oauth_code( self, code: str, diff --git a/tests/integration/api/test_user_space_oauth.py b/tests/integration/api/test_user_space_oauth.py index 34c09451b..f608153a0 100644 --- a/tests/integration/api/test_user_space_oauth.py +++ b/tests/integration/api/test_user_space_oauth.py @@ -71,7 +71,7 @@ async def space_oauth_api(): application.space_service.get_oauth_authorize_url = Mock( side_effect=lambda redirect_uri, state: f'https://space.example/authorize?state={state}' ) - application.space_service.get_cloud_entry_url = Mock(return_value='https://space.example/cloud?environment=beta') + application.space_service.exchange_oauth_code = AsyncMock( return_value={ 'access_token': 'space-access-token', @@ -129,7 +129,7 @@ async def test_cloud_launch_state_is_server_issued_and_workspace_bound(space_oau @pytest.mark.asyncio -async def test_cloud_login_entry_redirects_to_space_workspace_launcher(space_oauth_api): +async def test_cloud_login_entry_starts_stateful_space_oauth(space_oauth_api): application, client = space_oauth_api application.deployment.mode = 'cloud' @@ -143,9 +143,9 @@ async def test_cloud_login_entry_redirects_to_space_workspace_launcher(space_oau ) assert response.status_code == 200 - assert (await response.get_json())['data']['authorize_url'] == ('https://space.example/cloud?environment=beta') - application.space_service.get_cloud_entry_url.assert_called_once_with() - application.user_service.issue_space_oauth_state.assert_not_awaited() + authorize_url = (await response.get_json())['data']['authorize_url'] + assert authorize_url.startswith('https://space.example/authorize?state=') + application.user_service.issue_space_oauth_state.assert_awaited_once_with('login') @pytest.mark.asyncio diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index 41aac21cb..c997f2332 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -1385,17 +1385,13 @@ export class BackendClient extends BaseHttpClient { } // ============ Space OAuth API (Redirect Flow) ============ - public getSpaceAuthorizeUrl( - redirectUri: string, - options?: { cloudEntry?: boolean }, - ): Promise<{ + public getSpaceAuthorizeUrl(redirectUri: string): Promise<{ authorize_url: string; }> { return this.get( '/api/v1/user/space/authorize-url', { redirect_uri: redirectUri, - ...(options?.cloudEntry ? { cloud_entry: '1' } : {}), }, { skipWorkspace: true }, ); diff --git a/web/src/app/login/page.tsx b/web/src/app/login/page.tsx index 061a21988..48436017f 100644 --- a/web/src/app/login/page.tsx +++ b/web/src/app/login/page.tsx @@ -202,13 +202,7 @@ export default function Login() { try { const currentOrigin = window.location.origin; const redirectUri = `${currentOrigin}/auth/space/callback`; - const response = await httpClient.getSpaceAuthorizeUrl(redirectUri, { - // Cloud Accounts must be launched from Space so a first visit can - // lazily create and project the personal Workspace. Invitation login - // remains on the OAuth callback path because it targets the invited - // Workspace instead. - cloudEntry: !getPendingInvitationToken(), - }); + const response = await httpClient.getSpaceAuthorizeUrl(redirectUri); window.location.href = response.authorize_url; } catch { toast.error(t('common.spaceLoginFailed')); diff --git a/web/tests/unit/cloud-new-account-entry.test.mjs b/web/tests/unit/cloud-new-account-entry.test.mjs index 5420da59e..13ff61e7a 100644 --- a/web/tests/unit/cloud-new-account-entry.test.mjs +++ b/web/tests/unit/cloud-new-account-entry.test.mjs @@ -7,11 +7,12 @@ const source = fs.readFileSync( 'utf8', ); -test('normal Cloud login enters through the Space Workspace launcher', () => { - assert.match(source, /cloudEntry:\s*!getPendingInvitationToken\(\)/); - assert.match(source, /getSpaceAuthorizeUrl\(redirectUri,\s*\{/); +test('normal Cloud login starts stateful Space OAuth', () => { + assert.match(source, /getSpaceAuthorizeUrl\(redirectUri\)/); + assert.doesNotMatch(source, /cloudEntry/); }); test('invitation login remains on the OAuth callback path', () => { - assert.match(source, /cloudEntry:\s*!getPendingInvitationToken\(\)/); + assert.match(source, /getPendingInvitationToken\(\)/); + assert.match(source, /getSpaceAuthorizeUrl\(redirectUri\)/); });