release(secrets-mcp): 0.5.9 — users.key_version 与会话失效;Web 条目解密 API 与列表增强
This commit is contained in:
@@ -128,7 +128,7 @@
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
th, td { text-align: left; vertical-align: top; padding: 12px 10px; border-top: 1px solid var(--border); }
|
||||
th, td { text-align: left; vertical-align: middle; padding: 12px 10px; border-top: 1px solid var(--border); }
|
||||
th {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
@@ -138,24 +138,28 @@
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: var(--surface);
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
td { font-size: 13px; line-height: 1.45; }
|
||||
tbody tr:nth-child(2n) td { background: rgba(255, 255, 255, 0.01); }
|
||||
.mono { font-family: 'JetBrains Mono', monospace; }
|
||||
.col-type { min-width: 108px; width: 1%; }
|
||||
.col-name { min-width: 180px; max-width: 260px; }
|
||||
.col-type { min-width: 108px; width: 1%; text-align: center; vertical-align: middle; }
|
||||
.col-name { min-width: 180px; max-width: 260px; text-align: center; vertical-align: middle; }
|
||||
.col-tags { min-width: 160px; max-width: 220px; }
|
||||
.col-secrets { min-width: 220px; max-width: 420px; vertical-align: top; }
|
||||
.col-secrets { min-width: 220px; max-width: 420px; vertical-align: middle; }
|
||||
.col-secrets .secret-list { max-height: 120px; overflow: auto; }
|
||||
.col-actions { min-width: 132px; width: 1%; }
|
||||
.col-actions { min-width: 132px; width: 1%; text-align: center; vertical-align: middle; }
|
||||
.cell-name, .cell-tags-val {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
.cell-notes { min-width: 260px; max-width: 360px; }
|
||||
.notes-scroll {
|
||||
max-height: 120px;
|
||||
height: calc(1.5em * 2 + 16px);
|
||||
min-height: calc(1.5em * 2 + 16px);
|
||||
overflow: auto;
|
||||
resize: vertical;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
padding: 8px;
|
||||
@@ -170,7 +174,7 @@
|
||||
max-width: 360px; max-height: 120px; overflow: auto;
|
||||
}
|
||||
.col-actions { white-space: nowrap; }
|
||||
.row-actions { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.row-actions { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; align-items: center; }
|
||||
.secret-list { display: flex; flex-wrap: wrap; gap: 6px; max-width: 100%; }
|
||||
.secret-chip {
|
||||
display: inline-flex;
|
||||
@@ -312,7 +316,11 @@
|
||||
color: var(--text); padding: 8px 10px; font-size: 13px; font-family: 'JetBrains Mono', monospace;
|
||||
outline: none;
|
||||
}
|
||||
.modal-field textarea { min-height: 72px; resize: vertical; }
|
||||
.modal-field textarea { resize: vertical; }
|
||||
#edit-notes {
|
||||
height: calc(1.5em * 2 + 16px);
|
||||
min-height: calc(1.5em * 2 + 16px);
|
||||
}
|
||||
.modal-field textarea.metadata-edit { min-height: 140px; }
|
||||
.modal-readonly-value {
|
||||
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
|
||||
@@ -348,6 +356,9 @@
|
||||
margin-bottom: 4px; text-transform: uppercase;
|
||||
content: attr(data-label);
|
||||
}
|
||||
.col-name, .col-type, .col-actions { text-align: left; }
|
||||
th, td { vertical-align: top; }
|
||||
.row-actions { justify-content: flex-start; }
|
||||
.detail, .notes-scroll, .secret-list { max-width: none; }
|
||||
}
|
||||
.pagination {
|
||||
@@ -368,6 +379,43 @@
|
||||
.page-info {
|
||||
color: var(--text-muted); font-size: 13px; font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.view-secret-row {
|
||||
display: flex; flex-direction: column; gap: 4px; padding: 8px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.view-secret-row:last-child { border-bottom: none; }
|
||||
.view-secret-header {
|
||||
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||||
}
|
||||
.view-secret-name {
|
||||
font-family: 'JetBrains Mono', monospace; font-size: 12px;
|
||||
color: var(--text); font-weight: 600;
|
||||
}
|
||||
.view-secret-type {
|
||||
font-family: 'JetBrains Mono', monospace; font-size: 11px;
|
||||
color: var(--text-muted); background: var(--surface2);
|
||||
border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px;
|
||||
}
|
||||
.view-secret-actions { margin-left: auto; display: flex; gap: 6px; }
|
||||
.view-secret-value-wrap { position: relative; }
|
||||
.view-secret-value {
|
||||
font-family: 'JetBrains Mono', monospace; font-size: 12px;
|
||||
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
|
||||
padding: 7px 10px; word-break: break-all; white-space: pre-wrap;
|
||||
max-height: 140px; overflow: auto; color: var(--text); line-height: 1.5;
|
||||
}
|
||||
.view-secret-value.masked { letter-spacing: 2px; user-select: none; filter: blur(4px); }
|
||||
.btn-icon {
|
||||
padding: 3px 8px; border-radius: 5px; font-size: 11px; cursor: pointer;
|
||||
border: 1px solid var(--border); background: var(--surface2); color: var(--text-muted);
|
||||
font-family: inherit;
|
||||
}
|
||||
.btn-icon:hover { color: var(--text); border-color: var(--text-muted); }
|
||||
.view-locked-msg {
|
||||
font-size: 13px; color: var(--text-muted); padding: 16px 0;
|
||||
line-height: 1.6; text-align: center;
|
||||
}
|
||||
.view-locked-msg a { color: var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -465,7 +513,8 @@
|
||||
</td>
|
||||
<td class="col-actions" data-label="操作">
|
||||
<div class="row-actions">
|
||||
<button type="button" class="btn-row btn-edit" data-i18n="rowEdit">编辑</button>
|
||||
<button type="button" class="btn-row btn-view-secrets" data-i18n="rowView">查看密文</button>
|
||||
<button type="button" class="btn-row btn-edit" data-i18n="rowEdit">编辑条目</button>
|
||||
<button type="button" class="btn-row danger btn-del" data-i18n="rowDelete">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -498,7 +547,7 @@
|
||||
|
||||
<div id="edit-overlay" class="modal-overlay" hidden>
|
||||
<div class="modal modal-wide" role="dialog" aria-modal="true" aria-labelledby="edit-title">
|
||||
<div class="modal-title" id="edit-title" data-i18n="modalTitle">编辑条目</div>
|
||||
<div class="modal-title" id="edit-title" data-i18n="modalTitle">编辑条目信息</div>
|
||||
<div id="edit-error" class="modal-error"></div>
|
||||
<div class="modal-field"><label for="edit-name" data-i18n="modalName">名称</label><input id="edit-name" type="text" autocomplete="off"></div>
|
||||
<div class="modal-field"><label for="edit-type" data-i18n="modalType">类型</label><input id="edit-type" type="text" autocomplete="off"></div>
|
||||
@@ -528,6 +577,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="view-overlay" class="modal-overlay" hidden>
|
||||
<div class="modal modal-wide" role="dialog" aria-modal="true" aria-labelledby="view-title">
|
||||
<div class="modal-title" id="view-title" data-i18n="viewTitle">查看条目密文</div>
|
||||
<div id="view-entry-name" style="font-size:13px;color:var(--text-muted);margin-bottom:14px;font-family:'JetBrains Mono',monospace;"></div>
|
||||
<div id="view-body"></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-modal" id="view-close" data-i18n="modalCancel">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/i18n.js"></script>
|
||||
<script id="secret-type-options" type="application/json">{{ secret_type_options_json|safe }}</script>
|
||||
<script>
|
||||
@@ -551,9 +611,9 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
colTags: '标签',
|
||||
colSecrets: '密文',
|
||||
colActions: '操作',
|
||||
rowEdit: '编辑',
|
||||
rowEdit: '编辑条目',
|
||||
rowDelete: '删除',
|
||||
modalTitle: '编辑条目',
|
||||
modalTitle: '编辑条目信息',
|
||||
modalName: '名称',
|
||||
modalType: '类型',
|
||||
modalFolder: '文件夹',
|
||||
@@ -591,6 +651,16 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
secretTypeInvalid: '类型不能为空',
|
||||
prevPage: '上一页',
|
||||
nextPage: '下一页',
|
||||
rowView: '查看密文',
|
||||
viewTitle: '查看条目密文',
|
||||
viewNoSecrets: '该条目没有关联的密文字段。',
|
||||
viewLockedMsg: '请先前往 <a href="/dashboard">MCP 配置页</a> 解锁密码短语,然后再查看密文。',
|
||||
viewDecryptError: '解密失败,请确认密码短语与加密时一致。',
|
||||
viewCopy: '复制',
|
||||
viewCopied: '已复制',
|
||||
viewShow: '显示',
|
||||
viewHide: '隐藏',
|
||||
viewLoading: '解密中…',
|
||||
},
|
||||
'zh-TW': {
|
||||
pageTitle: 'Secrets — 條目',
|
||||
@@ -609,9 +679,9 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
colTags: '標籤',
|
||||
colSecrets: '密文',
|
||||
colActions: '操作',
|
||||
rowEdit: '編輯',
|
||||
rowEdit: '編輯條目',
|
||||
rowDelete: '刪除',
|
||||
modalTitle: '編輯條目',
|
||||
modalTitle: '編輯條目資訊',
|
||||
modalName: '名稱',
|
||||
modalType: '類型',
|
||||
modalFolder: '資料夾',
|
||||
@@ -649,6 +719,16 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
secretTypeInvalid: '類型不能為空',
|
||||
prevPage: '上一頁',
|
||||
nextPage: '下一頁',
|
||||
rowView: '查看密文',
|
||||
viewTitle: '查看條目密文',
|
||||
viewNoSecrets: '該條目沒有關聯的密文欄位。',
|
||||
viewLockedMsg: '請先前往 <a href="/dashboard">MCP 設定頁</a> 解鎖密碼短語,再查看密文。',
|
||||
viewDecryptError: '解密失敗,請確認密碼短語與加密時一致。',
|
||||
viewCopy: '複製',
|
||||
viewCopied: '已複製',
|
||||
viewShow: '顯示',
|
||||
viewHide: '隱藏',
|
||||
viewLoading: '解密中…',
|
||||
},
|
||||
en: {
|
||||
pageTitle: 'Secrets — Entries',
|
||||
@@ -667,9 +747,9 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
colTags: 'Tags',
|
||||
colSecrets: 'Secrets',
|
||||
colActions: 'Actions',
|
||||
rowEdit: 'Edit',
|
||||
rowEdit: 'Edit entry',
|
||||
rowDelete: 'Delete',
|
||||
modalTitle: 'Edit entry',
|
||||
modalTitle: 'Edit entry details',
|
||||
modalName: 'Name',
|
||||
modalType: 'Type',
|
||||
modalFolder: 'Folder',
|
||||
@@ -706,7 +786,17 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
secretTypePlaceholder: 'Select type',
|
||||
secretTypeInvalid: 'Type cannot be empty',
|
||||
prevPage: 'Previous',
|
||||
nextPage: 'Next'
|
||||
nextPage: 'Next',
|
||||
rowView: 'View secrets',
|
||||
viewTitle: 'View entry secrets',
|
||||
viewNoSecrets: 'This entry has no associated secret fields.',
|
||||
viewLockedMsg: 'Please go to the <a href="/dashboard">MCP config page</a> to unlock your passphrase first.',
|
||||
viewDecryptError: 'Decryption failed. Please verify your passphrase matches the one used when encrypting.',
|
||||
viewCopy: 'Copy',
|
||||
viewCopied: 'Copied',
|
||||
viewShow: 'Show',
|
||||
viewHide: 'Hide',
|
||||
viewLoading: 'Decrypting…',
|
||||
}
|
||||
};
|
||||
|
||||
@@ -757,6 +847,137 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
var currentEntryId = null;
|
||||
var pendingDeleteId = null;
|
||||
|
||||
// ── View secrets modal ────────────────────────────────────────────────────
|
||||
var viewOverlay = document.getElementById('view-overlay');
|
||||
var viewEntryName = document.getElementById('view-entry-name');
|
||||
var viewBody = document.getElementById('view-body');
|
||||
|
||||
function closeView() {
|
||||
viewOverlay.hidden = true;
|
||||
viewBody.innerHTML = '';
|
||||
viewEntryName.textContent = '';
|
||||
}
|
||||
|
||||
document.getElementById('view-close').addEventListener('click', closeView);
|
||||
viewOverlay.addEventListener('click', function (e) {
|
||||
if (e.target === viewOverlay) closeView();
|
||||
});
|
||||
|
||||
function renderViewSecrets(secrets) {
|
||||
viewBody.innerHTML = '';
|
||||
var names = Object.keys(secrets);
|
||||
if (names.length === 0) {
|
||||
var msg = document.createElement('div');
|
||||
msg.className = 'view-locked-msg';
|
||||
msg.textContent = t('viewNoSecrets');
|
||||
viewBody.appendChild(msg);
|
||||
return;
|
||||
}
|
||||
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 isPassword = (name === 'password' || name === 'passwd' || name === 'secret');
|
||||
var masked = isPassword;
|
||||
|
||||
var row = document.createElement('div');
|
||||
row.className = 'view-secret-row';
|
||||
|
||||
var header = document.createElement('div');
|
||||
header.className = 'view-secret-header';
|
||||
|
||||
var nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'view-secret-name';
|
||||
nameSpan.textContent = name;
|
||||
header.appendChild(nameSpan);
|
||||
|
||||
var actions = document.createElement('div');
|
||||
actions.className = 'view-secret-actions';
|
||||
|
||||
if (isPassword) {
|
||||
var toggleBtn = document.createElement('button');
|
||||
toggleBtn.type = 'button';
|
||||
toggleBtn.className = 'btn-icon btn-toggle-mask';
|
||||
toggleBtn.textContent = t('viewShow');
|
||||
toggleBtn.addEventListener('click', function () {
|
||||
masked = !masked;
|
||||
valueEl.classList.toggle('masked', masked);
|
||||
toggleBtn.textContent = masked ? t('viewShow') : t('viewHide');
|
||||
});
|
||||
actions.appendChild(toggleBtn);
|
||||
}
|
||||
|
||||
var copyBtn = document.createElement('button');
|
||||
copyBtn.type = 'button';
|
||||
copyBtn.className = 'btn-icon';
|
||||
copyBtn.textContent = t('viewCopy');
|
||||
copyBtn.addEventListener('click', function () {
|
||||
navigator.clipboard.writeText(valueStr).then(function () {
|
||||
copyBtn.textContent = t('viewCopied');
|
||||
setTimeout(function () { copyBtn.textContent = t('viewCopy'); }, 1800);
|
||||
}).catch(function () {});
|
||||
});
|
||||
actions.appendChild(copyBtn);
|
||||
|
||||
header.appendChild(actions);
|
||||
row.appendChild(header);
|
||||
|
||||
var valueWrap = document.createElement('div');
|
||||
valueWrap.className = 'view-secret-value-wrap';
|
||||
var valueEl = document.createElement('div');
|
||||
valueEl.className = 'view-secret-value' + (masked ? ' masked' : '');
|
||||
valueEl.textContent = valueStr;
|
||||
valueWrap.appendChild(valueEl);
|
||||
row.appendChild(valueWrap);
|
||||
|
||||
viewBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function openView(tr) {
|
||||
var entryId = tr.getAttribute('data-entry-id');
|
||||
var nameEl = tr.querySelector('.cell-name');
|
||||
var entryName = nameEl ? nameEl.textContent.trim() : '';
|
||||
var encKey = sessionStorage.getItem('enc_key');
|
||||
|
||||
viewEntryName.textContent = entryName;
|
||||
viewBody.innerHTML = '';
|
||||
viewOverlay.hidden = false;
|
||||
|
||||
if (!encKey) {
|
||||
var msg = document.createElement('div');
|
||||
msg.className = 'view-locked-msg';
|
||||
msg.innerHTML = t('viewLockedMsg');
|
||||
viewBody.appendChild(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
var loadingMsg = document.createElement('div');
|
||||
loadingMsg.className = 'view-locked-msg';
|
||||
loadingMsg.textContent = t('viewLoading');
|
||||
viewBody.appendChild(loadingMsg);
|
||||
|
||||
fetch('/api/entries/' + encodeURIComponent(entryId) + '/secrets/decrypt', {
|
||||
credentials: 'same-origin',
|
||||
headers: { 'X-Encryption-Key': encKey }
|
||||
}).then(function (r) {
|
||||
return r.json().then(function (data) {
|
||||
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
|
||||
return data;
|
||||
});
|
||||
}).then(function (data) {
|
||||
renderViewSecrets(data.secrets || {});
|
||||
}).catch(function (e) {
|
||||
viewBody.innerHTML = '';
|
||||
var errMsg = document.createElement('div');
|
||||
errMsg.className = 'view-locked-msg';
|
||||
errMsg.style.color = '#f85149';
|
||||
errMsg.textContent = e.message || t('viewDecryptError');
|
||||
viewBody.appendChild(errMsg);
|
||||
});
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function showEditErr(msg) {
|
||||
editError.textContent = msg || '';
|
||||
editError.classList.toggle('visible', !!msg);
|
||||
@@ -981,6 +1202,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && !editOverlay.hidden) closeEdit();
|
||||
if (e.key === 'Escape' && !deleteOverlay.hidden) closeDelete();
|
||||
if (e.key === 'Escape' && !viewOverlay.hidden) closeView();
|
||||
});
|
||||
|
||||
function showDeleteErr(msg) {
|
||||
@@ -1295,6 +1517,12 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
});
|
||||
|
||||
document.querySelectorAll('tr[data-entry-id]').forEach(function (tr) {
|
||||
var viewBtn = tr.querySelector('.btn-view-secrets');
|
||||
if (viewBtn) {
|
||||
var hasSecrets = tr.querySelectorAll('.secret-chip').length > 0;
|
||||
if (!hasSecrets) viewBtn.disabled = true;
|
||||
viewBtn.addEventListener('click', function () { openView(tr); });
|
||||
}
|
||||
tr.querySelector('.btn-edit').addEventListener('click', function () { openEdit(tr); });
|
||||
tr.querySelector('.btn-del').addEventListener('click', function () {
|
||||
var id = tr.getAttribute('data-entry-id');
|
||||
|
||||
Reference in New Issue
Block a user