fix: site name in top logo + server-list public + external API docs page

- app.js: set #logo-text (top bar) from window.__SITE_NAME__ - the
  top-right site name stayed default because only #site-title and
  #sidebar-title were set
- security.js: apiKeyGuard whitelist /api/polls/server-list so the
  ticket create page can load sub-servers (was 401 without api key)
- external-api.js: management page now only creates/manages clients,
  removed inline endpoint table
- new external-api-docs.js: full API doc page (auth flow, all 11
  endpoints, request/response examples, Node sample) at
  #/external-api-docs, linked from the management page
- index.html: load external-api-docs.js
This commit is contained in:
2026-08-22 02:18:29 +08:00
parent c4363deb46
commit effc262382
5 changed files with 130 additions and 19 deletions

View File

@@ -154,7 +154,7 @@ const captchaLimiter = rateLimit({
function apiKeyGuard(req, res, next) { function apiKeyGuard(req, res, next) {
const p = req.originalUrl; const p = req.originalUrl;
if (p === '/api/health' || p.startsWith('/api/install')) return next(); if (p === '/api/health' || p.startsWith('/api/install') || p.startsWith('/api/polls/server-list')) return next();
const key = getApiKey(); const key = getApiKey();
if (!key) return next(); if (!key) return next();
if (!req.headers['x-api-key'] || req.headers['x-api-key'].length !== key.length) return res.status(401).json({ error: '无效的 API 密钥' }); if (!req.headers['x-api-key'] || req.headers['x-api-key'].length !== key.length) return res.status(401).json({ error: '无效的 API 密钥' });

View File

@@ -70,6 +70,7 @@
<script src="js/pages/templates.js"></script> <script src="js/pages/templates.js"></script>
<script src="js/pages/notifications.js"></script> <script src="js/pages/notifications.js"></script>
<script src="js/pages/external-api.js"></script> <script src="js/pages/external-api.js"></script>
<script src="js/pages/external-api-docs.js"></script>
<script src="js/pages/settings-page.js"></script> <script src="js/pages/settings-page.js"></script>
<script src="js/pages/export-page.js"></script> <script src="js/pages/export-page.js"></script>
<script src="js/pages/polls.js"></script> <script src="js/pages/polls.js"></script>

View File

@@ -49,6 +49,7 @@ const App = {
} }
if (window.__SITE_NAME__) { if (window.__SITE_NAME__) {
document.getElementById('site-title').textContent = window.__SITE_NAME__; document.getElementById('site-title').textContent = window.__SITE_NAME__;
document.getElementById('logo-text').textContent = window.__SITE_NAME__;
document.getElementById('sidebar-title').textContent = window.__SITE_NAME__ || '举报系统'; document.getElementById('sidebar-title').textContent = window.__SITE_NAME__ || '举报系统';
} }
this._footerLoaded = false; this._footerLoaded = false;
@@ -129,6 +130,7 @@ const App = {
case 'users': this.renderMain('用户管理', UsersPage, param); break; case 'users': this.renderMain('用户管理', UsersPage, param); break;
case 'notifications': this.renderMain('通知配置', NotificationsPage, param); break; case 'notifications': this.renderMain('通知配置', NotificationsPage, param); break;
case 'external-api': this.renderMain('外部API', ExternalApiPage, param); break; case 'external-api': this.renderMain('外部API', ExternalApiPage, param); break;
case 'external-api-docs': this.renderMain('外部API文档', ExternalApiDocsPage, param); break;
case 'sources': this.renderMain('来源管理', SourcesPage, param); break; case 'sources': this.renderMain('来源管理', SourcesPage, param); break;
case 'templates': this.renderMain('邮件模板', TemplatesPage, param); break; case 'templates': this.renderMain('邮件模板', TemplatesPage, param); break;
case 'settings': this.renderMain('系统设置', SettingsPage, param); break; case 'settings': this.renderMain('系统设置', SettingsPage, param); break;

View File

@@ -0,0 +1,122 @@
/*
* MC Report System
* Copyright (C) 2026 Sea Network Technology Studio
* Author: CangLan <admin@sea-studio.top>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* 外部API 全量文档页: 仅展示, 无数据请求 */
const ExternalApiDocsPage = {
async render() {
return `
<div style="display:flex;flex-direction:column;gap:16px;max-width:960px">
<div class="card"><div class="card-h"><i class="fas fa-key"></i> 鉴权流程</div><div class="card-b">
<ol style="padding-left:20px;line-height:2">
<li>在「外部API」页面创建客户端, 获得 <code>Client ID</code>(16位数字)与 <code>Secret</code>(32位随机字符串), Secret 仅创建时显示一次。</li>
<li>调 <code>POST /api/external/auth/session</code>, 请求头携带 <code>x-api-client-id</code> 与 <code>x-api-secret</code>, 换取 <code>SESSION</code>(24小时有效, 单会话, 重复换取使旧会话失效)。</li>
<li>之后所有接口请求头使用 <code>Authorization: Bearer &lt;SESSION&gt;</code>。除换取 SESSION 外, 所有接口均需 Bearer SESSION。</li>
</ol>
<p class="help">SESSION 有效期 24 小时; 客户端被停用后 SESSION 立即失效。所有响应为 JSON; 错误响应形如 <code>{"error":"..."}</code>。</p>
</div></div>
<div class="card"><div class="card-h"><i class="fas fa-table-list"></i> 接口一览</div><div class="card-b"><div class="table-wrap"><table><thead><tr><th>方法</th><th>路径</th><th>鉴权</th><th>说明</th></tr></thead><tbody>
<tr><td>POST</td><td><code>/api/external/auth/session</code></td><td>ID+Secret</td><td>换取 SESSION(唯一使用 ID+Secret 的接口)</td></tr>
<tr><td>POST</td><td><code>/api/external/auth/register</code></td><td>SESSION</td><td>插件代玩家注册账号(需邮箱)</td></tr>
<tr><td>POST</td><td><code>/api/external/tickets</code></td><td>SESSION</td><td>提交工单(举报/建议/申诉), 返回 <code>id</code> + <code>tracking_token</code>; 可带 <code>server</code>(别名或「分组/子服」)</td></tr>
<tr><td>GET</td><td><code>/api/external/tickets/track?token=xxx</code></td><td>SESSION</td><td>按追踪码查工单状态 + 回复, 从提交到结束全程可查</td></tr>
<tr><td>GET</td><td><code>/api/external/all-tickets?server=&amp;status=&amp;type=&amp;page=&amp;limit=</code></td><td>SESSION</td><td>工单列表(可按子服过滤, 分页)</td></tr>
<tr><td>GET</td><td><code>/api/external/all-tickets/:id</code></td><td>SESSION</td><td>工单详情(含回复/附件)</td></tr>
<tr><td>GET</td><td><code>/api/external/bans?server=&amp;status=&amp;player=</code></td><td>SESSION</td><td>拉取封禁列表</td></tr>
<tr><td>POST</td><td><code>/api/external/bans</code></td><td>SESSION</td><td>新增封禁(可带 server)</td></tr>
<tr><td>GET</td><td><code>/api/external/reported-players?server=</code></td><td>SESSION</td><td>被举报人统计</td></tr>
<tr><td>GET</td><td><code>/api/external/servers</code></td><td>SESSION</td><td>服务器列表(含别名)</td></tr>
<tr><td>GET</td><td><code>/api/external/stats?server=</code></td><td>SESSION</td><td>工单统计</td></tr>
</tbody></table></div></div></div>
<div class="card"><div class="card-h"><i class="fas fa-arrow-right-to-bracket"></i> 1. 换取 SESSION</div><div class="card-b">
<pre>POST /api/external/auth/session
Headers: x-api-client-id: 1234567890123456
x-api-secret: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6</pre>
<p>响应:</p>
<pre>{ "session_token": "xxxx...", "expires_in": 86400 }</pre>
</div></div>
<div class="card"><div class="card-h"><i class="fas fa-arrow-right-to-bracket"></i> 2. 插件注册账号(可选)</div><div class="card-b">
<pre>POST /api/external/auth/register
Authorization: Bearer &lt;SESSION&gt;
{ "username": "player1", "password": "123456", "email": "p1@example.com", "game_name": "Steve" }</pre>
<p>响应: <code>201 { "message": "注册成功!已自动激活,请登录" }</code>。注册后游戏UID由系统自动生成。</p>
</div></div>
<div class="card"><div class="card-h"><i class="fas fa-file-circle-plus"></i> 3. 提交工单</div><div class="card-b">
<pre>POST /api/external/tickets
Authorization: Bearer &lt;SESSION&gt;
{
"type": "report",
"title": "玩家破坏建筑",
"reporter_game_name": "Steve",
"target_game_name": "Alex",
"target_game_uid": "123456789",
"reason": "使用外挂",
"description": "在生存服主城附近...",
"server": "生存服" // 可选: 别名或「分组/子服」
}</pre>
<p>type 取值: <code>report</code>(举报) / <code>suggestion</code>(建议) / <code>appeal</code>(申诉)。</p>
<p>响应:</p>
<pre>201 { "id": 42, "tracking_token": "uuid-...", "server": "生存服" }</pre>
</div></div>
<div class="card"><div class="card-h"><i class="fas fa-magnifying-glass"></i> 4. 查询工单状态</div><div class="card-b">
<pre>GET /api/external/tickets/track?token=uuid-...</pre>
<p>响应(含全部回复):</p>
<pre>{ "id": 42, "status": "processing", "replies": [ { "content": "...", "is_staff": true, "created_at": "..." } ] }</pre>
</div></div>
<div class="card"><div class="card-h"><i class="fas fa-ban"></i> 5. 封禁同步</div><div class="card-b">
<pre>GET /api/external/bans?server=生存服&status=active
POST /api/external/bans
Authorization: Bearer &lt;SESSION&gt;
{ "player_name": "Alex", "player_uid": "123456789", "reason": "外挂", "type": "ban", "duration": "7d", "server": "生存服" }</pre>
</div></div>
<div class="card"><div class="card-h"><i class="fas fa-database"></i> 6. 其他数据接口</div><div class="card-b">
<pre>GET /api/external/all-tickets?server=生存服&status=pending&page=1&limit=20
GET /api/external/all-tickets/42
GET /api/external/reported-players?server=生存服
GET /api/external/servers
GET /api/external/stats?server=生存服</pre>
</div></div>
<div class="card"><div class="card-h"><i class="fas fa-bolt"></i> 插件接入示例(Node.js)</div><div class="card-b">
<pre>const API = 'https://你的站点/api/external';
// 1. 换取 SESSION
const s = await fetch(API + '/auth/session', { method: 'POST', headers: {
'x-api-client-id': '你的ClientID', 'x-api-secret': '你的Secret' } });
const { session_token } = await s.json();
// 2. 提交工单
await fetch(API + '/tickets', { method: 'POST', headers: {
'Authorization': 'Bearer ' + session_token, 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'report', title: '...', reporter_game_name: 'Steve', reason: '...' }) });</pre>
<p class="help">完整 Java(Bukkit/Paper) 示例见仓库 <code>docs/PLUGIN-GUIDE.md</code>。</p>
</div></div>
</div>`;
},
async mount() {
document.getElementById('page-title').textContent = '外部API文档';
document.getElementById('page-actions').innerHTML = '<button class="btn btn-o btn-sm" onclick="App.navigate(\'external-api\')"><i class="fas fa-arrow-left"></i> 返回客户端管理</button>';
}
};

View File

@@ -22,7 +22,9 @@ const ExternalApiPage = {
async mount() { async mount() {
document.getElementById('page-title').textContent = '外部API'; 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>'; document.getElementById('page-actions').innerHTML = `
<button class="btn btn-o btn-sm" onclick="App.navigate('external-api-docs')"><i class="fas fa-book"></i> 接口文档</button>
<button class="btn btn-p btn-sm" onclick="ExternalApiPage.showCreate()"><i class="fas fa-plus"></i> 创建客户端</button>`;
await this.load(); await this.load();
}, },
@@ -44,24 +46,8 @@ const ExternalApiPage = {
<button class="btn btn-d btn-sm" onclick="ExternalApiPage.del(${c.id})"><i class="fas fa-trash"></i></button> <button class="btn btn-d btn-sm" onclick="ExternalApiPage.del(${c.id})"><i class="fas fa-trash"></i></button>
</td> </td>
</tr>`).join('')}</tbody></table></div>`} </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 &lt;SESSION&gt;</code>。secret 仅创建时显示一次,请立即保存。</div> <div class="alert alert-i" style="margin-top:12px"><b>使用流程:</b> 创建客户端后, 用 <code>Client ID</code> + <code>Secret</code> <code>POST /api/external/auth/session</code> 换取 SESSION, 之后所有接口用 <code>Authorization: Bearer &lt;SESSION&gt;</code>。完整说明见右上角「接口文档」。</div>
</div></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>`; } } catch (e) { ct.innerHTML = `<div class="alert alert-e">${e.message}</div>`; }
}, },