Files
MC_Report/public/js/pages/external-api.js
canglan 257cd14051 feat: external API for QQ bot - client id+secret auth, ticket tracking, bans, per-server data
- db: api_clients table + server_groups.alias + tickets/bans server_name (auto-migrate)
- external.js: client_id+secret auth (multi-client, bcrypt, show-once secret),
  legacy x-external-key compat, JWT pass-through
  - POST /tickets (optional server), GET /tickets/track?token= (full lifecycle)
  - GET/POST /bans, reported-players/stats/all-tickets with server filter
  - GET /servers (alias list), client CRUD (owner/admin, audit logged)
  - resolveServer: alias | 分组/子服 | server_name
- polls.js: server_groups CRUD with alias (add/edit, dup check)
- servers page: alias display + edit; new 外部API page: client mgmt + quick docs
- API.md: full external API section (auth matrix, server param syntax)
- verified: 37 checks incl. resolveServer sandbox (alias/group/fallback)
2026-08-19 19:12:34 +08:00

82 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const ExternalApiPage = {
async render() { return '<div class="loading"><i class="fas fa-spinner"></i></div>'; },
async mount() {
document.getElementById('page-title').textContent = '外部API';
document.getElementById('page-actions').innerHTML = '<button class="btn btn-p btn-sm" onclick="ExternalApiPage.showCreate()"><i class="fas fa-plus"></i> 创建客户端</button>';
await this.load();
},
async load() {
const ct = document.getElementById('page-content');
try {
const clients = await API.get('/external/clients');
ct.innerHTML = `
<div class="card"><div class="card-h"><i class="fas fa-key"></i> API 客户端</div><div class="card-b">
${clients.length === 0 ? '<div class="empty"><i class="fas fa-key"></i><p>暂无客户端</p></div>' : `<div class="table-wrap"><table><thead><tr><th>#</th><th>名称</th><th>Client ID</th><th>状态</th><th>创建时间</th><th>操作</th></tr></thead><tbody>${clients.map(c => `
<tr>
<td>${c.id}</td>
<td>${U.esc(c.name)}</td>
<td><code style="font-size:11px">${U.esc(c.client_id)}</code></td>
<td>${c.active ? '<span class="badge bg-resolved">启用</span>' : '<span class="badge bg-rejected">停用</span>'}</td>
<td class="ts">${U.date(c.created_at)}</td>
<td>
<button class="btn btn-o btn-sm" onclick="ExternalApiPage.toggle(${c.id},${c.active?0:1})">${c.active?'停用':'启用'}</button>
<button class="btn btn-d btn-sm" onclick="ExternalApiPage.del(${c.id})"><i class="fas fa-trash"></i></button>
</td>
</tr>`).join('')}</tbody></table></div>`}
<div class="alert alert-i" style="margin-top:12px"><b>鉴权方式:</b> 请求头 <code>x-api-client-id</code> + <code>x-api-secret</code>(推荐);旧版单 key 仍可用 <code>x-external-key</code>。secret 仅创建时显示一次,请立即保存。</div>
</div></div>
<div class="card"><div class="card-h"><i class="fas fa-book"></i> 接口速览(免用户登录, 需上述鉴权头)</div><div class="card-b"><div class="table-wrap"><table><thead><tr><th>方法</th><th>路径</th><th>说明</th></tr></thead><tbody>
<tr><td>POST</td><td><code>/api/external/tickets</code></td><td>提交工单(举报/建议/申诉),返回 id + tracking_token;可带 <code>server</code>(别名或「分组/子服」)</td></tr>
<tr><td>GET</td><td><code>/api/external/tickets/track?token=xxx</code></td><td>按追踪码查工单状态 + 回复,从提交到结束全程可查</td></tr>
<tr><td>GET</td><td><code>/api/external/all-tickets?server=&status=&type=&page=&limit=</code></td><td>工单列表(可按子服过滤)</td></tr>
<tr><td>GET</td><td><code>/api/external/all-tickets/:id</code></td><td>工单详情(含回复/附件)</td></tr>
<tr><td>GET</td><td><code>/api/external/bans?server=&status=&player=</code></td><td>拉取封禁列表</td></tr>
<tr><td>POST</td><td><code>/api/external/bans</code></td><td>新增封禁(可带 server)</td></tr>
<tr><td>GET</td><td><code>/api/external/reported-players?server=</code></td><td>被举报人统计</td></tr>
<tr><td>GET</td><td><code>/api/external/servers</code></td><td>服务器列表(含别名)</td></tr>
<tr><td>GET</td><td><code>/api/external/stats?server=</code></td><td>工单统计</td></tr>
<tr><td>POST</td><td><code>/api/external/clients</code></td><td>创建客户端(需登录态, 本页操作)</td></tr>
</tbody></table></div>
<p class="help" style="margin-top:8px">完整请求/响应示例见 <code>docs/API.md</code> 外部API章节。</p></div></div>
`;
} catch (e) { ct.innerHTML = `<div class="alert alert-e">${e.message}</div>`; }
},
async showCreate() {
U.modal('创建 API 客户端', `
<form id="apikey-form">
<div class="form-group"><label>名称 *</label><input id="ak-name" required placeholder="如QQ官方机器人"></div>
<div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">取消</button><button type="submit" class="btn btn-p">创建</button></div>
</form>
`);
document.getElementById('apikey-form').onsubmit = async e => {
e.preventDefault();
try {
const r = await API.post('/external/clients', { name: document.getElementById('ak-name').value });
App.closeModal();
U.modal('客户端已创建', `
<p>请立即保存以下凭据(secret 只显示一次):</p>
<div class="alert alert-i" style="word-break:break-all;font-family:monospace;font-size:12px">
<div>Client ID: <b>${U.esc(r.client_id)}</b></div>
<div style="margin-top:6px">Secret: <b>${U.esc(r.secret)}</b></div>
</div>
<div class="modal-f"><button type="button" class="btn btn-p" data-action="App:closeModal">我已保存</button></div>
`);
this.load();
} catch(ex){alert(ex.message);}
};
},
async toggle(id, active) {
try { await API.put(`/external/clients/${id}`, { active: !!active }); this.load(); } catch(ex){alert(ex.message);}
},
async del(id) {
if (!await U.confirm('确认删除该客户端?')) return;
try { await API.delete(`/external/clients/${id}`); this.load(); } catch(ex){alert(ex.message);}
}
};