feat: admin user management supports multi-identity bind/unbind
- users list: identity_count column (backend subquery) - user detail: returns identities array - POST /users/:id/identities, DELETE /users/:id/identities/:identityId (owner protection, dup checks, keep >=1, audit logs) - users page: identity column in table + manage section in edit modal - API docs updated
This commit is contained in:
@@ -6,13 +6,17 @@ const { authenticate, requireRole } = require('../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', authenticate, requireRole('owner','admin'), async (req, res) => {
|
||||
res.json(await query('SELECT id, username, email, game_name, game_uid, source, role, active, email_verified, created_at FROM users ORDER BY created_at DESC'));
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -20,6 +24,58 @@ router.get('/:id/servers', authenticate, requireRole('owner','admin'), async (re
|
||||
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 (!['netease','skin'].includes(source)) return res.status(400).json({ error: '无效的来源' });
|
||||
if (!game_name) return res.status(400).json({ error: '游戏名不能为空' });
|
||||
if (source === 'netease' && !game_uid) return res.status(400).json({ error: '网易端必须填写UID' });
|
||||
|
||||
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||!game_uid||!role) return res.status(400).json({ error: '所有字段必填' });
|
||||
|
||||
Reference in New Issue
Block a user