feat: game UID fully internal + remove multi-identity copy
- game_uid is now an internal param: auto-generated server-side (U + 12 hex) on register/bind-identity/install/admin-create/external register; player-supplied game_uid ignored - Removed all player-facing UID inputs: register page, install page, identity bind modal, ticket-create 'your UID' field - Removed player-facing UID display: identity table column, ticket detail/tracking reporter UID - Removed '同一账号可绑定网易端 + 皮肤站身份' / '可绑定多个来源身份' copy - Kept target_game_uid (reported player) and admin panel UID management - settings-page skin-site placeholder updated
This commit is contained in:
@@ -118,13 +118,15 @@ router.post('/register', validateLengths({
|
||||
} catch {}
|
||||
|
||||
const hashed = await bcrypt.hash(password, 10);
|
||||
// 游戏UID为内置参数: 不接收玩家输入, 由系统自动生成
|
||||
const builtinUid = 'U' + uuid().replace(/-/g, '').slice(0, 12).toUpperCase();
|
||||
const r = await query(
|
||||
'INSERT INTO users(username, password, email, game_name, game_uid, active, email_verified, source) VALUES (?,?,?,?,?,?,?,?)',
|
||||
[username, hashed, email, game_name, game_uid||'', 1, 1, source || '']
|
||||
[username, hashed, email, game_name, builtinUid, 1, 1, source || '']
|
||||
);
|
||||
try {
|
||||
await query('INSERT INTO user_identities(user_id, source, game_name, game_uid) VALUES (?,?,?,?)',
|
||||
[r.insertId, source || '', game_name, game_uid || '']);
|
||||
[r.insertId, source || '', game_name, builtinUid]);
|
||||
} catch {}
|
||||
res.status(201).json({ message: '注册成功,请登录' });
|
||||
} catch (err) {
|
||||
@@ -233,7 +235,8 @@ router.post('/identities', authenticate, async (req, res) => {
|
||||
if (dupName) return res.status(400).json({ error: '该游戏名已绑定其他账号' });
|
||||
|
||||
const r = await query('INSERT INTO user_identities(user_id, source, game_name, game_uid) VALUES (?,?,?,?)',
|
||||
[req.user.id, source || '', game_name, game_uid || '']);
|
||||
// 游戏UID为内置参数: 不接收玩家输入, 由系统自动生成
|
||||
[req.user.id, source || '', game_name, 'U' + uuid().replace(/-/g, '').slice(0, 12).toUpperCase()]);
|
||||
res.status(201).json({ id: r.insertId, message: '绑定成功' });
|
||||
} catch (err) {
|
||||
console.error('[auth]', err);
|
||||
|
||||
@@ -166,14 +166,16 @@ router.post('/auth/register', async (req, res) => {
|
||||
|
||||
const hashed = bcrypt.hashSync(password, 10);
|
||||
const verifyToken = uuid();
|
||||
const r = await query('INSERT INTO users(username,password,email,game_name,game_uid,verify_token,verify_expires,source) VALUES (?,?,?,?,?,?,DATE_ADD(NOW(), INTERVAL 24 HOUR),?)', [username, hashed, email, game_name, game_uid || '', verifyToken, source || '']);
|
||||
// 游戏UID为内置参数: 不接收客户端输入, 由系统自动生成
|
||||
const builtinUid = 'U' + crypto.randomBytes(6).toString('hex').toUpperCase();
|
||||
const r = await query('INSERT INTO users(username,password,email,game_name,game_uid,verify_token,verify_expires,source) VALUES (?,?,?,?,?,?,DATE_ADD(NOW(), INTERVAL 24 HOUR),?)', [username, hashed, email, game_name, builtinUid, verifyToken, source || '']);
|
||||
try {
|
||||
await query('INSERT INTO user_identities(user_id, source, game_name, game_uid) VALUES (?,?,?,?)',
|
||||
[r.insertId, source || '', game_name, game_uid || '']);
|
||||
[r.insertId, source || '', game_name, builtinUid]);
|
||||
} catch {}
|
||||
|
||||
const site = await getRow("SELECT v FROM settings WHERE k='site_url'");
|
||||
const sent = await sendEmail(email, 'verify_email', { username, game_name, game_uid, verify_link: `${site?.v||'http://localhost:3100'}#/verify?token=${verifyToken}` });
|
||||
const sent = await sendEmail(email, 'verify_email', { username, game_name, game_uid: builtinUid, verify_link: `${site?.v||'http://localhost:3100'}#/verify?token=${verifyToken}` });
|
||||
|
||||
if (!sent) {
|
||||
await query('UPDATE users SET email_verified=1,active=1,verify_token=NULL WHERE username=?', [username]);
|
||||
|
||||
@@ -85,9 +85,11 @@ router.post('/complete', async (req, res) => {
|
||||
await initSchema(conn);
|
||||
|
||||
const hashed = bcrypt.hashSync(admin_password, 10);
|
||||
// 游戏UID为内置参数: 不接收输入, 由系统自动生成
|
||||
const builtinUid = 'U' + crypto.randomBytes(6).toString('hex').toUpperCase();
|
||||
await conn.query(
|
||||
'INSERT INTO users(username, password, email, game_name, game_uid, role, active, email_verified) VALUES (?,?,?,?,?,?,1,1)',
|
||||
[admin_username, hashed, admin_email, admin_game_name || admin_username, admin_game_uid || 'OWNER001', 'owner']
|
||||
[admin_username, hashed, admin_email, admin_game_name || admin_username, builtinUid, 'owner']
|
||||
);
|
||||
|
||||
const jwtSecret = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const crypto = require('crypto');
|
||||
const { query, getRow } = require('../db');
|
||||
const { authenticate, requireRole } = require('../middleware/auth');
|
||||
|
||||
@@ -115,10 +116,12 @@ router.post('/', authenticate, requireRole('owner','admin'), async (req, res) =>
|
||||
if (await getRow('SELECT id FROM users WHERE username = ?', [username])) return res.status(400).json({ error: '用户名已存在' });
|
||||
if (await getRow('SELECT id FROM users WHERE email = ?', [email])) return res.status(400).json({ error: '邮箱已被使用' });
|
||||
const hashed = bcrypt.hashSync(password, 10);
|
||||
const r = await query('INSERT INTO users(username, password, email, game_name, game_uid, role, source, active, email_verified) VALUES (?,?,?,?,?,?,?,1,1)', [username, hashed, email, game_name, game_uid||'', role, source || '']);
|
||||
// 游戏UID为内置参数: 管理员未指定时自动生成
|
||||
const builtinUid = game_uid || ('U' + crypto.randomBytes(6).toString('hex').toUpperCase());
|
||||
const r = await query('INSERT INTO users(username, password, email, game_name, game_uid, role, source, active, email_verified) VALUES (?,?,?,?,?,?,?,1,1)', [username, hashed, email, game_name, builtinUid, role, source || '']);
|
||||
try {
|
||||
await query('INSERT INTO user_identities(user_id, source, game_name, game_uid) VALUES (?,?,?,?)',
|
||||
[r.insertId, source || '', game_name, game_uid || '']);
|
||||
[r.insertId, source || '', game_name, builtinUid]);
|
||||
} catch {}
|
||||
await query("INSERT INTO audit_logs(user_id, username, action, entity_type, entity_id, details) VALUES (?,?,?,?,?,?)", [req.user.id, req.user.username, 'create_user', 'user', r.insertId, `创建用户 ${username}`]);
|
||||
res.status(201).json({ id: r.insertId, message: '创建成功' });
|
||||
|
||||
@@ -68,13 +68,12 @@ const Dashboard = {
|
||||
|
||||
<div class="card"><div class="card-h"><i class="fas fa-id-card"></i> 我的身份 ${idents.length < 2 ? `<button class="btn btn-p btn-sm" onclick="Dashboard.showAddIdentity()">绑定${idents.length === 1 ? '另一来源' : '身份'}</button>` : ''}</div>
|
||||
<div class="card-b">
|
||||
${idents.length === 0 ? '<span class="tm">暂无身份,点击右上角绑定</span>' : `<table><thead><tr><th>来源</th><th>游戏名</th><th>UID</th><th>绑定时间</th><th>操作</th></tr></thead><tbody>${idents.map(it=>`<tr>
|
||||
${idents.length === 0 ? '<span class="tm">暂无身份,点击右上角绑定</span>' : `<table><thead><tr><th>来源</th><th>游戏名</th><th>绑定时间</th><th>操作</th></tr></thead><tbody>${idents.map(it=>`<tr>
|
||||
<td><span class="badge bg-admin" data-src="${U.esc(it.source)}">${U.esc(it.source||'-')}</span></td>
|
||||
<td>${U.esc(it.game_name)}</td><td class="ts">${U.esc(it.game_uid||'-')}</td>
|
||||
<td>${U.esc(it.game_name)}</td>
|
||||
<td class="ts">${U.date(it.created_at)}</td>
|
||||
<td>${idents.length > 1 ? `<button class="btn btn-d btn-sm" onclick="Dashboard.removeIdentity(${it.id})"><i class="fas fa-unlink"></i> 解绑</button>` : '<span class="tm ts">主身份</span>'}</td>
|
||||
</tr>`).join('')}</tbody></table>`}
|
||||
<p class="tm ts mt-4"><i class="fas fa-info-circle"></i> 同一账号可绑定多个来源身份,提交工单时选择使用哪个身份。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -123,15 +122,6 @@ const Dashboard = {
|
||||
<form id="ident-form">
|
||||
<div class="form-group"><label>来源</label><select id="ident-source"><option value="">加载中...</option></select></div>
|
||||
<div class="form-group"><label>游戏名 *</label><input id="ident-name" required placeholder="Minecraft ID"></div>
|
||||
<div class="form-group" id="ident-uid-group" style="display:none"><label>UID(可选)</label><input id="ident-uid" placeholder="选填"></div>
|
||||
<div class="form-group hidden" id="ident-skin-group">
|
||||
<label>皮肤站地址</label>
|
||||
<div style="display:flex;gap:6px">
|
||||
<input id="ident-site" placeholder="https://littleskin.cn" style="flex:1">
|
||||
<button type="button" class="btn btn-o btn-sm" id="ident-fetch-btn" style="flex:0 0 auto;white-space:nowrap"><i class="fas fa-magnifying-glass"></i> 获取UUID</button>
|
||||
</div>
|
||||
<span class="help">输入皮肤站用户名后点「获取UUID」,自动填入下方UID</span>
|
||||
</div>
|
||||
<div id="ident-err" class="alert alert-e hidden"></div>
|
||||
<div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="submit" class="btn btn-p">绑定</button></div>
|
||||
</form>
|
||||
@@ -142,44 +132,13 @@ const Dashboard = {
|
||||
sel.innerHTML = list.length
|
||||
? list.map(s => `<option value="${U.esc(s.code)}">${U.esc(s.label)}</option>`).join('')
|
||||
: '<option value="">不指定</option>';
|
||||
toggleSource();
|
||||
}).catch(()=>{});
|
||||
const toggleSource = () => {
|
||||
const src = document.getElementById('ident-source').value;
|
||||
const isSkin = src === 'skin';
|
||||
document.getElementById('ident-uid-group').style.display = isSkin ? 'none' : '';
|
||||
document.getElementById('ident-skin-group').classList.toggle('hidden', !isSkin);
|
||||
};
|
||||
document.getElementById('ident-source').onchange = toggleSource;
|
||||
// 自动带出站点配置的皮肤站地址
|
||||
API.get('/settings/skin-site').then(r => {
|
||||
if (r.site && !document.getElementById('ident-site').value) document.getElementById('ident-site').value = r.site;
|
||||
}).catch(()=>{});
|
||||
document.getElementById('ident-fetch-btn').onclick = async () => {
|
||||
const site = document.getElementById('ident-site').value.trim();
|
||||
const name = document.getElementById('ident-name').value.trim();
|
||||
if (!site) return alert('请填写皮肤站地址');
|
||||
if (!name) return alert('请先填写游戏名(皮肤站用户名)');
|
||||
const btn = document.getElementById('ident-fetch-btn');
|
||||
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 查询中';
|
||||
try {
|
||||
const r = await API.get(`/settings/uuid-lookup?site=${encodeURIComponent(site)}&name=${encodeURIComponent(name)}`);
|
||||
if (r.found) {
|
||||
document.getElementById('ident-uid').value = r.uuid;
|
||||
alert('已获取 UUID: ' + r.uuid);
|
||||
} else {
|
||||
alert(r.message || '未找到该用户名');
|
||||
}
|
||||
} catch (ex) { alert(ex.message); }
|
||||
btn.disabled = false; btn.innerHTML = '<i class="fas fa-magnifying-glass"></i> 获取UUID';
|
||||
};
|
||||
document.getElementById('ident-form').onsubmit = async e => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await API.post('/auth/identities', {
|
||||
source: document.getElementById('ident-source').value,
|
||||
game_name: document.getElementById('ident-name').value,
|
||||
game_uid: document.getElementById('ident-uid').value,
|
||||
});
|
||||
App.closeModal();
|
||||
this.mount();
|
||||
|
||||
@@ -65,7 +65,7 @@ const HomePage = {
|
||||
API.get(`/tickets/tracking?token=${token}`).then(tickets => {
|
||||
const t = tickets.find(x=>x.id===id);
|
||||
if(!t) return;
|
||||
document.getElementById('tkd').innerHTML = `<div class="detail-section"><h4>基本信息</h4><div class="detail-row"><span class="lbl">标题</span><span class="val fb">${U.esc(t.title)}</span></div><div class="detail-row"><span class="lbl">类型</span><span class="val">${U.badge(t.type,'type')}</span></div><div class="detail-row"><span class="lbl">状态</span><span class="val">${U.badge(t.status,'status')}</span></div><div class="detail-row"><span class="lbl">提交者</span><span class="val">${U.esc(t.reporter_game_name)} (UID: ${U.esc(t.reporter_game_uid)})</span></div>${t.description?`<div class="detail-row"><span class="lbl">详情</span><span class="val">${U.esc(t.description)}</span></div>`:''}</div>
|
||||
document.getElementById('tkd').innerHTML = `<div class="detail-section"><h4>基本信息</h4><div class="detail-row"><span class="lbl">标题</span><span class="val fb">${U.esc(t.title)}</span></div><div class="detail-row"><span class="lbl">类型</span><span class="val">${U.badge(t.type,'type')}</span></div><div class="detail-row"><span class="lbl">状态</span><span class="val">${U.badge(t.status,'status')}</span></div><div class="detail-row"><span class="lbl">提交者</span><span class="val">${U.esc(t.reporter_game_name)}</span></div>${t.description?`<div class="detail-row"><span class="lbl">详情</span><span class="val">${U.esc(t.description)}</span></div>`:''}</div>
|
||||
<form id="anon-reply-form" class="mt-4"><div class="form-group"><textarea id="anon-reply-content" rows="3" placeholder="追加补充信息..."></textarea></div><button type="submit" class="btn btn-p btn-sm"><i class="fas fa-reply"></i> 补充</button></form>`;
|
||||
document.getElementById('anon-reply-form').onsubmit = async e => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -91,7 +91,6 @@ const InstallPage = {
|
||||
<div class="form-group"><label>邮箱 *</label><input type="email" id="ia-email" required></div></div>
|
||||
<div class="grid-2"><div class="form-group"><label>密码 *</label><input type="password" id="ia-password" required minlength="6"></div>
|
||||
<div class="form-group"><label>游戏名称</label><input id="ia-game" placeholder="Minecraft ID"></div></div>
|
||||
<div class="form-group"><label>游戏UID</label><input id="ia-uid" placeholder="游戏唯一ID"></div>
|
||||
<div id="ia-err" class="alert alert-e hidden"></div>
|
||||
<div id="ia-warn" class="alert alert-i"><strong>注意:</strong>安装完成后需要重启服务器才能加载业务路由。请记好账号密码。</div>
|
||||
<button type="submit" class="btn btn-p btn-lg w-full"><i class="fas fa-check"></i> 完成安装</button>
|
||||
@@ -114,7 +113,6 @@ const InstallPage = {
|
||||
admin_password: document.getElementById('ia-password').value,
|
||||
admin_email: document.getElementById('ia-email').value,
|
||||
admin_game_name: document.getElementById('ia-game').value,
|
||||
admin_game_uid: document.getElementById('ia-uid').value,
|
||||
});
|
||||
document.getElementById('install-admin').innerHTML = '<div class="alert alert-s"><h3><i class="fas fa-check-circle"></i> 安装成功!</h3><p>系统已就绪,正在刷新并跳转登录...</p></div>';
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
|
||||
@@ -49,8 +49,6 @@ const RegisterPage = {
|
||||
<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" style="display:none"><label id="rs-label2">UID(可选)</label><input id="rgu" placeholder="选填"></div>
|
||||
|
||||
${this.captcha ? `
|
||||
<div class="form-group"><label>人机验证</label>
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
@@ -85,10 +83,6 @@ const RegisterPage = {
|
||||
toggleSource() {
|
||||
const src = document.getElementById('rs').value;
|
||||
document.getElementById('rs-label1').textContent = '游戏名称';
|
||||
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() {
|
||||
@@ -124,7 +118,7 @@ const RegisterPage = {
|
||||
await Auth.register({
|
||||
username: document.getElementById('ru').value.trim(), password: document.getElementById('rp').value,
|
||||
email: document.getElementById('re').value.trim(), game_name: document.getElementById('rgn').value.trim(),
|
||||
game_uid: document.getElementById('rgu').value.trim(), source: document.getElementById('rs').value,
|
||||
source: document.getElementById('rs').value,
|
||||
captcha_id: document.getElementById('captcha-id')?.value,
|
||||
captcha_answer: document.getElementById('captcha-a')?.value.trim(),
|
||||
code_id: this.codeId, code: document.getElementById('vf-code').value.trim(),
|
||||
|
||||
@@ -33,7 +33,7 @@ const SettingsPage = {
|
||||
<div class="form-group"><label>站点地址</label><input id="s-url" value="${U.esc(settings.site_url?.value||'')}"></div></div>
|
||||
<div class="grid-2"><div class="form-group"><label>页尾版权</label><input id="s-copyright" value="${U.esc(settings.copyright?.value||'')}" placeholder="© 2024 MC举报系统"></div>
|
||||
<div class="form-group"><label>ICP备案号</label><input id="s-icp" value="${U.esc(settings.icp?.value||'')}" placeholder="沪ICP备XXXXXXXX号"></div></div>
|
||||
<div class="form-group"><label>皮肤站地址</label><input id="s-skin-site" value="${U.esc(settings.skin_site?.value||'')}" placeholder="https://littleskin.cn(用于绑定皮肤站身份时自动查询UUID)"></div>
|
||||
<div class="form-group"><label>皮肤站地址</label><input id="s-skin-site" value="${U.esc(settings.skin_site?.value||'')}" placeholder="https://littleskin.cn"></div>
|
||||
<button type="submit" class="btn btn-p btn-sm"><i class="fas fa-save"></i> 保存站点设置</button>
|
||||
</form>
|
||||
</div></div>
|
||||
|
||||
@@ -35,7 +35,7 @@ const TicketCreate = {
|
||||
this.identities = list || [];
|
||||
const sel = container.querySelector('#identity-select');
|
||||
if (sel && this.identities.length > 1) {
|
||||
sel.innerHTML = this.identities.map((it, i) => `<option value="${i}">${U.esc(it.source)} — ${U.esc(it.game_name)}${it.game_uid ? ' (UID: ' + U.esc(it.game_uid) + ')' : ''}</option>`).join('');
|
||||
sel.innerHTML = this.identities.map((it, i) => `<option value="${i}">${U.esc(it.source)} — ${U.esc(it.game_name)}</option>`).join('');
|
||||
sel.closest('.form-group').classList.remove('hidden');
|
||||
}
|
||||
}).catch(() => {});
|
||||
@@ -68,18 +68,11 @@ const TicketCreate = {
|
||||
${user ? `<div class="form-group hidden" id="identity-group">
|
||||
<label>使用身份</label>
|
||||
<select id="identity-select"></select>
|
||||
<span class="help">同一账号可绑定网易端 + 皮肤站身份</span>
|
||||
</div>` : ''}
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label>您的游戏名称 *</label>
|
||||
<input name="reporter_game_name" required placeholder="Minecraft ID" value="${U.esc(prefill?.game_name || user?.game_name || '')}" ${prefill?.game_name ? 'readonly' : ''}>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>您的游戏UID (可选)</label>
|
||||
<input name="reporter_game_uid" placeholder="游戏UID(选填)" value="${U.esc(prefill?.game_uid || user?.game_uid || '')}" ${prefill?.game_uid ? 'readonly' : ''}>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>您的游戏名称 *</label>
|
||||
<input name="reporter_game_name" required placeholder="Minecraft ID" value="${U.esc(prefill?.game_name || user?.game_name || '')}" ${prefill?.game_name ? 'readonly' : ''}>
|
||||
</div>
|
||||
|
||||
${isReport ? `
|
||||
|
||||
@@ -42,7 +42,7 @@ const TicketDetail = {
|
||||
<div class="detail-grid"><div>
|
||||
<div class="card"><div class="card-h"><span>基本信息</span><div>${U.badge(t.status,'status')} ${U.badge(t.type,'type')} ${U.badge(t.priority,'priority')}</div></div><div class="card-b">
|
||||
<div class="detail-row"><span class="lbl">标题</span><span class="val fb">${U.esc(t.title)}</span></div>
|
||||
<div class="detail-row"><span class="lbl">提交者</span><span class="val">${U.esc(t.reporter_game_name)} (UID: ${U.esc(t.reporter_game_uid)})</span></div>
|
||||
<div class="detail-row"><span class="lbl">提交者</span><span class="val">${U.esc(t.reporter_game_name)}</span></div>
|
||||
${isRep?`<div class="detail-row"><span class="lbl">被举报人</span><span class="val">${U.esc(t.target_game_name||'?')} (UID: ${U.esc(t.target_game_uid||'?')})</span></div>
|
||||
<div class="detail-row"><span class="lbl">原因</span><span class="val">${U.esc(t.reason)}</span></div>`:''}
|
||||
${isAppeal?`<div class="detail-row"><span class="lbl">申诉理由</span><span class="val">${U.esc(t.reason)}</span></div>`:''}
|
||||
|
||||
Reference in New Issue
Block a user