refactor: session-based external auth, dynamic sources, drop netease UID

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)
This commit is contained in:
2026-08-19 20:08:06 +08:00
parent 6056153f57
commit 65edbaf157
19 changed files with 497 additions and 103 deletions

View File

@@ -30,6 +30,36 @@ const U = {
return String(s).replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/"/g,'"').replace(/\n/g,'\\n');
},
// ---- 动态来源(缓存, 注册/绑定身份/用户管理等处共用) ----
_sourcesCache: null,
async sources(force) {
if (force || !this._sourcesCache) {
try { this._sourcesCache = await API.get('/sources'); }
catch { this._sourcesCache = []; }
}
return this._sourcesCache;
},
// 生成下拉 HTML; includeEmpty 时带"不指定"选项
async sourcesOptions(selected, includeEmpty) {
const list = await this.sources();
const opts = (includeEmpty ? '<option value="">不指定</option>' : '') +
list.map(s => `<option value="${this.esc(s.code)}" ${s.code===selected?'selected':''}>${this.esc(s.label)}</option>`).join('');
return opts;
},
// code → 显示标签(找不到时显示原 code)
async sourceLabel(code) {
if (!code) return '-';
const list = await this.sources();
const hit = list.find(s => s.code === code);
return hit ? hit.label : code;
},
async sourceBadge(code) {
if (!code) return '-';
const list = await this.sources();
const hit = list.find(s => s.code === code);
return `<span class="badge bg-admin">${this.esc(hit ? hit.label : code)}</span>`;
},
showAlert(containerId, type, msg) {
const ct = document.getElementById(containerId || 'page-content');
if (!ct) return;