feat: add wizard auth selector
This commit is contained in:
@@ -2,20 +2,20 @@
|
||||
|
||||
## Current
|
||||
|
||||
### `feat/secrets-ui`
|
||||
### `feat/wizard-auth-selector`
|
||||
|
||||
Status: completed
|
||||
|
||||
DoD:
|
||||
- separate `Secrets` page exists next to `API Keys`
|
||||
- UI supports list/create/rotate/delete for workspace secrets on live backend endpoints
|
||||
- UI shows read-only auth profile usage references for each secret
|
||||
- no UI flow pretends to reveal plaintext after create/rotate when backend does not return it
|
||||
- wizard step 2 uses auth selector instead of `${secrets.*}` placeholders
|
||||
- upstream auth is serialized into `execution_config.auth_profile_ref`
|
||||
- raw auth headers are removed from primary wizard UX
|
||||
- quick-create secret and auth profile flow works against live backend endpoints
|
||||
- node checks, UI docker build, and workspace check pass
|
||||
|
||||
## Next
|
||||
|
||||
- `feat/wizard-auth-selector`
|
||||
- `feat/manual-regression-pass`
|
||||
|
||||
## Backlog
|
||||
|
||||
|
||||
@@ -1358,6 +1358,11 @@
|
||||
color: #58a6ff;
|
||||
border: 1px solid rgba(88, 166, 255, 0.2);
|
||||
}
|
||||
.upstream-auth-badge.auth-basic {
|
||||
background: rgba(240, 173, 78, 0.12);
|
||||
color: #f0ad4e;
|
||||
border: 1px solid rgba(240, 173, 78, 0.2);
|
||||
}
|
||||
.upstream-auth-badge.auth-apikey {
|
||||
background: rgba(188, 140, 255, 0.1);
|
||||
color: #bc8cff;
|
||||
|
||||
@@ -239,6 +239,41 @@
|
||||
|
||||
<script src="../../js/streaming-form.js"></script>
|
||||
<script src="../../js/stream-test-run.js"></script>
|
||||
<div class="modal-overlay" id="quick-secret-modal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" data-i18n="wizard.step2.quick_secret_title">Quick create secret</span>
|
||||
<button class="modal-close" id="quick-secret-close-btn" type="button">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
||||
<line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" style="display:grid; gap:14px;">
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="wizard.step2.quick_secret_name">Secret name</label>
|
||||
<input class="field-input" id="quick-secret-name" type="text" autocomplete="off">
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="wizard.step2.quick_secret_kind">Secret kind</label>
|
||||
<select class="field-select" id="quick-secret-kind">
|
||||
<option value="token" data-i18n="secrets.kind.token">Token</option>
|
||||
<option value="header" data-i18n="secrets.kind.header">Header value</option>
|
||||
<option value="generic" data-i18n="secrets.kind.generic">Generic JSON</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="wizard.step2.quick_secret_value">Secret value</label>
|
||||
<input class="field-input" id="quick-secret-value" type="text" autocomplete="off">
|
||||
<div class="field-hint" data-i18n="wizard.step2.quick_secret_hint">The plaintext is encrypted immediately and will not be returned again.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" id="quick-secret-cancel-btn" type="button" data-i18n="btn.cancel">Cancel</button>
|
||||
<button class="btn-primary" id="quick-secret-submit-btn" type="button" data-i18n="wizard.step2.quick_secret_submit">Create secret</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="../../js/wizard.js"></script>
|
||||
<script src="../../js/nav.js"></script>
|
||||
|
||||
|
||||
@@ -84,21 +84,88 @@
|
||||
<div class="form-group">
|
||||
<label class="form-label"><span data-i18n="wizard.step2.base_url">Base URL</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
|
||||
<input class="form-input input-mono" id="new-upstream-url" type="text" placeholder="https://api.example.com" autocomplete="off" spellcheck="false">
|
||||
<div class="form-hint" data-i18n="wizard.step2.base_url_hint">Root URL — no trailing slash. Supports ${secrets.*}.</div>
|
||||
<div class="form-hint" data-i18n="wizard.step2.base_url_hint">Root URL without a trailing slash. Auth is configured separately below.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.auth_headers">Auth headers</label>
|
||||
<label class="form-label" data-i18n="wizard.step2.auth_selector">Upstream auth</label>
|
||||
<select class="form-select" id="new-upstream-auth-mode" onchange="updateUpstreamAuthUi()">
|
||||
<option value="none" data-i18n="wizard.step2.auth_mode.none">No auth</option>
|
||||
<option value="existing" data-i18n="wizard.step2.auth_mode.existing">Use existing auth profile</option>
|
||||
<option value="create" data-i18n="wizard.step2.auth_mode.create">Create auth profile now</option>
|
||||
</select>
|
||||
<div class="form-hint" data-i18n="wizard.step2.auth_selector_hint">Use secrets-backed auth profiles instead of embedding credential headers in the upstream definition.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="upstream-auth-existing-group" style="display:none;">
|
||||
<label class="form-label" data-i18n="wizard.step2.auth_profile">Auth profile</label>
|
||||
<select class="form-select" id="new-upstream-auth-profile"></select>
|
||||
<div class="form-hint" id="new-upstream-auth-profile-hint" data-i18n="wizard.step2.auth_profile_hint">Selected profile will be resolved at runtime and attached through `execution_config.auth_profile_ref`.</div>
|
||||
</div>
|
||||
|
||||
<div class="config-card" id="upstream-auth-create-group" style="display:none; margin-top: 6px;">
|
||||
<div class="config-card-header">
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step2.create_profile_title">Create auth profile</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step2.create_profile_subtitle">Create a reusable profile backed by encrypted workspace secrets.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-card-body" style="gap: 14px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label"><span data-i18n="wizard.step2.profile_name">Profile name</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
|
||||
<input class="form-input" id="new-auth-profile-name" type="text" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="form-row" style="grid-template-columns: 1fr 1fr;">
|
||||
<div class="form-group">
|
||||
<label class="form-label"><span data-i18n="wizard.step2.profile_kind">Auth kind</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
|
||||
<select class="form-select" id="new-auth-profile-kind" onchange="updateAuthProfileCreateUi()">
|
||||
<option value="bearer" data-i18n="wizard.step2.auth_kind.bearer">Bearer token</option>
|
||||
<option value="basic" data-i18n="wizard.step2.auth_kind.basic">Basic auth</option>
|
||||
<option value="api_key_header" data-i18n="wizard.step2.auth_kind.api_key_header">API key header</option>
|
||||
<option value="api_key_query" data-i18n="wizard.step2.auth_kind.api_key_query">API key query</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="auth-profile-header-name-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.header_name">Header name</label>
|
||||
<input class="form-input input-mono" id="new-auth-profile-header-name" type="text" value="Authorization" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="auth-profile-query-param-group" style="display:none;">
|
||||
<label class="form-label" data-i18n="wizard.step2.query_param">Query param</label>
|
||||
<input class="form-input input-mono" id="new-auth-profile-query-param" type="text" value="api_key" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="form-row" id="auth-profile-basic-secret-row" style="display:none; grid-template-columns: 1fr 1fr;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.username_secret">Username secret</label>
|
||||
<select class="form-select" id="new-auth-profile-username-secret"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.password_secret">Password secret</label>
|
||||
<select class="form-select" id="new-auth-profile-password-secret"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="auth-profile-single-secret-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.secret_value">Secret</label>
|
||||
<select class="form-select" id="new-auth-profile-secret-id"></select>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
|
||||
<button class="btn-ghost-sm" onclick="openQuickSecretModal(event)" data-i18n="wizard.step2.quick_secret">Quick create secret</button>
|
||||
<a class="btn-ghost-sm" href="/secrets" target="_blank" rel="noopener" data-i18n="wizard.step2.manage_secrets">Open secrets page</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.static_headers">Static headers (optional)</label>
|
||||
<div class="code-block" style="border-radius: 6px;">
|
||||
<div class="code-toolbar">
|
||||
<div class="code-dots"><span></span><span></span><span></span></div>
|
||||
<span class="code-toolbar-label">json / auth-headers</span>
|
||||
<span class="code-toolbar-label">json / static-headers</span>
|
||||
</div>
|
||||
<textarea class="form-textarea code-textarea" id="new-upstream-auth-headers" rows="7">{
|
||||
"Authorization": "${secrets.API_KEY}"
|
||||
<textarea class="form-textarea code-textarea" id="new-upstream-static-headers" rows="5">{
|
||||
}</textarea>
|
||||
</div>
|
||||
<div class="form-hint" data-i18n="wizard.step2.auth_headers_hint">These headers are sent on every request to this upstream. Use secrets for credentials.</div>
|
||||
<div class="form-hint" data-i18n="wizard.step2.static_headers_hint">Optional non-secret headers sent on every request to this upstream. Use auth profiles for credentials.</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; margin-top:4px;">
|
||||
<button class="btn-primary-sm" onclick="saveNewUpstream(event)" data-i18n="wizard.step2.save_upstream">Save upstream</button>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<span data-i18n-html="wizard.step_of">Step 5 of 5</span>
|
||||
</div>
|
||||
<h1 class="step-panel-title" data-i18n="wizard.step5.title">Mapping and execution</h1>
|
||||
<p class="step-panel-subtitle" data-i18n="wizard.step5.subtitle">Define how MCP tool arguments map to upstream request fields, and how the upstream response maps back to MCP output. Then set execution parameters — auth is configured per-upstream in step 3.</p>
|
||||
<p class="step-panel-subtitle" data-i18n="wizard.step5.subtitle">Define how MCP tool arguments map to upstream request fields, and how the upstream response maps back to MCP output. Then set execution parameters — auth is configured per-upstream in step 2.</p>
|
||||
</div>
|
||||
|
||||
<div class="section-divider" style="margin-bottom: 16px;">
|
||||
|
||||
+97
-8
@@ -558,14 +558,44 @@ var TRANSLATIONS = {
|
||||
'wizard.step2.change': 'Change',
|
||||
'wizard.step2.auth_none': 'No auth',
|
||||
'wizard.step2.auth_bearer': 'Bearer token',
|
||||
'wizard.step2.auth_basic': 'Basic auth',
|
||||
'wizard.step2.auth_apikey': 'API key',
|
||||
'wizard.step2.register_new': 'Register new upstream',
|
||||
'wizard.step2.name': 'Name',
|
||||
'wizard.step2.base_url': 'Base URL',
|
||||
'wizard.step2.unique_hint': 'Unique identifier for this upstream.',
|
||||
'wizard.step2.base_url_hint': 'Root URL — no trailing slash. Supports ${secrets.*}.',
|
||||
'wizard.step2.auth_headers': 'Auth headers',
|
||||
'wizard.step2.auth_headers_hint': 'These headers are sent on every request to this upstream. Use secrets for credentials.',
|
||||
'wizard.step2.base_url_hint': 'Root URL without a trailing slash. Auth is configured separately below.',
|
||||
'wizard.step2.auth_selector': 'Upstream auth',
|
||||
'wizard.step2.auth_selector_hint': 'Use secrets-backed auth profiles instead of embedding credential headers in the upstream definition.',
|
||||
'wizard.step2.auth_mode.none': 'No auth',
|
||||
'wizard.step2.auth_mode.existing': 'Use existing auth profile',
|
||||
'wizard.step2.auth_mode.create': 'Create auth profile now',
|
||||
'wizard.step2.auth_profile': 'Auth profile',
|
||||
'wizard.step2.auth_profile_hint': 'Selected profile will be resolved at runtime and attached through `execution_config.auth_profile_ref`.',
|
||||
'wizard.step2.auth_profile_empty': 'No auth profiles yet. Create one below or on the Secrets page.',
|
||||
'wizard.step2.create_profile_title': 'Create auth profile',
|
||||
'wizard.step2.create_profile_subtitle': 'Create a reusable profile backed by encrypted workspace secrets.',
|
||||
'wizard.step2.profile_name': 'Profile name',
|
||||
'wizard.step2.profile_kind': 'Auth kind',
|
||||
'wizard.step2.auth_kind.bearer': 'Bearer token',
|
||||
'wizard.step2.auth_kind.basic': 'Basic auth',
|
||||
'wizard.step2.auth_kind.api_key_header': 'API key header',
|
||||
'wizard.step2.auth_kind.api_key_query': 'API key query',
|
||||
'wizard.step2.header_name': 'Header name',
|
||||
'wizard.step2.query_param': 'Query param',
|
||||
'wizard.step2.username_secret': 'Username secret',
|
||||
'wizard.step2.password_secret': 'Password secret',
|
||||
'wizard.step2.secret_value': 'Secret',
|
||||
'wizard.step2.quick_secret': 'Quick create secret',
|
||||
'wizard.step2.manage_secrets': 'Open secrets page',
|
||||
'wizard.step2.static_headers': 'Static headers (optional)',
|
||||
'wizard.step2.static_headers_hint': 'Optional non-secret headers sent on every request to this upstream. Use auth profiles for credentials.',
|
||||
'wizard.step2.quick_secret_title': 'Quick create secret',
|
||||
'wizard.step2.quick_secret_name': 'Secret name',
|
||||
'wizard.step2.quick_secret_kind': 'Secret kind',
|
||||
'wizard.step2.quick_secret_value': 'Secret value',
|
||||
'wizard.step2.quick_secret_hint': 'The plaintext is encrypted immediately and will not be returned again.',
|
||||
'wizard.step2.quick_secret_submit': 'Create secret',
|
||||
'wizard.step2.save_upstream': 'Save upstream',
|
||||
'wizard.step2.endpoint_title': 'Endpoint',
|
||||
'wizard.step2.endpoint_subtitle': 'Path and HTTP method for this specific operation',
|
||||
@@ -657,7 +687,7 @@ var TRANSLATIONS = {
|
||||
'wizard.step4.protocol_agnostic_title': 'Schemas are protocol-agnostic',
|
||||
'wizard.step4.protocol_agnostic_body': 'These schemas describe the MCP contract, not the upstream wire format. Field mapping in Step 5 translates between the two. You can upload a sample JSON response to auto-generate the output schema.',
|
||||
'wizard.step5.title': 'Mapping and execution',
|
||||
'wizard.step5.subtitle': 'Define how MCP tool arguments map to upstream request fields, and how the upstream response maps back to MCP output. Then set execution parameters — auth is configured per-upstream in step 3.',
|
||||
'wizard.step5.subtitle': 'Define how MCP tool arguments map to upstream request fields, and how the upstream response maps back to MCP output. Then set execution parameters — auth is configured per-upstream in step 2.',
|
||||
'wizard.step5.input_request': 'Input → Request',
|
||||
'wizard.step5.output_response': 'Response → Output',
|
||||
'wizard.step5.execution': 'Execution',
|
||||
@@ -706,6 +736,21 @@ var TRANSLATIONS = {
|
||||
'wizard.error.tool_name': 'Tool name is required',
|
||||
'wizard.error.parser_unavailable': 'YAML parser is not available',
|
||||
'wizard.error.select_upstream': 'Select or register an upstream first',
|
||||
'wizard.error.auth_profile_required': 'Select an auth profile or switch upstream auth to No auth.',
|
||||
'wizard.error.auth_profile_name': 'Auth profile name is required.',
|
||||
'wizard.error.secret_required': 'Select the secret required by this auth profile.',
|
||||
'wizard.error.basic_secrets_required': 'Select both username and password secrets.',
|
||||
'wizard.error.header_name_required': 'Header name is required for this auth profile.',
|
||||
'wizard.error.query_param_required': 'Query parameter name is required for this auth profile.',
|
||||
'wizard.error.secret_name_required': 'Secret name is required.',
|
||||
'wizard.error.secret_value_required': 'Secret value is required.',
|
||||
'wizard.error.save_upstream_first': 'Save the upstream after creating a new auth profile before continuing.',
|
||||
'wizard.toast.auth_profile_created_title': 'Auth profile created',
|
||||
'wizard.toast.auth_profile_created_body': 'New upstream auth profile is ready and selected.',
|
||||
'wizard.toast.auth_profile_error_title': 'Auth profile creation failed',
|
||||
'wizard.toast.quick_secret_created_title': 'Secret created',
|
||||
'wizard.toast.quick_secret_created_body': 'The new secret is now available in the auth selector.',
|
||||
'wizard.toast.quick_secret_error_title': 'Secret creation failed',
|
||||
'wizard.error.save': 'Failed to save operation',
|
||||
'wizard.error.save_title': 'Save failed',
|
||||
'wizard.save.created': 'Draft created',
|
||||
@@ -1443,9 +1488,38 @@ var TRANSLATIONS = {
|
||||
'wizard.step2.name': 'Имя',
|
||||
'wizard.step2.base_url': 'Base URL',
|
||||
'wizard.step2.unique_hint': 'Уникальный идентификатор этого upstream-а.',
|
||||
'wizard.step2.base_url_hint': 'Корневой URL без завершающего слеша. Поддерживает ${secrets.*}.',
|
||||
'wizard.step2.auth_headers': 'Auth headers',
|
||||
'wizard.step2.auth_headers_hint': 'Эти заголовки отправляются с каждым запросом в этот upstream. Используйте secrets для учетных данных.',
|
||||
'wizard.step2.base_url_hint': 'Корневой URL без завершающего слеша. Auth настраивается отдельно ниже.',
|
||||
'wizard.step2.auth_selector': 'Upstream auth',
|
||||
'wizard.step2.auth_selector_hint': 'Используйте secrets-backed auth profiles вместо встраивания credential headers в определение upstream-а.',
|
||||
'wizard.step2.auth_mode.none': 'Без авторизации',
|
||||
'wizard.step2.auth_mode.existing': 'Использовать существующий auth profile',
|
||||
'wizard.step2.auth_mode.create': 'Создать auth profile сейчас',
|
||||
'wizard.step2.auth_profile': 'Auth profile',
|
||||
'wizard.step2.auth_profile_hint': 'Выбранный профиль будет резолвиться в runtime и подключаться через `execution_config.auth_profile_ref`.',
|
||||
'wizard.step2.auth_profile_empty': 'Auth profiles пока нет. Создайте его ниже или на странице Secrets.',
|
||||
'wizard.step2.create_profile_title': 'Создать auth profile',
|
||||
'wizard.step2.create_profile_subtitle': 'Создайте переиспользуемый профиль на базе зашифрованных workspace secrets.',
|
||||
'wizard.step2.profile_name': 'Имя профиля',
|
||||
'wizard.step2.profile_kind': 'Тип auth',
|
||||
'wizard.step2.auth_kind.bearer': 'Bearer token',
|
||||
'wizard.step2.auth_kind.basic': 'Basic auth',
|
||||
'wizard.step2.auth_kind.api_key_header': 'API key header',
|
||||
'wizard.step2.auth_kind.api_key_query': 'API key query',
|
||||
'wizard.step2.header_name': 'Имя заголовка',
|
||||
'wizard.step2.query_param': 'Query param',
|
||||
'wizard.step2.username_secret': 'Секрет логина',
|
||||
'wizard.step2.password_secret': 'Секрет пароля',
|
||||
'wizard.step2.secret_value': 'Секрет',
|
||||
'wizard.step2.quick_secret': 'Быстро создать секрет',
|
||||
'wizard.step2.manage_secrets': 'Открыть страницу Secrets',
|
||||
'wizard.step2.static_headers': 'Статические заголовки (опционально)',
|
||||
'wizard.step2.static_headers_hint': 'Необязательные не-секретные заголовки для каждого запроса к этому upstream-у. Для credential flows используйте auth profiles.',
|
||||
'wizard.step2.quick_secret_title': 'Быстро создать секрет',
|
||||
'wizard.step2.quick_secret_name': 'Имя секрета',
|
||||
'wizard.step2.quick_secret_kind': 'Тип секрета',
|
||||
'wizard.step2.quick_secret_value': 'Значение секрета',
|
||||
'wizard.step2.quick_secret_hint': 'Plaintext сразу шифруется и больше не возвращается.',
|
||||
'wizard.step2.quick_secret_submit': 'Создать секрет',
|
||||
'wizard.step2.save_upstream': 'Сохранить upstream',
|
||||
'wizard.step2.endpoint_title': 'Endpoint',
|
||||
'wizard.step2.endpoint_subtitle': 'Путь и HTTP-метод для конкретной операции',
|
||||
@@ -1537,7 +1611,7 @@ var TRANSLATIONS = {
|
||||
'wizard.step4.protocol_agnostic_title': 'Схемы не зависят от протокола',
|
||||
'wizard.step4.protocol_agnostic_body': 'Эти схемы описывают MCP-контракт, а не wire-format upstream-а. Маппинг на шаге 5 переводит одно в другое. Можно загрузить sample JSON-ответа, чтобы автоматически сгенерировать output schema.',
|
||||
'wizard.step5.title': 'Mapping и исполнение',
|
||||
'wizard.step5.subtitle': 'Определите, как аргументы MCP-инструмента мапятся в поля upstream-запроса и как ответ upstream-а мапится обратно в MCP output. Затем задайте execution-параметры — auth настраивается на уровне upstream на шаге 3.',
|
||||
'wizard.step5.subtitle': 'Определите, как аргументы MCP-инструмента мапятся в поля upstream-запроса и как ответ upstream-а мапится обратно в MCP output. Затем задайте execution-параметры — auth настраивается на уровне upstream на шаге 2.',
|
||||
'wizard.step5.input_request': 'Input → Request',
|
||||
'wizard.step5.output_response': 'Response → Output',
|
||||
'wizard.step5.execution': 'Исполнение',
|
||||
@@ -1586,6 +1660,21 @@ var TRANSLATIONS = {
|
||||
'wizard.error.tool_name': 'Имя инструмента обязательно',
|
||||
'wizard.error.parser_unavailable': 'YAML parser недоступен',
|
||||
'wizard.error.select_upstream': 'Сначала выберите или зарегистрируйте upstream',
|
||||
'wizard.error.auth_profile_required': 'Выберите auth profile или переключите upstream auth в режим «Без авторизации».',
|
||||
'wizard.error.auth_profile_name': 'Нужно указать имя auth profile.',
|
||||
'wizard.error.secret_required': 'Выберите секрет, необходимый для этого auth profile.',
|
||||
'wizard.error.basic_secrets_required': 'Выберите секреты и для логина, и для пароля.',
|
||||
'wizard.error.header_name_required': 'Для этого auth profile нужно указать имя заголовка.',
|
||||
'wizard.error.query_param_required': 'Для этого auth profile нужно указать имя query-параметра.',
|
||||
'wizard.error.secret_name_required': 'Нужно указать имя секрета.',
|
||||
'wizard.error.secret_value_required': 'Нужно указать значение секрета.',
|
||||
'wizard.error.save_upstream_first': 'Сначала сохраните upstream после создания нового auth profile.',
|
||||
'wizard.toast.auth_profile_created_title': 'Auth profile создан',
|
||||
'wizard.toast.auth_profile_created_body': 'Новый upstream auth profile готов и уже выбран.',
|
||||
'wizard.toast.auth_profile_error_title': 'Не удалось создать auth profile',
|
||||
'wizard.toast.quick_secret_created_title': 'Секрет создан',
|
||||
'wizard.toast.quick_secret_created_body': 'Новый секрет уже доступен в auth selector.',
|
||||
'wizard.toast.quick_secret_error_title': 'Не удалось создать секрет',
|
||||
'wizard.error.save': 'Не удалось сохранить операцию',
|
||||
'wizard.error.save_title': 'Не удалось сохранить',
|
||||
'wizard.save.created': 'Черновик создан',
|
||||
|
||||
+407
-63
@@ -276,9 +276,44 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
await loadProtocolCapabilities();
|
||||
|
||||
await loadWizardPanels([1, 2, 3, 4, 5]);
|
||||
await loadWizardAuthResources();
|
||||
bindProtocolCards();
|
||||
bindWizardLiveActions();
|
||||
bindStreamingConfigControls();
|
||||
updateUpstreamAuthUi();
|
||||
|
||||
var quickSecretModal = document.getElementById('quick-secret-modal');
|
||||
var quickSecretClose = document.getElementById('quick-secret-close-btn');
|
||||
var quickSecretCancel = document.getElementById('quick-secret-cancel-btn');
|
||||
var quickSecretSubmit = document.getElementById('quick-secret-submit-btn');
|
||||
if (quickSecretClose) quickSecretClose.addEventListener('click', closeQuickSecretModal);
|
||||
if (quickSecretCancel) quickSecretCancel.addEventListener('click', closeQuickSecretModal);
|
||||
if (quickSecretModal) {
|
||||
quickSecretModal.addEventListener('click', function(event) {
|
||||
if (event.target === quickSecretModal) closeQuickSecretModal();
|
||||
});
|
||||
}
|
||||
if (quickSecretSubmit) {
|
||||
quickSecretSubmit.addEventListener('click', async function() {
|
||||
var button = quickSecretSubmit;
|
||||
var original = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = tKey('wizard.step2.quick_secret_submit');
|
||||
try {
|
||||
await submitQuickSecret();
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(
|
||||
error.message || tKey('wizard.toast.quick_secret_error_title'),
|
||||
tKey('wizard.toast.quick_secret_error_title')
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = original;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
if (params.get('mode') === 'edit' && params.get('operationId')) {
|
||||
@@ -338,46 +373,170 @@ function selectGqlType(btn) {
|
||||
if (defaults[type]) editor.value = defaults[type];
|
||||
}
|
||||
|
||||
/* ── Upstream selector (Step 3) ── */
|
||||
/* ── Upstream selector (Step 2) ── */
|
||||
|
||||
var upstreams = [
|
||||
{ id: 'acme-api', name: 'acme-api', url: 'https://api.acme.com', authHeaders: '{\n \"Authorization\": \"Bearer ${secrets.ACME_API_TOKEN}\"\n}' },
|
||||
{ id: 'stripe', name: 'stripe-payments', url: 'https://api.stripe.com', authHeaders: '{\n \"Authorization\": \"Bearer ${secrets.STRIPE_API_KEY}\"\n}' },
|
||||
{ id: 'internal-crm', name: 'internal-crm', url: 'https://crm.internal.acme.io', authHeaders: '{\n}' },
|
||||
{ id: 'sendgrid', name: 'sendgrid-mail', url: 'https://api.sendgrid.com', authHeaders: '{\n \"Authorization\": \"Bearer ${secrets.SENDGRID_API_KEY}\"\n}' },
|
||||
{ id: 'twilio', name: 'twilio-sms', url: 'https://api.twilio.com', authHeaders: '{\n \"Authorization\": \"Bearer ${secrets.TWILIO_API_KEY}\"\n}' },
|
||||
{ id: 'open-meteo', name: 'Open Meteo', url: 'https://api.open-meteo.com', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null },
|
||||
{ id: 'countries-graphql', name: 'Countries GraphQL', url: 'https://countries.trevorblades.com', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null },
|
||||
{ id: 'grpcb', name: 'grpcb.in', url: 'https://grpcb.in:443', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null }
|
||||
];
|
||||
|
||||
var wizardSecrets = [];
|
||||
var wizardAuthProfiles = [];
|
||||
var selectedUpstreamId = null;
|
||||
var dropdownOpen = false;
|
||||
var editingUpstreamId = null;
|
||||
|
||||
function inferUpstreamAuth(headersText) {
|
||||
var parsed = parseStructuredText(headersText || '{}');
|
||||
var keys = parsed && typeof parsed === 'object' ? Object.keys(parsed) : [];
|
||||
if (!keys.length) {
|
||||
function authProfileById(authProfileId) {
|
||||
return wizardAuthProfiles.find(function(item) { return item.id === authProfileId; }) || null;
|
||||
}
|
||||
|
||||
function syncUpstreamAuthMetadata(upstream) {
|
||||
if (!upstream) return upstream;
|
||||
if (!upstream.authProfileId) {
|
||||
upstream.authProfileName = '';
|
||||
upstream.authKind = null;
|
||||
return upstream;
|
||||
}
|
||||
|
||||
var profile = authProfileById(upstream.authProfileId);
|
||||
if (profile) {
|
||||
upstream.authProfileName = profile.name;
|
||||
upstream.authKind = profile.kind;
|
||||
}
|
||||
return upstream;
|
||||
}
|
||||
|
||||
function inferUpstreamAuth(upstream) {
|
||||
syncUpstreamAuthMetadata(upstream);
|
||||
if (!upstream || !upstream.authProfileId || !upstream.authKind) {
|
||||
return { kind: 'none', label: tKey('wizard.step2.auth_none') };
|
||||
}
|
||||
|
||||
var hasAuthorization = keys.some(function(key) {
|
||||
return String(key).toLowerCase() === 'authorization';
|
||||
});
|
||||
|
||||
if (hasAuthorization) {
|
||||
if (upstream.authKind === 'bearer') {
|
||||
return { kind: 'bearer', label: tKey('wizard.step2.auth_bearer') };
|
||||
}
|
||||
|
||||
if (upstream.authKind === 'basic') {
|
||||
return { kind: 'basic', label: tKey('wizard.step2.auth_basic') };
|
||||
}
|
||||
return { kind: 'apikey', label: tKey('wizard.step2.auth_apikey') };
|
||||
}
|
||||
|
||||
function upstreamMeta(upstream) {
|
||||
var auth = inferUpstreamAuth(upstream && upstream.authHeaders);
|
||||
var auth = inferUpstreamAuth(upstream);
|
||||
return {
|
||||
kind: auth.kind,
|
||||
label: auth.label,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadWizardAuthResources() {
|
||||
if (!wizardWorkspaceId || !window.CrankApi) {
|
||||
wizardSecrets = [];
|
||||
wizardAuthProfiles = [];
|
||||
renderAuthProfileOptions();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var results = await Promise.all([
|
||||
window.CrankApi.listSecrets(wizardWorkspaceId),
|
||||
window.CrankApi.listAuthProfiles(wizardWorkspaceId),
|
||||
]);
|
||||
wizardSecrets = (results[0] && results[0].items) || [];
|
||||
wizardAuthProfiles = (results[1] && results[1].items) || [];
|
||||
} catch (_error) {
|
||||
wizardSecrets = [];
|
||||
wizardAuthProfiles = [];
|
||||
}
|
||||
|
||||
upstreams.forEach(syncUpstreamAuthMetadata);
|
||||
renderAuthProfileOptions();
|
||||
}
|
||||
|
||||
function renderAuthProfileOptions() {
|
||||
var profileSelect = document.getElementById('new-upstream-auth-profile');
|
||||
var secretSelect = document.getElementById('new-auth-profile-secret-id');
|
||||
var usernameSelect = document.getElementById('new-auth-profile-username-secret');
|
||||
var passwordSelect = document.getElementById('new-auth-profile-password-secret');
|
||||
var profileHint = document.getElementById('new-upstream-auth-profile-hint');
|
||||
|
||||
if (profileSelect) {
|
||||
var previousProfile = profileSelect.value;
|
||||
profileSelect.innerHTML = '';
|
||||
if (!wizardAuthProfiles.length) {
|
||||
var emptyProfile = document.createElement('option');
|
||||
emptyProfile.value = '';
|
||||
emptyProfile.textContent = tKey('wizard.step2.auth_profile_empty');
|
||||
profileSelect.appendChild(emptyProfile);
|
||||
if (profileHint) profileHint.textContent = tKey('wizard.step2.auth_profile_empty');
|
||||
} else {
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = tKey('wizard.step2.auth_profile');
|
||||
profileSelect.appendChild(placeholder);
|
||||
wizardAuthProfiles.forEach(function(profile) {
|
||||
var option = document.createElement('option');
|
||||
option.value = profile.id;
|
||||
option.textContent = profile.name + ' · ' + profile.kind;
|
||||
profileSelect.appendChild(option);
|
||||
});
|
||||
if (previousProfile) {
|
||||
profileSelect.value = previousProfile;
|
||||
}
|
||||
if (profileHint) profileHint.textContent = tKey('wizard.step2.auth_profile_hint');
|
||||
}
|
||||
}
|
||||
|
||||
[secretSelect, usernameSelect, passwordSelect].forEach(function(select) {
|
||||
if (!select) return;
|
||||
var previous = select.value;
|
||||
select.innerHTML = '';
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = tKey('wizard.step2.secret_value');
|
||||
select.appendChild(placeholder);
|
||||
wizardSecrets.forEach(function(secret) {
|
||||
var option = document.createElement('option');
|
||||
option.value = secret.id;
|
||||
option.textContent = secret.name + ' · ' + secret.kind;
|
||||
select.appendChild(option);
|
||||
});
|
||||
if (previous) {
|
||||
select.value = previous;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateAuthProfileCreateUi() {
|
||||
var kind = textValue('new-auth-profile-kind') || 'bearer';
|
||||
var headerGroup = document.getElementById('auth-profile-header-name-group');
|
||||
var queryGroup = document.getElementById('auth-profile-query-param-group');
|
||||
var basicRow = document.getElementById('auth-profile-basic-secret-row');
|
||||
var singleSecretGroup = document.getElementById('auth-profile-single-secret-group');
|
||||
|
||||
if (headerGroup) {
|
||||
headerGroup.style.display = (kind === 'bearer' || kind === 'api_key_header') ? '' : 'none';
|
||||
}
|
||||
if (queryGroup) {
|
||||
queryGroup.style.display = kind === 'api_key_query' ? '' : 'none';
|
||||
}
|
||||
if (basicRow) {
|
||||
basicRow.style.display = kind === 'basic' ? '' : 'none';
|
||||
}
|
||||
if (singleSecretGroup) {
|
||||
singleSecretGroup.style.display = kind === 'basic' ? 'none' : '';
|
||||
}
|
||||
}
|
||||
|
||||
function updateUpstreamAuthUi() {
|
||||
var mode = textValue('new-upstream-auth-mode') || 'none';
|
||||
var existingGroup = document.getElementById('upstream-auth-existing-group');
|
||||
var createGroup = document.getElementById('upstream-auth-create-group');
|
||||
if (existingGroup) existingGroup.style.display = mode === 'existing' ? '' : 'none';
|
||||
if (createGroup) createGroup.style.display = mode === 'create' ? '' : 'none';
|
||||
updateAuthProfileCreateUi();
|
||||
}
|
||||
|
||||
function renderDropdownList(filter) {
|
||||
var list = document.getElementById('upstream-dropdown-list');
|
||||
if (!list) return;
|
||||
@@ -522,7 +681,17 @@ function startNewUpstream() {
|
||||
form.style.display = '';
|
||||
setValue('new-upstream-name', '');
|
||||
setValue('new-upstream-url', '');
|
||||
setValue('new-upstream-auth-headers', '{\n}');
|
||||
setValue('new-upstream-static-headers', '{\n}');
|
||||
setValue('new-upstream-auth-mode', 'none');
|
||||
setValue('new-upstream-auth-profile', '');
|
||||
setValue('new-auth-profile-name', '');
|
||||
setValue('new-auth-profile-kind', 'bearer');
|
||||
setValue('new-auth-profile-header-name', 'Authorization');
|
||||
setValue('new-auth-profile-query-param', 'api_key');
|
||||
setValue('new-auth-profile-secret-id', '');
|
||||
setValue('new-auth-profile-username-secret', '');
|
||||
setValue('new-auth-profile-password-secret', '');
|
||||
updateUpstreamAuthUi();
|
||||
var first = form.querySelector('input');
|
||||
if (first) setTimeout(function() { first.focus(); }, 50);
|
||||
}
|
||||
@@ -545,46 +714,151 @@ function beginEditSelectedUpstream(e) {
|
||||
form.style.display = '';
|
||||
setValue('new-upstream-name', upstream.name);
|
||||
setValue('new-upstream-url', upstream.url);
|
||||
setValue('new-upstream-auth-headers', upstream.authHeaders || '{\n}');
|
||||
setValue('new-upstream-static-headers', upstream.staticHeaders || '{\n}');
|
||||
setValue('new-upstream-auth-profile', upstream.authProfileId || '');
|
||||
setValue('new-upstream-auth-mode', upstream.authProfileId ? 'existing' : 'none');
|
||||
updateUpstreamAuthUi();
|
||||
}
|
||||
}
|
||||
|
||||
function saveNewUpstream(e) {
|
||||
e.stopPropagation();
|
||||
var nameEl = document.getElementById('new-upstream-name');
|
||||
var urlEl = document.getElementById('new-upstream-url');
|
||||
var name = nameEl ? nameEl.value.trim() : '';
|
||||
var url = urlEl ? urlEl.value.trim() : '';
|
||||
if (!name) { if (nameEl) nameEl.focus(); return; }
|
||||
if (!url) { if (urlEl) urlEl.focus(); return; }
|
||||
|
||||
var authHeadersEl = document.getElementById('new-upstream-auth-headers');
|
||||
var authHeaders = authHeadersEl ? authHeadersEl.value : '{\n}';
|
||||
var nextRecord = {
|
||||
id: editingUpstreamId || ('custom-' + Date.now()),
|
||||
name: name,
|
||||
url: url,
|
||||
authHeaders: authHeaders
|
||||
};
|
||||
|
||||
if (editingUpstreamId) {
|
||||
upstreams = upstreams.map(function(item) {
|
||||
return item.id === editingUpstreamId ? nextRecord : item;
|
||||
});
|
||||
} else {
|
||||
upstreams.push(nextRecord);
|
||||
function buildAuthProfileConfig(kind) {
|
||||
if (kind === 'bearer') {
|
||||
var bearerHeader = textValue('new-auth-profile-header-name') || 'Authorization';
|
||||
var bearerSecretId = textValue('new-auth-profile-secret-id');
|
||||
if (!bearerHeader) throw new Error(tKey('wizard.error.header_name_required'));
|
||||
if (!bearerSecretId) throw new Error(tKey('wizard.error.secret_required'));
|
||||
return {
|
||||
bearer: {
|
||||
header_name: bearerHeader,
|
||||
secret_id: bearerSecretId,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Reset form
|
||||
if (nameEl) nameEl.value = '';
|
||||
if (urlEl) urlEl.value = '';
|
||||
if (authHeadersEl) authHeadersEl.value = '{\n}';
|
||||
var form = document.getElementById('upstream-new-form');
|
||||
if (form) form.style.display = 'none';
|
||||
editingUpstreamId = null;
|
||||
if (kind === 'basic') {
|
||||
var usernameSecretId = textValue('new-auth-profile-username-secret');
|
||||
var passwordSecretId = textValue('new-auth-profile-password-secret');
|
||||
if (!usernameSecretId || !passwordSecretId) throw new Error(tKey('wizard.error.basic_secrets_required'));
|
||||
return {
|
||||
basic: {
|
||||
username_secret_id: usernameSecretId,
|
||||
password_secret_id: passwordSecretId,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Select the new upstream
|
||||
pickUpstream(nextRecord.id);
|
||||
if (kind === 'api_key_header') {
|
||||
var headerName = textValue('new-auth-profile-header-name');
|
||||
var headerSecretId = textValue('new-auth-profile-secret-id');
|
||||
if (!headerName) throw new Error(tKey('wizard.error.header_name_required'));
|
||||
if (!headerSecretId) throw new Error(tKey('wizard.error.secret_required'));
|
||||
return {
|
||||
api_key_header: {
|
||||
header_name: headerName,
|
||||
secret_id: headerSecretId,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var queryParam = textValue('new-auth-profile-query-param');
|
||||
var querySecretId = textValue('new-auth-profile-secret-id');
|
||||
if (!queryParam) throw new Error(tKey('wizard.error.query_param_required'));
|
||||
if (!querySecretId) throw new Error(tKey('wizard.error.secret_required'));
|
||||
return {
|
||||
api_key_query: {
|
||||
param_name: queryParam,
|
||||
secret_id: querySecretId,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function createAuthProfileFromForm() {
|
||||
var name = textValue('new-auth-profile-name');
|
||||
var kind = textValue('new-auth-profile-kind') || 'bearer';
|
||||
if (!name) throw new Error(tKey('wizard.error.auth_profile_name'));
|
||||
|
||||
var profile = await window.CrankApi.createAuthProfile(wizardWorkspaceId, {
|
||||
name: name,
|
||||
kind: kind,
|
||||
config: buildAuthProfileConfig(kind),
|
||||
});
|
||||
wizardAuthProfiles.push(profile);
|
||||
renderAuthProfileOptions();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
tKey('wizard.toast.auth_profile_created_body'),
|
||||
tKey('wizard.toast.auth_profile_created_title')
|
||||
);
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
async function saveNewUpstream(e) {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
var nameEl = document.getElementById('new-upstream-name');
|
||||
var urlEl = document.getElementById('new-upstream-url');
|
||||
var name = nameEl ? nameEl.value.trim() : '';
|
||||
var url = urlEl ? urlEl.value.trim() : '';
|
||||
if (!name) { if (nameEl) nameEl.focus(); return; }
|
||||
if (!url) { if (urlEl) urlEl.focus(); return; }
|
||||
|
||||
var staticHeadersEl = document.getElementById('new-upstream-static-headers');
|
||||
var staticHeaders = staticHeadersEl ? staticHeadersEl.value : '{\n}';
|
||||
parseHeaderMap(staticHeaders);
|
||||
|
||||
var authMode = textValue('new-upstream-auth-mode') || 'none';
|
||||
var authProfileId = null;
|
||||
var authProfileName = '';
|
||||
var authKind = null;
|
||||
|
||||
if (authMode === 'existing') {
|
||||
authProfileId = textValue('new-upstream-auth-profile');
|
||||
if (!authProfileId) throw new Error(tKey('wizard.error.auth_profile_required'));
|
||||
var existingProfile = authProfileById(authProfileId);
|
||||
authProfileName = existingProfile ? existingProfile.name : '';
|
||||
authKind = existingProfile ? existingProfile.kind : null;
|
||||
} else if (authMode === 'create') {
|
||||
var createdProfile = await createAuthProfileFromForm();
|
||||
authProfileId = createdProfile.id;
|
||||
authProfileName = createdProfile.name;
|
||||
authKind = createdProfile.kind;
|
||||
setValue('new-upstream-auth-profile', createdProfile.id);
|
||||
setValue('new-upstream-auth-mode', 'existing');
|
||||
}
|
||||
var nextRecord = {
|
||||
id: editingUpstreamId || ('custom-' + Date.now()),
|
||||
name: name,
|
||||
url: url,
|
||||
staticHeaders: staticHeaders,
|
||||
authProfileId: authProfileId,
|
||||
authProfileName: authProfileName,
|
||||
authKind: authKind,
|
||||
};
|
||||
|
||||
if (editingUpstreamId) {
|
||||
upstreams = upstreams.map(function(item) {
|
||||
return item.id === editingUpstreamId ? nextRecord : item;
|
||||
});
|
||||
} else {
|
||||
upstreams.push(nextRecord);
|
||||
}
|
||||
|
||||
if (nameEl) nameEl.value = '';
|
||||
if (urlEl) urlEl.value = '';
|
||||
if (staticHeadersEl) staticHeadersEl.value = '{\n}';
|
||||
var form = document.getElementById('upstream-new-form');
|
||||
if (form) form.style.display = 'none';
|
||||
editingUpstreamId = null;
|
||||
pickUpstream(nextRecord.id);
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(
|
||||
error.message || tKey('wizard.toast.auth_profile_error_title'),
|
||||
tKey('wizard.toast.auth_profile_error_title')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cancelNewUpstream(e) {
|
||||
@@ -596,6 +870,58 @@ function cancelNewUpstream(e) {
|
||||
editingUpstreamId = null;
|
||||
}
|
||||
|
||||
function openQuickSecretModal(e) {
|
||||
if (e) e.preventDefault();
|
||||
var modal = document.getElementById('quick-secret-modal');
|
||||
if (!modal) return;
|
||||
setValue('quick-secret-name', '');
|
||||
setValue('quick-secret-value', '');
|
||||
setValue('quick-secret-kind', 'token');
|
||||
modal.classList.add('open');
|
||||
setTimeout(function() {
|
||||
var input = document.getElementById('quick-secret-name');
|
||||
if (input) input.focus();
|
||||
}, 30);
|
||||
}
|
||||
|
||||
function closeQuickSecretModal() {
|
||||
var modal = document.getElementById('quick-secret-modal');
|
||||
if (modal) modal.classList.remove('open');
|
||||
}
|
||||
|
||||
async function submitQuickSecret() {
|
||||
var name = textValue('quick-secret-name');
|
||||
var value = textValue('quick-secret-value');
|
||||
var kind = textValue('quick-secret-kind') || 'token';
|
||||
if (!name) throw new Error(tKey('wizard.error.secret_name_required'));
|
||||
if (!value) throw new Error(tKey('wizard.error.secret_value_required'));
|
||||
|
||||
var secret = await window.CrankApi.createSecret(wizardWorkspaceId, {
|
||||
name: name,
|
||||
kind: kind,
|
||||
value: value,
|
||||
});
|
||||
wizardSecrets.push(secret);
|
||||
renderAuthProfileOptions();
|
||||
var primarySelect = document.getElementById('new-auth-profile-secret-id');
|
||||
var usernameSelect = document.getElementById('new-auth-profile-username-secret');
|
||||
var passwordSelect = document.getElementById('new-auth-profile-password-secret');
|
||||
if (primarySelect && primarySelect.offsetParent !== null) {
|
||||
primarySelect.value = secret.id;
|
||||
} else if (usernameSelect && !usernameSelect.value) {
|
||||
usernameSelect.value = secret.id;
|
||||
} else if (passwordSelect) {
|
||||
passwordSelect.value = secret.id;
|
||||
}
|
||||
closeQuickSecretModal();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
tKey('wizard.toast.quick_secret_created_body'),
|
||||
tKey('wizard.toast.quick_secret_created_title')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
@@ -1154,10 +1480,17 @@ function parseExecutionConfig(text) {
|
||||
var value = parseStructuredText(text);
|
||||
var retry = value.retry || value.retry_policy || null;
|
||||
var headers = value.headers && typeof value.headers === 'object' ? value.headers : {};
|
||||
var selectedUpstream = currentUpstream();
|
||||
if (selectedUpstream && selectedUpstream.authMode === 'create') {
|
||||
throw new Error(tKey('wizard.error.save_upstream_first'));
|
||||
}
|
||||
var authProfileRef = selectedUpstream && selectedUpstream.authProfileId
|
||||
? selectedUpstream.authProfileId
|
||||
: (value.auth && value.auth.profile ? value.auth.profile : null);
|
||||
var config = {
|
||||
timeout_ms: Number(value.timeout_ms || 10000),
|
||||
retry_policy: retry && retry.max_attempts ? { max_attempts: Number(retry.max_attempts) } : null,
|
||||
auth_profile_ref: value.auth && value.auth.profile ? value.auth.profile : null,
|
||||
auth_profile_ref: authProfileRef,
|
||||
headers: headers,
|
||||
protocol_options: null,
|
||||
};
|
||||
@@ -1185,12 +1518,18 @@ function currentUpstream() {
|
||||
var customUrl = textValue('new-upstream-url');
|
||||
if (!customUrl) return null;
|
||||
|
||||
var authHeaders = textValue('new-upstream-auth-headers');
|
||||
var authMode = textValue('new-upstream-auth-mode') || 'none';
|
||||
var authProfileId = authMode === 'existing' ? textValue('new-upstream-auth-profile') : null;
|
||||
var authProfile = authProfileById(authProfileId);
|
||||
return {
|
||||
id: 'custom',
|
||||
name: customName || 'custom-upstream',
|
||||
url: customUrl,
|
||||
authHeaders: authHeaders,
|
||||
staticHeaders: textValue('new-upstream-static-headers') || '{\n}',
|
||||
authMode: authMode,
|
||||
authProfileId: authProfileId || null,
|
||||
authProfileName: authProfile ? authProfile.name : '',
|
||||
authKind: authProfile ? authProfile.kind : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1230,7 +1569,7 @@ function buildTarget() {
|
||||
base_url: upstream.url,
|
||||
method: activeMethod ? activeMethod.dataset.method : 'POST',
|
||||
path_template: pathTemplate,
|
||||
static_headers: parseHeaderMap(upstream.authHeaders),
|
||||
static_headers: parseHeaderMap(upstream.staticHeaders),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1634,8 +1973,10 @@ function selectProtocol(protocol) {
|
||||
updateWizardProtocolVisibility();
|
||||
}
|
||||
|
||||
function prefillUpstream(baseUrl, headers) {
|
||||
var known = upstreams.find(function(item) { return item.url === baseUrl; });
|
||||
function prefillUpstream(baseUrl, headers, authProfileRef) {
|
||||
var known = upstreams.find(function(item) {
|
||||
return item.url === baseUrl && (item.authProfileId || null) === (authProfileRef || null);
|
||||
});
|
||||
if (known) {
|
||||
pickUpstream(known.id);
|
||||
return;
|
||||
@@ -1648,7 +1989,10 @@ function prefillUpstream(baseUrl, headers) {
|
||||
if (form) form.style.display = '';
|
||||
setValue('new-upstream-name', 'imported-upstream');
|
||||
setValue('new-upstream-url', baseUrl);
|
||||
setValue('new-upstream-auth-headers', headers && Object.keys(headers).length ? JSON.stringify(headers, null, 2) : '{\n}');
|
||||
setValue('new-upstream-static-headers', headers && Object.keys(headers).length ? JSON.stringify(headers, null, 2) : '{\n}');
|
||||
setValue('new-upstream-auth-mode', authProfileRef ? 'existing' : 'none');
|
||||
setValue('new-upstream-auth-profile', authProfileRef || '');
|
||||
updateUpstreamAuthUi();
|
||||
|
||||
var valueEl = document.getElementById('upstream-combobox-value');
|
||||
if (valueEl) {
|
||||
@@ -1725,7 +2069,7 @@ function prefillWizardFromEdit(detail, versionDocument) {
|
||||
|
||||
var target = versionDocument.target || {};
|
||||
if (target.kind === 'rest') {
|
||||
prefillUpstream(target.base_url, target.static_headers);
|
||||
prefillUpstream(target.base_url, target.static_headers, versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null);
|
||||
setValue('endpoint-path', target.path_template);
|
||||
document.querySelectorAll('.method-card').forEach(function(card) {
|
||||
card.classList.toggle('active', card.dataset.method === target.method);
|
||||
@@ -1733,14 +2077,14 @@ function prefillWizardFromEdit(detail, versionDocument) {
|
||||
} else if (target.kind === 'graphql') {
|
||||
var endpoint = target.endpoint || '';
|
||||
var parsedEndpoint = new URL(endpoint, window.location.origin);
|
||||
prefillUpstream(parsedEndpoint.origin, {});
|
||||
prefillUpstream(parsedEndpoint.origin, {}, versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null);
|
||||
setValue('endpoint-path', parsedEndpoint.pathname + parsedEndpoint.search);
|
||||
setValue('gql-query-editor', target.query_template);
|
||||
document.querySelectorAll('.gql-type-card').forEach(function(card) {
|
||||
card.classList.toggle('active', card.dataset.gqlType === target.operation_type);
|
||||
});
|
||||
} else if (target.kind === 'grpc') {
|
||||
prefillUpstream(target.server_addr, {});
|
||||
prefillUpstream(target.server_addr, {}, versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null);
|
||||
var route = '/' + (target.package ? target.package + '.' : '') + target.service + '/' + target.method;
|
||||
setValue('grpc-manual-route', route);
|
||||
setValue('grpc-manual-req-type', '');
|
||||
|
||||
@@ -247,5 +247,6 @@ DoD:
|
||||
2. `feat/auth-profile-secret-resolution`
|
||||
3. `feat/secrets-ui`
|
||||
4. `feat/wizard-auth-selector`
|
||||
5. `feat/manual-regression-pass`
|
||||
|
||||
Сейчас `feat/secrets-ui` уже закрыт: оператор может управлять encrypted secrets и видеть, какие `AuthProfile` на них ссылаются. Следующий шаг - убрать raw `Auth headers` из wizard primary flow и заменить их на нормальный auth selector и quick-create path для `Secret`/`AuthProfile`.
|
||||
Сейчас `feat/wizard-auth-selector` уже закрыт: wizard использует auth selector на шаге 2, сериализует `auth_profile_ref` в `execution_config`, умеет quick-create для `Secret` и `AuthProfile`, а raw `Auth headers` убраны из primary UX. Следующий шаг - ручной regression pass по REST / GraphQL / gRPC / WebSocket / SOAP и проверка связки `Secrets -> Auth Profiles -> Test Run / MCP call`.
|
||||
|
||||
Reference in New Issue
Block a user