feat(auth-portal): 添加部署控制服务调用支持

- 导入callDeployControl和normalizeTokenResponse函数用于处理部署配置
- 新增hasTargetFrontendUrl函数检查响应中是否存在目标前端URL
- 在注册流程中添加部署路由解析逻辑,当缺少前端URL时调用部署控制服务获取配置
- 更新normalizeTokenResponse函数以支持从实例对象中提取URL配置

refactor(runtime-control): 增强令牌响应标准化功能

- 扩展normalizeTokenResponse函数支持从instance对象中获取URL配置
- 添加对instance字段的支持,优先级为routing > instance配置
- 支持从instance中提取frontend_base_url、api_base_url和public_url

build(tsconfig): 排除测试文件构建

- 在tsconfig.json中添加排除规则,排除**/*.test.ts和**/*.test.tsx文件
- 避免测试文件参与生产构建

refactor(authz-service): 优化Python后端令牌响应处理

- 更新_normalize_portal_token_response函数支持从实例对象中提取URL配置
- 重构URL优先级逻辑,支持routing和instance双重数据源
- 改进代码可读性,将复杂的URL赋值逻辑拆分为多行
This commit is contained in:
2026-06-16 10:17:30 +08:00
parent aadbe80a23
commit b736fc9c81
5 changed files with 91 additions and 11 deletions

View File

@ -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) });
}

View File

@ -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');
});
});

View File

@ -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 = {

View File

@ -36,7 +36,8 @@
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
"node_modules",
"**/*.test.ts",
"**/*.test.tsx"
]
}

View File

@ -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,
}