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)
109 lines
7.7 KiB
JavaScript
109 lines
7.7 KiB
JavaScript
const BansPage = {
|
|
async render() { return '<div class="loading"><i class="fas fa-spinner"></i></div>'; },
|
|
|
|
async mount() {
|
|
document.getElementById('page-title').textContent = '封禁列表';
|
|
document.getElementById('page-actions').innerHTML = Auth.isOwner()
|
|
? '<button class="btn btn-p btn-sm" id="add-ban-btn"><i class="fas fa-plus"></i> 添加封禁</button>'
|
|
: '';
|
|
const btn = document.getElementById('add-ban-btn');
|
|
if (btn) btn.onclick = () => this.showAddBan();
|
|
await this.load();
|
|
},
|
|
|
|
async load() {
|
|
const ct = document.getElementById('page-content');
|
|
try {
|
|
const bans = await API.get('/bans/active');
|
|
const isStaff = Auth.isAdmin();
|
|
|
|
ct.innerHTML = bans.length === 0
|
|
? '<div class="empty"><i class="fas fa-ban"></i><p>暂无封禁记录</p></div>'
|
|
: `<div class="card table-wrap"><table><thead><tr><th>玩家</th><th>UID</th><th>帐号来源</th><th>处罚来源</th><th>类型</th><th>原因</th><th>时长</th><th>时间</th>${Auth.isOwner()?'<th>操作</th>':''}</tr></thead><tbody>${bans.map(b => `
|
|
<tr>
|
|
<td>${isStaff ? U.esc(b.player_name) : maskName(b.player_name)}</td>
|
|
<td class="ts">${isStaff ? U.esc(b.player_uid||'-') : maskUid(b.player_uid)}</td>
|
|
<td class="ts">${b.source==='netease'?'网易':b.source==='skin'?'皮肤站':'-'}</td>
|
|
<td class="ts">${b.ticket_id?'<span class="badge bg-processing">工单处理</span>':'<span class="badge bg-player">手动添加</span>'}</td>
|
|
<td>${b.type==='ban'?'<span class="badge bg-urgent">封禁</span>':b.type==='mute'?'<span class="badge bg-medium">禁言</span>':'<span class="badge bg-player">警告</span>'}</td>
|
|
<td class="ts">${U.esc((b.reason||'').substring(0,50))}</td>
|
|
<td>${b.duration||'永久'}</td>
|
|
<td>${b.status==='active'?'<span class="badge bg-rejected">生效中</span>':'<span class="badge bg-closed">已解除</span>'}</td>
|
|
<td class="ts">${U.date(b.created_at)}</td>
|
|
${Auth.isOwner()?`<td><button class="btn btn-o btn-sm" onclick="BansPage.edit(${b.id},'${U.escJs(b.player_name)}','${U.escJs(b.player_uid||'')}','${b.source||''}','${b.type}','${U.escJs(b.reason||'')}','${b.duration||''}','${b.status}')"><i class="fas fa-edit"></i></button> <button class="btn btn-d btn-sm" onclick="BansPage.lift(${b.id})"><i class="fas fa-check"></i> 解除</button></td>`:''}
|
|
</tr>`).join('')}</tbody></table></div>`;
|
|
} catch(e) { ct.innerHTML = `<div class="alert alert-e">${e.message}</div>`; }
|
|
},
|
|
|
|
showAddBan() {
|
|
U.modal('添加封禁', `
|
|
<form id="ban-form">
|
|
<div class="grid-2"><div class="form-group"><label>玩家名 *</label><input id="ban-name" required></div><div class="form-group"><label>UID</label><input id="ban-uid"></div></div>
|
|
<div class="grid-2"><div class="form-group"><label>来源</label><select id="ban-source"><option value="">未知</option></select></div><div class="form-group"><label>类型</label><select id="ban-type" onchange="BansPage.checkMinDuration()"><option value="ban">封禁</option><option value="mute">禁言</option><option value="warn">警告</option></select></div></div>
|
|
<div class="grid-2"><div class="form-group"><label>时长</label><select id="ban-dur"><option value="">永久</option><option value="1天">1天</option><option value="3天">3天</option><option value="7天">7天</option><option value="30天">30天</option></select></div></div>
|
|
<div class="form-group"><label>原因</label><textarea id="ban-reason" rows="2"></textarea></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>
|
|
`);
|
|
U.sources().then(list => {
|
|
const sel = document.getElementById('ban-source');
|
|
if (sel) sel.innerHTML = '<option value="">未知</option>' + list.map(s => `<option value="${U.esc(s.code)}">${U.esc(s.label)}</option>`).join('');
|
|
}).catch(()=>{});
|
|
document.getElementById('ban-form').onsubmit = async e => {
|
|
e.preventDefault();
|
|
try {
|
|
await API.post('/bans', { player_name: document.getElementById('ban-name').value, player_uid: document.getElementById('ban-uid').value, source: document.getElementById('ban-source').value, type: document.getElementById('ban-type').value, duration: document.getElementById('ban-dur').value, reason: document.getElementById('ban-reason').value });
|
|
App.closeModal(); this.load();
|
|
} catch(ex) { alert(ex.message); }
|
|
};
|
|
setTimeout(() => BansPage.checkMinDuration(), 100);
|
|
},
|
|
|
|
async checkMinDuration() {
|
|
const name = document.getElementById('ban-name')?.value;
|
|
const uid = document.getElementById('ban-uid')?.value;
|
|
const type = document.getElementById('ban-type')?.value;
|
|
if (!name && !uid) return;
|
|
try {
|
|
const r = await API.get('/bans/min-duration?player_name='+name+'&player_uid='+uid+'&type='+type);
|
|
const sel = document.getElementById('ban-dur');
|
|
if (r.minDays > 0) {
|
|
sel.innerHTML = `<option value="${r.minDays}天">≥ ${r.minDays}天</option><option value="30天">30天</option><option value="">永久</option>`;
|
|
} else {
|
|
sel.innerHTML = `<option value="">永久</option><option value="1天">1天</option><option value="3天">3天</option><option value="7天">7天</option><option value="30天">30天</option>`;
|
|
}
|
|
} catch {}
|
|
},
|
|
|
|
async lift(id) {
|
|
if (!await U.confirm('确认解除该封禁?')) return;
|
|
try { await API.put('/bans/'+id, { status: 'lifted' }); this.load(); } catch(ex){alert(ex.message);}
|
|
},
|
|
|
|
edit(id, name, uid, source, type, reason, duration, status) {
|
|
U.modal('编辑封禁', `
|
|
<form id="be-form">
|
|
<div class="grid-2"><div class="form-group"><label>玩家名</label><input id="be-name" value="${name}"></div><div class="form-group"><label>UID</label><input id="be-uid" value="${uid}"></div></div>
|
|
<div class="grid-2"><div class="form-group"><label>来源</label><select id="be-source"><option value="">未知</option></select></div><div class="form-group"><label>状态</label><select id="be-status"><option value="active" ${status==='active'?'selected':''}>生效中</option><option value="lifted" ${status==='lifted'?'selected':''}>已解除</option><option value="expired" ${status==='expired'?'selected':''}>已过期</option></select></div></div>
|
|
<div class="grid-2"><div class="form-group"><label>时长</label><input id="be-dur" value="${duration}"></div></div>
|
|
<div class="form-group"><label>原因</label><textarea id="be-reason" rows="2">${reason}</textarea></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>
|
|
`);
|
|
U.sources().then(list => {
|
|
const sel = document.getElementById('be-source');
|
|
if (sel) sel.innerHTML = '<option value="">未知</option>' + list.map(s => `<option value="${U.esc(s.code)}" ${s.code===source?'selected':''}>${U.esc(s.label)}</option>`).join('');
|
|
}).catch(()=>{});
|
|
document.getElementById('be-form').onsubmit = async e => {
|
|
e.preventDefault();
|
|
try {
|
|
await API.put('/bans/'+id, { status: document.getElementById('be-status').value, reason: document.getElementById('be-reason').value, duration: document.getElementById('be-dur').value });
|
|
App.closeModal(); this.load();
|
|
} catch(ex) { alert(ex.message); }
|
|
};
|
|
}
|
|
};
|
|
|
|
function maskName(n) { if (!n) return '***'; return n[0] + '***' + (n.length>2?n[n.length-1]:''); }
|
|
function maskUid(u) { if (!u) return '***'; return u.substring(0,3) + '****' + u.substring(u.length-3); }
|