/* * MC Report System * Copyright (C) 2026 Sea Network Technology Studio * Author: CangLan * * 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 . */ const express = require('express'); const bcrypt = require('bcryptjs'); const { query, getRow } = require('../db'); const { authenticate, requireRole } = require('../middleware/auth'); const router = express.Router(); 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;