feat: add wizard auth selector

This commit is contained in:
a.tolmachev
2026-04-07 11:38:00 +03:00
parent 341496e314
commit 6711214bb1
8 changed files with 626 additions and 85 deletions
+407 -63
View File
@@ -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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
@@ -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', '');