Files
MC_Report/backend/routes/users.js
canglan cb5847d650 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
2026-08-21 21:52:40 +08:00

182 lines
10 KiB
JavaScript

/*
* 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/>.
*/
const express = require('express');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const { query, getRow } = require('../db');
const { authenticate, requireRole } = require('../middleware/auth');
const router = express.Router();
// 统一校验 :id 路径参数(必须是正整数)
router.param('id', (req, res, next, id) => {
if (!/^\d+$/.test(id)) return res.status(400).json({ error: '无效的ID' });
next();
});
router.get('/', authenticate, requireRole('owner','admin'), async (req, res) => {
const rows = await query(`SELECT u.id, u.username, u.email, u.game_name, u.game_uid, u.source, u.role, u.active, u.email_verified, u.created_at,
(SELECT COUNT(*) FROM user_identities ui WHERE ui.user_id = u.id) as identity_count
FROM users u ORDER BY u.created_at DESC`);
res.json(rows);
});
router.get('/:id', authenticate, requireRole('owner','admin'), async (req, res) => {
const u = await getRow('SELECT id, username, email, game_name, game_uid, source, role, active, email_verified, created_at FROM users WHERE id = ?', [req.params.id]);
if (!u) return res.status(404).json({ error: '用户不存在' });
u.servers = await query('SELECT group_name, server_name FROM user_servers WHERE user_id = ?', [req.params.id]);
u.identities = await query('SELECT id, source, game_name, game_uid, created_at FROM user_identities WHERE user_id = ? ORDER BY id', [req.params.id]);
res.json(u);
});
router.get('/:id/servers', authenticate, requireRole('owner','admin'), async (req, res) => {
res.json(await query('SELECT group_name, server_name FROM user_servers WHERE user_id = ?', [req.params.id]));
});
// ---- 管理员: 为用户绑定额外来源身份(多身份管理) ----
router.post('/:id/identities', authenticate, requireRole('owner','admin'), async (req, res) => {
try {
const uid = parseInt(req.params.id);
const u = await getRow('SELECT id, username, role FROM users WHERE id = ?', [uid]);
if (!u) return res.status(404).json({ error: '用户不存在' });
if (u.role === 'owner' && req.user.role !== 'owner') return res.status(403).json({ error: '无权修改服主' });
const { source, game_name, game_uid } = req.body;
if (!game_name) return res.status(400).json({ error: '游戏名不能为空' });
// 来源动态校验
if (source) {
try {
const srcRow = await getRow('SELECT id FROM sources WHERE code = ? AND enabled = 1', [source]);
if (!srcRow) return res.status(400).json({ error: '无效的来源' });
} catch {}
}
const dup = await getRow('SELECT id FROM user_identities WHERE user_id = ? AND source = ?', [uid, source || '']);
if (dup) return res.status(400).json({ error: '该来源已绑定,可先删除再重新绑定' });
const dupName = await getRow('SELECT id FROM user_identities WHERE source = ? AND game_name = ?', [source || '', game_name]);
if (dupName) return res.status(400).json({ error: '该游戏名已绑定其他账号' });
const r = await query('INSERT INTO user_identities(user_id, source, game_name, game_uid) VALUES (?,?,?,?)',
[uid, source || '', game_name, game_uid || '']);
await query("INSERT INTO audit_logs(user_id, username, action, entity_type, entity_id, details) VALUES (?,?,?,?,?,?)",
[req.user.id, req.user.username, 'add_identity', 'user', uid, `${u.username || uid} 绑定 ${source} 身份 ${game_name}`]);
res.status(201).json({ id: r.insertId, message: '绑定成功' });
} catch (err) {
console.error('[users]', err);
res.status(500).json({ error: '服务器内部错误' });
}
});
router.delete('/:id/identities/:identityId', authenticate, requireRole('owner','admin'), async (req, res) => {
try {
const uid = parseInt(req.params.id);
const iid = parseInt(req.params.identityId);
const u = await getRow('SELECT id, username, role FROM users WHERE id = ?', [uid]);
if (!u) return res.status(404).json({ error: '用户不存在' });
if (u.role === 'owner' && req.user.role !== 'owner') return res.status(403).json({ error: '无权修改服主' });
const ident = await getRow('SELECT * FROM user_identities WHERE id = ? AND user_id = ?', [iid, uid]);
if (!ident) return res.status(404).json({ error: '身份不存在' });
const count = await getRow('SELECT COUNT(*) as c FROM user_identities WHERE user_id = ?', [uid]);
if (count.c <= 1) return res.status(400).json({ error: '至少保留一个身份' });
await query('DELETE FROM user_identities WHERE id = ?', [iid]);
await query("INSERT INTO audit_logs(user_id, username, action, entity_type, entity_id, details) VALUES (?,?,?,?,?,?)",
[req.user.id, req.user.username, 'remove_identity', 'user', uid, `解绑 ${ident.source} 身份 ${ident.game_name}`]);
res.json({ message: '已解绑' });
} catch (err) {
console.error('[users]', err);
res.status(500).json({ error: '服务器内部错误' });
}
});
router.post('/', authenticate, requireRole('owner','admin'), async (req, res) => {
const { username, password, email, game_name, game_uid, role, source } = req.body;
if (!username||!password||!email||!game_name||!role) return res.status(400).json({ error: '所有字段必填' });
if (!['owner','admin','player'].includes(role)) return res.status(400).json({ error: '无效角色' });
if (role === 'owner' && req.user.role !== 'owner') return res.status(403).json({ error: '仅服主可创建服主账号' });
if (password.length < 6) return res.status(400).json({ error: '密码至少6位' });
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);
// 游戏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, 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: '创建成功' });
});
router.put('/:id', authenticate, requireRole('owner','admin'), async (req, res) => {
const u = await getRow('SELECT * FROM users WHERE id = ?', [req.params.id]);
if (!u) return res.status(404).json({ error: '用户不存在' });
if (u.role === 'owner' && req.user.role !== 'owner') return res.status(403).json({ error: '无权修改服主' });
const fields = {};
if (req.body.email) fields.email = req.body.email;
if (req.body.game_name) fields.game_name = req.body.game_name;
if (req.body.game_uid) fields.game_uid = req.body.game_uid;
if (req.body.source) {
// 动态来源校验
let srcOk = false;
try {
const srcRow = await getRow('SELECT id FROM sources WHERE code = ? AND enabled = 1', [req.body.source]);
srcOk = !!srcRow;
} catch { srcOk = true; }
if (!srcOk) return res.status(400).json({ error: '无效来源' });
fields.source = req.body.source;
}
if (req.body.role) { if (!['owner','admin','player'].includes(req.body.role)) return res.status(400).json({ error: '无效角色' }); fields.role = req.body.role; }
if (req.body.active !== undefined) fields.active = req.body.active ? 1 : 0;
if (req.body.password) { if (req.body.password.length < 6) return res.status(400).json({ error: '密码至少6位' }); fields.password = bcrypt.hashSync(req.body.password, 10); }
if (!Object.keys(fields).length) return res.status(400).json({ error: '无更新内容' });
const sets = Object.keys(fields).map(k => `${k} = ?`).join(', ');
await query(`UPDATE users SET ${sets} WHERE id = ?`, [...Object.values(fields), req.params.id]);
// 主身份字段变化时同步 user_identities 中对应来源
if (fields.game_name !== undefined || fields.source !== undefined || fields.game_uid !== undefined) {
const cur = await getRow('SELECT source, game_name, game_uid FROM users WHERE id = ?', [req.params.id]);
if (cur?.game_name) {
try {
await query('INSERT INTO user_identities(user_id, source, game_name, game_uid) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE game_name = VALUES(game_name), game_uid = VALUES(game_uid)',
[req.params.id, cur.source || '', cur.game_name, cur.game_uid || '']);
} catch {}
}
}
if (req.body.admin_servers !== undefined) {
await query('DELETE FROM user_servers WHERE user_id = ?', [req.params.id]);
if (Array.isArray(req.body.admin_servers)) {
for (const s of req.body.admin_servers) {
if (s.group_name && s.server_name) {
await query('INSERT IGNORE INTO user_servers(user_id, group_name, server_name) VALUES (?,?,?)', [req.params.id, s.group_name, s.server_name]);
}
}
}
}
await query("INSERT INTO audit_logs(user_id, username, action, entity_type, entity_id, details) VALUES (?,?,?,?,?,?)", [req.user.id, req.user.username, 'update_user', 'user', req.params.id, '更新用户']);
res.json({ message: '更新成功' });
});
module.exports = router;