Compare commits
1 Commits
secrets-mc
...
secrets-mc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf93488c6a |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -2065,7 +2065,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "secrets-mcp"
|
||||
version = "0.5.17"
|
||||
version = "0.5.18"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"askama",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "secrets-mcp"
|
||||
version = "0.5.17"
|
||||
version = "0.5.18"
|
||||
edition.workspace = true
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -138,6 +138,25 @@ pub(super) struct EntryOptionQuery {
|
||||
|
||||
type EntryApiError = (StatusCode, Json<serde_json::Value>);
|
||||
|
||||
fn require_encryption_key(headers: &HeaderMap, lang: UiLang) -> Result<[u8; 32], EntryApiError> {
|
||||
let enc_key_hex = headers
|
||||
.get("x-encryption-key")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "error": tr(lang, "缺少 X-Encryption-Key 请求头", "缺少 X-Encryption-Key 請求標頭", "Missing X-Encryption-Key header") })),
|
||||
)
|
||||
})?;
|
||||
|
||||
secrets_core::crypto::extract_key_from_hex(enc_key_hex).map_err(|_| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "error": tr(lang, "X-Encryption-Key 格式无效", "X-Encryption-Key 格式無效", "Invalid X-Encryption-Key format") })),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn map_entry_mutation_err(e: anyhow::Error, lang: UiLang) -> EntryApiError {
|
||||
if let Some(app_err) = e.downcast_ref::<AppError>() {
|
||||
return map_app_error(app_err, lang);
|
||||
@@ -876,6 +895,7 @@ pub(super) struct SecretPatchBody {
|
||||
name: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
secret_type: Option<String>,
|
||||
value: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
pub(super) async fn api_secret_patch(
|
||||
@@ -901,6 +921,7 @@ pub(super) async fn api_secret_patch(
|
||||
|
||||
let name = body.name.as_ref().map(|s| s.trim());
|
||||
let secret_type = body.secret_type.as_ref().map(|s| s.trim());
|
||||
let secret_value = body.value.as_ref();
|
||||
|
||||
if let Some(n) = name {
|
||||
if n.is_empty() {
|
||||
@@ -940,30 +961,37 @@ pub(super) async fn api_secret_patch(
|
||||
}
|
||||
}
|
||||
|
||||
if name.is_none() && secret_type.is_none() {
|
||||
if name.is_none() && secret_type.is_none() && secret_value.is_none() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(
|
||||
json!({ "error": tr(lang, "至少需要提供 name 或 type 之一", "至少需要提供 name 或 type 之一", "At least one of name or type is required") }),
|
||||
json!({ "error": tr(lang, "至少需要提供 name、type 或 value 之一", "至少需要提供 name、type 或 value 之一", "At least one of name, type, or value is required") }),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let master_key = if secret_value.is_some() {
|
||||
Some(require_encryption_key(&headers, lang)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut tx = state
|
||||
.pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| map_entry_mutation_err(e.into(), lang))?;
|
||||
|
||||
let secret_row: Option<(String, String)> =
|
||||
sqlx::query_as("SELECT name, type FROM secrets WHERE id = $1 AND user_id = $2 FOR UPDATE")
|
||||
.bind(secret_id)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| map_entry_mutation_err(e.into(), lang))?;
|
||||
let secret_row: Option<(String, String, Vec<u8>)> = sqlx::query_as(
|
||||
"SELECT name, type, encrypted FROM secrets WHERE id = $1 AND user_id = $2 FOR UPDATE",
|
||||
)
|
||||
.bind(secret_id)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| map_entry_mutation_err(e.into(), lang))?;
|
||||
|
||||
let Some((old_name, old_type)) = secret_row else {
|
||||
let Some((old_name, old_type, old_encrypted)) = secret_row else {
|
||||
let _ = tx.rollback().await;
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
@@ -988,13 +1016,47 @@ pub(super) async fn api_secret_patch(
|
||||
|
||||
let new_name = name.unwrap_or(&old_name).to_string();
|
||||
let new_type = secret_type.unwrap_or(&old_type).to_string();
|
||||
let new_encrypted = if let Some(value) = secret_value {
|
||||
let encrypted = secrets_core::crypto::encrypt_json(
|
||||
master_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "error": tr(lang, "请先设置密码短语后再编辑密文值", "請先設定密碼短語後再編輯密文值", "Unlock your passphrase before editing secret values") })),
|
||||
)
|
||||
})?,
|
||||
value,
|
||||
)
|
||||
.map_err(|e| map_entry_mutation_err(e, lang))?;
|
||||
Some(encrypted)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let value_changed = new_encrypted.is_some();
|
||||
|
||||
if let Err(e) = secrets_core::db::snapshot_secret_history(
|
||||
&mut tx,
|
||||
secrets_core::db::SecretSnapshotParams {
|
||||
secret_id,
|
||||
name: &old_name,
|
||||
encrypted: &old_encrypted,
|
||||
action: if value_changed { "update" } else { "rename" },
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, %secret_id, "failed to snapshot secret history before patch");
|
||||
}
|
||||
|
||||
let result = sqlx::query(
|
||||
"UPDATE secrets SET name = $1, type = $2, version = version + 1, updated_at = NOW() \
|
||||
WHERE id = $3",
|
||||
"UPDATE secrets SET name = $1, type = $2, encrypted = $3, version = version + 1, updated_at = NOW() \
|
||||
WHERE id = $4",
|
||||
)
|
||||
.bind(&new_name)
|
||||
.bind(&new_type)
|
||||
.bind(new_encrypted.as_deref().unwrap_or(&old_encrypted))
|
||||
.bind(secret_id)
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
@@ -1018,7 +1080,11 @@ pub(super) async fn api_secret_patch(
|
||||
secrets_core::audit::log_tx(
|
||||
&mut tx,
|
||||
Some(user_id),
|
||||
"rename_secret",
|
||||
if value_changed {
|
||||
"update_secret"
|
||||
} else {
|
||||
"rename_secret"
|
||||
},
|
||||
"",
|
||||
"",
|
||||
&old_name,
|
||||
@@ -1029,6 +1095,7 @@ pub(super) async fn api_secret_patch(
|
||||
"new_name": new_name,
|
||||
"old_type": old_type,
|
||||
"new_type": new_type,
|
||||
"value_updated": value_changed,
|
||||
"linked_entries": linked_entries,
|
||||
}),
|
||||
)
|
||||
@@ -1154,23 +1221,7 @@ pub(super) async fn api_entry_secrets_decrypt(
|
||||
Json(json!({ "error": tr(lang, "未登录", "尚未登入", "Not logged in") })),
|
||||
))?;
|
||||
|
||||
let enc_key_hex = headers
|
||||
.get("x-encryption-key")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "error": tr(lang, "缺少 X-Encryption-Key 请求头", "缺少 X-Encryption-Key 請求標頭", "Missing X-Encryption-Key header") })),
|
||||
)
|
||||
})?;
|
||||
|
||||
let master_key =
|
||||
secrets_core::crypto::extract_key_from_hex(enc_key_hex).map_err(|_| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "error": tr(lang, "X-Encryption-Key 格式无效", "X-Encryption-Key 格式無效", "Invalid X-Encryption-Key format") })),
|
||||
)
|
||||
})?;
|
||||
let master_key = require_encryption_key(&headers, lang)?;
|
||||
|
||||
let secrets =
|
||||
get_all_secrets_by_id(&state.pool, entry_id, &master_key, Some(user_id))
|
||||
|
||||
@@ -456,7 +456,16 @@
|
||||
padding: 7px 10px; word-break: break-all; white-space: pre-wrap;
|
||||
max-height: 140px; overflow: auto; color: #c9d1d9; line-height: 1.5;
|
||||
}
|
||||
|
||||
.view-secret-editor {
|
||||
width: 100%; min-height: 108px; resize: vertical; box-sizing: border-box;
|
||||
font-family: 'JetBrains Mono', monospace; font-size: 12px; line-height: 1.5;
|
||||
background: #0d1117; border: 1px solid rgba(240,246,252,0.08); border-radius: 10px;
|
||||
color: #c9d1d9; padding: 10px 12px; outline: none;
|
||||
}
|
||||
.view-secret-editor:focus { border-color: rgba(56,139,253,0.5); }
|
||||
.view-secret-hint {
|
||||
margin-top: 6px; font-size: 12px; color: #8b949e; line-height: 1.5;
|
||||
}
|
||||
.btn-icon {
|
||||
padding: 6px 10px; border-radius: 8px; font-size: 12px; cursor: pointer;
|
||||
border: 1px solid rgba(240,246,252,0.12); background: #161b22; color: #8b949e;
|
||||
@@ -484,7 +493,7 @@
|
||||
.btn-view-edit { color: #58a6ff; }
|
||||
.btn-view-save { color: #3fb950; }
|
||||
.btn-view-cancel { color: #8b949e; }
|
||||
.btn-view-unlink { color: #f85149; font-size: 14px; }
|
||||
.btn-view-unlink { color: #f85149; font-size: 12px; white-space: nowrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -763,6 +772,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
viewCopy: '复制',
|
||||
viewCopied: '已复制',
|
||||
viewLoading: '解密中…',
|
||||
viewEditSecret: '编辑密文',
|
||||
viewValueHintJson: '此值按 JSON 保存,请输入合法 JSON。',
|
||||
viewValueInvalidJson: '请输入合法 JSON 值',
|
||||
viewValueUnlockRequired: '请先在 MCP 配置页解锁密码短语后再修改密文值。',
|
||||
viewSaveChanges: '保存更改',
|
||||
viewChangesSaved: '已保存',
|
||||
viewUnlinkConfirm: '确定解除密文关联「{name}」?',
|
||||
@@ -843,6 +856,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
viewCopy: '複製',
|
||||
viewCopied: '已複製',
|
||||
viewLoading: '解密中…',
|
||||
viewEditSecret: '編輯密文',
|
||||
viewValueHintJson: '此值會以 JSON 儲存,請輸入合法 JSON。',
|
||||
viewValueInvalidJson: '請輸入合法 JSON 值',
|
||||
viewValueUnlockRequired: '請先在 MCP 設定頁解鎖密碼短語,再修改密文值。',
|
||||
viewSaveChanges: '儲存變更',
|
||||
viewChangesSaved: '已儲存',
|
||||
viewUnlinkConfirm: '確定解除密文關聯「{name}」?',
|
||||
@@ -923,6 +940,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
viewCopy: 'Copy',
|
||||
viewCopied: 'Copied',
|
||||
viewLoading: 'Decrypting…',
|
||||
viewEditSecret: 'Edit secret',
|
||||
viewValueHintJson: 'This value is stored as JSON. Enter valid JSON.',
|
||||
viewValueInvalidJson: 'Enter a valid JSON value',
|
||||
viewValueUnlockRequired: 'Unlock your passphrase on the MCP config page before editing secret values.',
|
||||
viewSaveChanges: 'Save changes',
|
||||
viewChangesSaved: 'Saved',
|
||||
viewUnlinkConfirm: 'Unlink secret "{name}"?',
|
||||
@@ -1161,9 +1182,81 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
if (e.target === viewOverlay) closeView();
|
||||
});
|
||||
|
||||
function parseEntrySecretSchema(tr) {
|
||||
if (!tr) return [];
|
||||
try {
|
||||
var raw = JSON.parse(tr.getAttribute('data-entry-secrets') || '[]');
|
||||
return Array.isArray(raw) ? raw : [];
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function renderEntrySecretChips(tr, secretSchema) {
|
||||
if (!tr) return;
|
||||
var list = tr.querySelector('.col-secrets .secret-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = '';
|
||||
(secretSchema || []).forEach(function (secret) {
|
||||
var chip = document.createElement('span');
|
||||
chip.className = 'secret-chip';
|
||||
chip.setAttribute('data-secret-id', secret.id || '');
|
||||
|
||||
var name = document.createElement('span');
|
||||
name.className = 'secret-name';
|
||||
name.title = secret.name || '';
|
||||
name.textContent = secret.name || '';
|
||||
|
||||
var type = document.createElement('span');
|
||||
type.className = 'secret-type';
|
||||
type.textContent = secret.secret_type || 'text';
|
||||
|
||||
chip.appendChild(name);
|
||||
chip.appendChild(type);
|
||||
list.appendChild(chip);
|
||||
});
|
||||
|
||||
var viewBtn = tr.querySelector('.btn-view-secrets');
|
||||
if (viewBtn) viewBtn.disabled = !(secretSchema && secretSchema.length);
|
||||
}
|
||||
|
||||
function writeEntrySecretSchema(entryId, secretSchema) {
|
||||
var tr = document.querySelector('tr[data-entry-id="' + entryId + '"]');
|
||||
if (!tr) return;
|
||||
tr.setAttribute('data-entry-secrets', JSON.stringify(secretSchema || []));
|
||||
renderEntrySecretChips(tr, secretSchema || []);
|
||||
}
|
||||
|
||||
function updateEntrySecretSchema(entryId, secretId, updater) {
|
||||
var tr = document.querySelector('tr[data-entry-id="' + entryId + '"]');
|
||||
if (!tr) return;
|
||||
var changed = false;
|
||||
var next = parseEntrySecretSchema(tr).map(function (secret) {
|
||||
if (String(secret.id || '') !== String(secretId || '')) return secret;
|
||||
changed = true;
|
||||
return updater(Object.assign({}, secret));
|
||||
});
|
||||
if (changed) writeEntrySecretSchema(entryId, next);
|
||||
}
|
||||
|
||||
function removeEntrySecretSchema(entryId, secretId) {
|
||||
var tr = document.querySelector('tr[data-entry-id="' + entryId + '"]');
|
||||
if (!tr) return;
|
||||
var schema = parseEntrySecretSchema(tr);
|
||||
writeEntrySecretSchema(entryId, schema.filter(function (secret) {
|
||||
return String(secret.id || '') !== String(secretId || '');
|
||||
}));
|
||||
}
|
||||
|
||||
function renderViewSecrets(secrets, secretSchema) {
|
||||
viewBody.innerHTML = '';
|
||||
var names = Object.keys(secrets);
|
||||
var names = [];
|
||||
(secretSchema || []).forEach(function (secret) {
|
||||
if (Object.prototype.hasOwnProperty.call(secrets, secret.name)) names.push(secret.name);
|
||||
});
|
||||
Object.keys(secrets).forEach(function (name) {
|
||||
if (names.indexOf(name) === -1) names.push(name);
|
||||
});
|
||||
if (names.length === 0) {
|
||||
var msg = document.createElement('div');
|
||||
msg.className = 'view-locked-msg';
|
||||
@@ -1177,18 +1270,42 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
|
||||
names.forEach(function (name) {
|
||||
var raw = secrets[name];
|
||||
var valueStr = (raw === null || raw === undefined) ? '' :
|
||||
(typeof raw === 'object') ? JSON.stringify(raw, null, 2) : String(raw);
|
||||
var currentName = name;
|
||||
var valueMode = (typeof raw === 'string') ? 'text' : 'json';
|
||||
var valueStr = (typeof raw === 'string') ? raw : JSON.stringify(raw, null, 2);
|
||||
|
||||
var schema = schemaMap[name] || {};
|
||||
var secretId = schema.id || '';
|
||||
var secretType = schema.secret_type || 'text';
|
||||
var originalName = name;
|
||||
var hasChanges = false;
|
||||
var currentType = schema.secret_type || 'text';
|
||||
|
||||
function formatSecretValue(value) {
|
||||
return (typeof value === 'string') ? value : JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function parseEditedSecretValue(text) {
|
||||
if (valueMode === 'text') return { ok: true, value: text };
|
||||
try {
|
||||
return { ok: true, value: JSON.parse(text) };
|
||||
} catch (err) {
|
||||
return { ok: false, error: t('viewValueInvalidJson') };
|
||||
}
|
||||
}
|
||||
|
||||
function comparableSecretValue(value) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function applyCurrentSecretValue(value) {
|
||||
raw = value;
|
||||
valueStr = formatSecretValue(value);
|
||||
valueEl.textContent = valueStr;
|
||||
valueEditor.value = valueStr;
|
||||
}
|
||||
|
||||
var row = document.createElement('div');
|
||||
row.className = 'view-secret-row';
|
||||
row.setAttribute('data-secret-id', secretId);
|
||||
row.setAttribute('data-original-name', originalName);
|
||||
row.setAttribute('data-original-name', currentName);
|
||||
|
||||
var header = document.createElement('div');
|
||||
header.className = 'view-secret-header';
|
||||
@@ -1203,14 +1320,13 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
var nameInput = document.createElement('input');
|
||||
nameInput.type = 'text';
|
||||
nameInput.className = 'view-secret-name-input';
|
||||
nameInput.value = name;
|
||||
nameInput.value = currentName;
|
||||
nameInput.placeholder = t('renameSecretPlaceholder');
|
||||
nameInput.setAttribute('data-original-name', originalName);
|
||||
nameInput.hidden = true;
|
||||
|
||||
var typeBadge = document.createElement('span');
|
||||
typeBadge.className = 'view-secret-type';
|
||||
typeBadge.textContent = secretType;
|
||||
typeBadge.textContent = currentType;
|
||||
|
||||
var typeSelect = document.createElement('select');
|
||||
typeSelect.className = 'view-secret-type-select';
|
||||
@@ -1219,13 +1335,13 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
var option = document.createElement('option');
|
||||
option.value = opt;
|
||||
option.textContent = opt;
|
||||
if (opt === secretType) option.selected = true;
|
||||
if (opt === currentType) option.selected = true;
|
||||
typeSelect.appendChild(option);
|
||||
});
|
||||
if (SECRET_TYPE_OPTIONS.indexOf(secretType) === -1 && secretType) {
|
||||
if (SECRET_TYPE_OPTIONS.indexOf(currentType) === -1 && currentType) {
|
||||
var fallback = document.createElement('option');
|
||||
fallback.value = secretType;
|
||||
fallback.textContent = secretType;
|
||||
fallback.value = currentType;
|
||||
fallback.textContent = currentType;
|
||||
fallback.selected = true;
|
||||
typeSelect.appendChild(fallback);
|
||||
}
|
||||
@@ -1242,8 +1358,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
var editBtn = document.createElement('button');
|
||||
editBtn.type = 'button';
|
||||
editBtn.className = 'btn-icon btn-view-edit';
|
||||
editBtn.textContent = '✎';
|
||||
editBtn.title = t('renameSecretTitle');
|
||||
editBtn.textContent = t('viewEditSecret');
|
||||
|
||||
var saveBtn = document.createElement('button');
|
||||
saveBtn.type = 'button';
|
||||
@@ -1272,8 +1387,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
var unlinkBtn = document.createElement('button');
|
||||
unlinkBtn.type = 'button';
|
||||
unlinkBtn.className = 'btn-icon btn-view-unlink';
|
||||
unlinkBtn.textContent = '×';
|
||||
unlinkBtn.title = t('unlinkTitle');
|
||||
unlinkBtn.textContent = t('unlinkTitle');
|
||||
actions.appendChild(unlinkBtn);
|
||||
|
||||
actions.appendChild(editBtn);
|
||||
@@ -1287,9 +1401,23 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
var valueEl = document.createElement('div');
|
||||
valueEl.className = 'view-secret-value';
|
||||
valueEl.textContent = valueStr;
|
||||
var valueEditor = document.createElement('textarea');
|
||||
valueEditor.className = 'view-secret-editor';
|
||||
valueEditor.hidden = true;
|
||||
valueEditor.value = valueStr;
|
||||
valueWrap.appendChild(valueEl);
|
||||
valueWrap.appendChild(valueEditor);
|
||||
row.appendChild(valueWrap);
|
||||
|
||||
var valueHint = null;
|
||||
if (valueMode === 'json') {
|
||||
valueHint = document.createElement('div');
|
||||
valueHint.className = 'view-secret-hint';
|
||||
valueHint.hidden = true;
|
||||
valueHint.textContent = t('viewValueHintJson');
|
||||
row.appendChild(valueHint);
|
||||
}
|
||||
|
||||
var nameStatus = document.createElement('div');
|
||||
nameStatus.className = 'secret-name-status';
|
||||
nameStatus.setAttribute('data-status', 'idle');
|
||||
@@ -1301,8 +1429,11 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
function enterEditMode() {
|
||||
nameSpan.hidden = true;
|
||||
typeBadge.hidden = true;
|
||||
valueEl.hidden = true;
|
||||
nameInput.hidden = false;
|
||||
typeSelect.hidden = false;
|
||||
valueEditor.hidden = false;
|
||||
if (valueHint) valueHint.hidden = false;
|
||||
saveBtn.hidden = false;
|
||||
cancelBtn.hidden = false;
|
||||
editBtn.hidden = true;
|
||||
@@ -1313,8 +1444,11 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
function exitEditMode() {
|
||||
nameSpan.hidden = false;
|
||||
typeBadge.hidden = false;
|
||||
valueEl.hidden = false;
|
||||
nameInput.hidden = true;
|
||||
typeSelect.hidden = true;
|
||||
valueEditor.hidden = true;
|
||||
if (valueHint) valueHint.hidden = true;
|
||||
saveBtn.hidden = true;
|
||||
cancelBtn.hidden = true;
|
||||
editBtn.hidden = false;
|
||||
@@ -1322,7 +1456,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
nameStatus.className = 'secret-name-status';
|
||||
nameInput.value = nameSpan.textContent;
|
||||
typeSelect.value = typeBadge.textContent;
|
||||
hasChanges = false;
|
||||
valueEditor.value = valueStr;
|
||||
}
|
||||
|
||||
editBtn.addEventListener('click', enterEditMode);
|
||||
@@ -1337,7 +1471,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
nameStatus.className = 'secret-name-status';
|
||||
debounceTimer = setTimeout(function () {
|
||||
var newName = nameInput.value.trim();
|
||||
if (!newName || newName === originalName) return;
|
||||
if (!newName || newName === currentName) return;
|
||||
nameStatus.textContent = t('checkingSecretName');
|
||||
nameStatus.className = 'secret-name-status checking';
|
||||
var checkId = Date.now();
|
||||
@@ -1352,18 +1486,15 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
if (data.ok && data.available) {
|
||||
nameStatus.textContent = t('secretNameAvailable');
|
||||
nameStatus.className = 'secret-name-status success';
|
||||
hasChanges = true;
|
||||
} else {
|
||||
nameStatus.textContent = data.error || t('secretNameTaken');
|
||||
nameStatus.className = 'secret-name-status error';
|
||||
hasChanges = false;
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
if (currentCheck !== checkId) return;
|
||||
nameStatus.textContent = t('secretNameCheckError');
|
||||
nameStatus.className = 'secret-name-status error';
|
||||
hasChanges = false;
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
@@ -1372,21 +1503,46 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
if (e.key === 'Enter') { e.preventDefault(); saveBtn.click(); }
|
||||
if (e.key === 'Escape') { cancelBtn.click(); }
|
||||
});
|
||||
valueEditor.addEventListener('keydown', function (e) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
saveBtn.click();
|
||||
}
|
||||
if (e.key === 'Escape') cancelBtn.click();
|
||||
});
|
||||
|
||||
// ── Save ──
|
||||
saveBtn.addEventListener('click', function () {
|
||||
var newName = nameInput.value.trim();
|
||||
var newType = typeSelect.value;
|
||||
var parsedValue = parseEditedSecretValue(valueEditor.value);
|
||||
if (!newName) { nameStatus.textContent = t('secretNameInvalid'); nameStatus.className = 'secret-name-status error'; return; }
|
||||
if (!newType) { nameStatus.textContent = t('secretTypeInvalid'); nameStatus.className = 'secret-name-status error'; return; }
|
||||
if (!parsedValue.ok) { nameStatus.textContent = parsedValue.error; nameStatus.className = 'secret-name-status error'; return; }
|
||||
|
||||
var nextValue = parsedValue.value;
|
||||
var patchBody = {};
|
||||
if (newName !== originalName) patchBody.name = newName;
|
||||
if (newType !== secretType) patchBody.type = newType;
|
||||
var valueChanged = comparableSecretValue(nextValue) !== comparableSecretValue(raw);
|
||||
if (newName !== currentName) patchBody.name = newName;
|
||||
if (newType !== currentType) patchBody.type = newType;
|
||||
if (valueChanged) patchBody.value = nextValue;
|
||||
if (Object.keys(patchBody).length === 0) { exitEditMode(); return; }
|
||||
var encKey = sessionStorage.getItem('enc_key');
|
||||
if (valueChanged && !encKey) {
|
||||
nameStatus.textContent = t('viewValueUnlockRequired');
|
||||
nameStatus.className = 'secret-name-status error';
|
||||
return;
|
||||
}
|
||||
saveBtn.textContent = '...';
|
||||
saveBtn.disabled = true;
|
||||
cancelBtn.disabled = true;
|
||||
editBtn.disabled = true;
|
||||
fetch('/api/secrets/' + encodeURIComponent(secretId), {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: Object.assign(
|
||||
{ 'Content-Type': 'application/json' },
|
||||
valueChanged ? { 'X-Encryption-Key': encKey } : {}
|
||||
),
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(patchBody)
|
||||
}).then(function (r) {
|
||||
@@ -1395,22 +1551,28 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
return data;
|
||||
});
|
||||
}).then(function () {
|
||||
currentName = newName;
|
||||
currentType = newType;
|
||||
nameSpan.textContent = newName;
|
||||
typeBadge.textContent = newType;
|
||||
originalName = newName;
|
||||
nameInput.setAttribute('data-original-name', newName);
|
||||
applyCurrentSecretValue(nextValue);
|
||||
saveBtn.textContent = t('viewChangesSaved');
|
||||
nameStatus.textContent = t('viewChangesSaved');
|
||||
nameStatus.className = 'secret-name-status success';
|
||||
updateEntrySecretSchema(viewBody.getAttribute('data-entry-id'), secretId, function (secret) {
|
||||
secret.name = newName;
|
||||
secret.secret_type = newType;
|
||||
return secret;
|
||||
});
|
||||
setTimeout(function () { exitEditMode(); saveBtn.textContent = t('viewSaveChanges'); }, 1200);
|
||||
// Update table row chip
|
||||
var tableRow = document.querySelector('tr[data-entry-id="' + viewBody.getAttribute('data-entry-id') + '"]');
|
||||
if (tableRow) {
|
||||
var chip = tableRow.querySelector('.secret-chip .secret-name');
|
||||
if (chip && chip.textContent === name) chip.textContent = newName;
|
||||
}
|
||||
}).catch(function (err) {
|
||||
nameStatus.textContent = err.message || String(err);
|
||||
nameStatus.className = 'secret-name-status error';
|
||||
saveBtn.textContent = t('viewSaveChanges');
|
||||
}).finally(function () {
|
||||
saveBtn.disabled = false;
|
||||
cancelBtn.disabled = false;
|
||||
editBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1434,15 +1596,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
msg.textContent = t('viewNoSecrets');
|
||||
viewBody.appendChild(msg);
|
||||
}
|
||||
// Update table row
|
||||
var tableRow = document.querySelector('tr[data-entry-id="' + viewBody.getAttribute('data-entry-id') + '"]');
|
||||
if (tableRow) {
|
||||
var chip = tableRow.querySelector('.secret-chip');
|
||||
if (chip) {
|
||||
var chipName = chip.querySelector('.secret-name');
|
||||
if (chipName && chipName.textContent === name) chip.remove();
|
||||
}
|
||||
}
|
||||
removeEntrySecretSchema(viewBody.getAttribute('data-entry-id'), secretId);
|
||||
}).catch(function (err) {
|
||||
alert(err.message || String(err));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user