- router.param('id'): all :id path params must be positive ints
(tickets/features/polls/auth/users; external/bans/notifications already
had parseInt - now consistent)
- notifications: type enum + webhook URL format + SSRF (isPrivateUrl
exported) + events whitelist + active boolean check on PUT
- bans: type enum + player_name length
- external: all-tickets type/status enums, bans status/type enums,
page/limit floor protection, ticket field length caps, clients active
boolean + id validation
- verified: 28 checks (syntax + validation coverage)
179 lines
10 KiB
JavaScript
179 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 { 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);
|
|
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 || '']);
|
|
try {
|
|
await query('INSERT INTO user_identities(user_id, source, game_name, game_uid) VALUES (?,?,?,?)',
|
|
[r.insertId, source || '', game_name, game_uid || '']);
|
|
} 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;
|