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

@@ -2,6 +2,17 @@ const RegisterPage = {
captcha: null,
codeId: null,
async mount() {
// 动态加载来源下拉(不设默认来源)
U.sources().then(list => {
const sel = document.getElementById('rs');
if (sel) {
sel.innerHTML = '<option value="">不指定</option>' + list.map(s => `<option value="${U.esc(s.code)}">${U.esc(s.label)}</option>`).join('');
this.toggleSource();
}
}).catch(()=>{});
},
async render() {
try { this.captcha = await API.get('/captcha'); } catch { this.captcha = null; }
return `
@@ -15,11 +26,11 @@ const RegisterPage = {
</div>
<div class="grid-2">
<div class="form-group"><label>帐号来源</label><select id="rs" onchange="RegisterPage.toggleSource()"><option value="netease">网易端</option><option value="skin">皮肤站</option></select></div>
<div class="form-group"><label>帐号来源</label><select id="rs" onchange="RegisterPage.toggleSource()"><option value="">加载中...</option></select></div>
<div class="form-group"><label id="rs-label1">游戏名称</label><input id="rgn" required placeholder="Minecraft ID"></div>
</div>
<div class="form-group" id="uid-group"><label id="rs-label2">网易UID</label><input id="rgu" placeholder="仅网易端必填"></div>
<div class="form-group" id="uid-group" style="display:none"><label id="rs-label2">UID(可选)</label><input id="rgu" placeholder="填"></div>
${this.captcha ? `
<div class="form-group"><label>人机验证</label>
@@ -55,9 +66,10 @@ const RegisterPage = {
toggleSource() {
const src = document.getElementById('rs').value;
document.getElementById('rs-label1').textContent = '游戏名称';
document.getElementById('rs-label2').textContent = src === 'netease' ? '网易UID' : '皮肤站UUID(自动获取)';
document.getElementById('uid-group').style.display = src === 'netease' ? '' : 'none';
document.getElementById('rgu').required = src === 'netease';
const isSkin = src === 'skin';
document.getElementById('rs-label2').textContent = isSkin ? '皮肤站UUID(可选)' : 'UID(可选)';
document.getElementById('uid-group').style.display = isSkin ? 'none' : '';
document.getElementById('rgu').required = false;
},
async refreshCaptcha() {