Auth (external API): - ID: 16-digit random (non-sequential); Secret: SeaReport- + 32 hex - POST /auth/session: ID+Secret -> Bearer SESSION (24h, single-session, old session invalidated on re-issue, disabled client invalidates) - clientAuth now validates Bearer SESSION via api_sessions JOIN api_clients Sources (dynamic, no default, open-source friendly): - sources table + CRUD route (/api/sources, owner; delete guarded by usage) - users/user_identities.source ENUM -> VARCHAR, seeded netease/skin - register/admin create/identity bind: validate against enabled sources - UI: 来源管理 page; source dropdowns loaded dynamically everywhere (register, dashboard identity, users admin, bans), labels dynamic UID removal: - game_uid/reporter_game_uid no longer required (db default '', validations dropped, frontend fields optional) Docs: EXTERNAL-API.md session flow + new credential format; API.md updated Verified: 37 checks (syntax, session logic, source CRUD, UID removal, docs)
84 lines
5.7 KiB
JavaScript
84 lines
5.7 KiB
JavaScript
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>POST /api/external/auth/session</code> 用 <code>x-api-client-id</code> + <code>x-api-secret</code> 换取 SESSION(24小时有效), 之后所有接口请求头用 <code>Authorization: Bearer <SESSION></code>。secret 仅创建时显示一次,请立即保存。</div>
|
||
</div></div>
|
||
|
||
<div class="card"><div class="card-h"><i class="fas fa-book"></i> 接口速览(除换取SESSION外, 均需 Bearer SESSION)</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/auth/session</code></td><td>ID+Secret 换取 SESSION(唯一使用 ID+Secret 的接口)</td></tr>
|
||
<tr><td>POST</td><td><code>/api/external/auth/register</code></td><td>插件注册账号(需 SESSION)</td></tr>
|
||
<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);}
|
||
}
|
||
};
|