diff --git a/auth-portal/src/app/api/runtime/register/route.ts b/auth-portal/src/app/api/runtime/register/route.ts index fcb1142..67eceb1 100644 --- a/auth-portal/src/app/api/runtime/register/route.ts +++ b/auth-portal/src/app/api/runtime/register/route.ts @@ -2,7 +2,13 @@ import { NextRequest, NextResponse } from 'next/server'; import type { TokenResponse } from '@/types/auth'; import { normalizePortalLocale, pickPortalText } from '@/lib/i18n/core'; -import { HttpError, REGISTER_REQUEST_TIMEOUT_MS, callAuthzService } from '@/lib/runtime-control'; +import { + HttpError, + REGISTER_REQUEST_TIMEOUT_MS, + callAuthzService, + callDeployControl, + normalizeTokenResponse, +} from '@/lib/runtime-control'; function errorStatus(error: unknown): number { if (error instanceof HttpError) { @@ -18,6 +24,15 @@ function errorDetail(error: unknown): string { return error instanceof Error ? error.message : 'registration failed'; } +function hasTargetFrontendUrl(response: TokenResponse): boolean { + return Boolean( + response.backend_connection?.frontend_base_url || + response.backend_connection?.public_base_url || + response.backend_connection?.api_base_url || + response.local_backend?.public_base_url + ); +} + export async function POST(request: NextRequest) { const locale = normalizePortalLocale( request.cookies.get('beaver_locale')?.value || @@ -46,7 +61,18 @@ export async function POST(request: NextRequest) { password, }, REGISTER_REQUEST_TIMEOUT_MS); - return NextResponse.json(response); + if (hasTargetFrontendUrl(response)) { + return NextResponse.json(response); + } + + const routing = await callDeployControl<{ + api_base_url?: string; + frontend_base_url?: string; + public_url?: string; + instance?: unknown; + }>('/api/instances/resolve', { username }); + + return NextResponse.json(normalizeTokenResponse(response, routing)); } catch (error) { return NextResponse.json({ detail: errorDetail(error) }, { status: errorStatus(error) }); } diff --git a/auth-portal/src/lib/runtime-control.test.ts b/auth-portal/src/lib/runtime-control.test.ts new file mode 100644 index 0000000..c6b4db5 --- /dev/null +++ b/auth-portal/src/lib/runtime-control.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; + +import { normalizeTokenResponse } from './runtime-control'; + +describe('normalizeTokenResponse', () => { + it('uses nested instance routing when top-level route URLs are missing', () => { + const response = normalizeTokenResponse({ + access_token: 'token', + refresh_token: '', + token_type: 'bearer', + user_id: 'alice', + username: 'alice', + role: 'owner', + handoff_code: 'handoff-1', + }, { + instance: { + public_url: 'workspace.example.com:8088', + frontend_base_url: 'workspace.example.com:8088', + }, + }); + + expect(response.backend_connection?.frontend_base_url).toBe('workspace.example.com:8088'); + expect(response.backend_connection?.public_base_url).toBe('workspace.example.com:8088'); + }); +}); diff --git a/auth-portal/src/lib/runtime-control.ts b/auth-portal/src/lib/runtime-control.ts index f3f494a..abd72e8 100644 --- a/auth-portal/src/lib/runtime-control.ts +++ b/auth-portal/src/lib/runtime-control.ts @@ -107,11 +107,20 @@ export function normalizeTokenResponse( frontend_base_url?: unknown; api_base_url?: unknown; public_url?: unknown; + instance?: unknown; } ): TokenResponse { - const frontendBaseUrl = asString(routing.frontend_base_url); - const apiBaseUrl = asString(routing.api_base_url) || asString(routing.public_url); - const publicUrl = asString(routing.public_url) || apiBaseUrl; + const instance = asObject(routing.instance); + const frontendBaseUrl = + asString(routing.frontend_base_url) || + asString(instance.frontend_base_url) || + asString(instance.public_url); + const apiBaseUrl = + asString(routing.api_base_url) || + asString(instance.api_base_url) || + asString(routing.public_url) || + asString(instance.public_url); + const publicUrl = asString(routing.public_url) || asString(instance.public_url) || apiBaseUrl; const backendConnection = asObject(response.backend_connection); const mergedBackendConnection = { diff --git a/auth-portal/src/tsconfig.json b/auth-portal/src/tsconfig.json index e5439d6..11fd9f5 100644 --- a/auth-portal/src/tsconfig.json +++ b/auth-portal/src/tsconfig.json @@ -36,7 +36,8 @@ ".next/types/**/*.ts" ], "exclude": [ - "node_modules" + "node_modules", + "**/*.test.ts", + "**/*.test.tsx" ] } - diff --git a/authz-service/src/app/main.py b/authz-service/src/app/main.py index ec91bc2..9e8d460 100644 --- a/authz-service/src/app/main.py +++ b/authz-service/src/app/main.py @@ -187,14 +187,33 @@ def _normalize_portal_token_response( response: dict[str, Any], routing: dict[str, Any], ) -> dict[str, Any]: - frontend_base_url = _as_string(routing.get("frontend_base_url")) - api_base_url = _as_string(routing.get("api_base_url")) or _as_string(routing.get("public_url")) - public_url = _as_string(routing.get("public_url")) or api_base_url + instance = _as_object(routing.get("instance")) + frontend_base_url = ( + _as_string(routing.get("frontend_base_url")) + or _as_string(instance.get("frontend_base_url")) + or _as_string(instance.get("public_url")) + ) + api_base_url = ( + _as_string(routing.get("api_base_url")) + or _as_string(instance.get("api_base_url")) + or _as_string(routing.get("public_url")) + or _as_string(instance.get("public_url")) + ) + public_url = ( + _as_string(routing.get("public_url")) + or _as_string(instance.get("public_url")) + or api_base_url + ) backend_connection = _as_object(response.get("backend_connection")) merged_backend_connection = { **backend_connection, - "frontend_base_url": _as_string(backend_connection.get("frontend_base_url")) or frontend_base_url or public_url or None, + "frontend_base_url": ( + _as_string(backend_connection.get("frontend_base_url")) + or frontend_base_url + or public_url + or None + ), "api_base_url": _as_string(backend_connection.get("api_base_url")) or api_base_url or public_url or None, "public_base_url": _as_string(backend_connection.get("public_base_url")) or public_url or api_base_url or None, }