refactor: session-based external auth, dynamic sources, drop netease UID
Auth (external API): - ID: 16-digit random (non-sequential); Secret: SeaReport- + 32 hex - POST /auth/session: ID+Secret -> Bearer SESSION (24h, single-session, old session invalidated on re-issue, disabled client invalidates) - clientAuth now validates Bearer SESSION via api_sessions JOIN api_clients Sources (dynamic, no default, open-source friendly): - sources table + CRUD route (/api/sources, owner; delete guarded by usage) - users/user_identities.source ENUM -> VARCHAR, seeded netease/skin - register/admin create/identity bind: validate against enabled sources - UI: 来源管理 page; source dropdowns loaded dynamically everywhere (register, dashboard identity, users admin, bans), labels dynamic UID removal: - game_uid/reporter_game_uid no longer required (db default '', validations dropped, frontend fields optional) Docs: EXTERNAL-API.md session flow + new credential format; API.md updated Verified: 37 checks (syntax, session logic, source CRUD, UID removal, docs)
This commit is contained in:
@@ -46,7 +46,15 @@ router.post('/register', validateLengths({
|
||||
if (!username || !password || !email || !game_name) {
|
||||
return res.status(400).json({ error: '所有字段均为必填' });
|
||||
}
|
||||
if (source === 'netease' && !game_uid) return res.status(400).json({ error: '网易端必须填写UID' });
|
||||
// 来源必须是已启用的动态来源(不设默认)
|
||||
let srcValid = false;
|
||||
if (source) {
|
||||
try {
|
||||
const srcRow = await getRow('SELECT id FROM sources WHERE code = ? AND enabled = 1', [source]);
|
||||
srcValid = !!srcRow;
|
||||
} catch { srcValid = true; } // sources 表不存在时兼容旧部署
|
||||
}
|
||||
if (source && !srcValid) return res.status(400).json({ error: '无效的来源' });
|
||||
|
||||
if (!code_id || !code) return res.status(400).json({ error: '请先完成邮箱验证' });
|
||||
const codeRow = await getRow("SELECT * FROM captchas WHERE id = ? AND created_at > NOW() - INTERVAL 10 MINUTE", [code_id]);
|
||||
@@ -74,24 +82,24 @@ router.post('/register', validateLengths({
|
||||
if (existingUser || existingEmail) {
|
||||
return res.status(400).json({ error: '用户名或邮箱已被使用' });
|
||||
}
|
||||
if (game_name && (source || 'netease') === 'netease') {
|
||||
const dup = await getRow('SELECT id FROM users WHERE game_name = ? AND source = ?', [game_name, source || 'netease']);
|
||||
if (game_name && source) {
|
||||
const dup = await getRow('SELECT id FROM users WHERE game_name = ? AND source = ?', [game_name, source]);
|
||||
if (dup) return res.status(400).json({ error: '该游戏名在此来源下已注册' });
|
||||
}
|
||||
// 多来源兼容: 该游戏名可能已被其他账号绑定为副身份
|
||||
try {
|
||||
const dupIdent = await getRow('SELECT id FROM user_identities WHERE source = ? AND game_name = ?', [source || 'netease', game_name]);
|
||||
const dupIdent = await getRow('SELECT id FROM user_identities WHERE source = ? AND game_name = ?', [source || '', game_name]);
|
||||
if (dupIdent) return res.status(400).json({ error: '该游戏名已绑定其他账号' });
|
||||
} catch {}
|
||||
|
||||
const hashed = await bcrypt.hash(password, 10);
|
||||
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 || 'netease']
|
||||
[username, hashed, email, game_name, game_uid||'', 1, 1, source || '']
|
||||
);
|
||||
try {
|
||||
await query('INSERT INTO user_identities(user_id, source, game_name, game_uid) VALUES (?,?,?,?)',
|
||||
[r.insertId, source || 'netease', game_name, game_uid || '']);
|
||||
[r.insertId, source || '', game_name, game_uid || '']);
|
||||
} catch {}
|
||||
res.status(201).json({ message: '注册成功,请登录' });
|
||||
} catch (err) {
|
||||
@@ -184,18 +192,23 @@ router.get('/identities', authenticate, async (req, res) => {
|
||||
router.post('/identities', authenticate, async (req, res) => {
|
||||
try {
|
||||
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' });
|
||||
// 来源动态校验(启用中的来源; sources 表缺失时兼容)
|
||||
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 = ?', [req.user.id, source]);
|
||||
const dup = await getRow('SELECT id FROM user_identities WHERE user_id = ? AND source = ?', [req.user.id, 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]);
|
||||
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 (?,?,?,?)',
|
||||
[req.user.id, source, game_name, game_uid || '']);
|
||||
[req.user.id, source || '', game_name, game_uid || '']);
|
||||
res.status(201).json({ id: r.insertId, message: '绑定成功' });
|
||||
} catch (err) {
|
||||
console.error('[auth]', err);
|
||||
|
||||
@@ -10,19 +10,46 @@ const { logSystem } = require('../logger');
|
||||
const router = express.Router();
|
||||
|
||||
// ============ 鉴权 ============
|
||||
// 外部 API 唯一鉴权方式: ID + Secret(api_clients 表)
|
||||
// 无凭据 / 凭据错误 / 客户端停用 → 一律 401(不允许匿名访问)
|
||||
// 流程: 客户端用 ID + Secret 换取 SESSION(短期有效), 之后所有请求用
|
||||
// Authorization: Bearer <SESSION>
|
||||
// 无 SESSION / SESSION 无效或过期 / 客户端停用 → 一律 401
|
||||
|
||||
const SESSION_TTL_HOURS = 24;
|
||||
|
||||
function genClientId() {
|
||||
// 16 位纯数字, 随机不递增
|
||||
let id = '';
|
||||
for (let i = 0; i < 16; i++) id += crypto.randomInt(0, 10);
|
||||
return id;
|
||||
}
|
||||
|
||||
function genSecret() {
|
||||
// SeaReport- + 32 位随机字符串
|
||||
return 'SeaReport-' + crypto.randomBytes(16).toString('hex'); // 16 bytes = 32 hex chars
|
||||
}
|
||||
|
||||
async function createSession(clientId) {
|
||||
const token = crypto.randomBytes(48).toString('hex');
|
||||
await query(
|
||||
'INSERT INTO api_sessions(client_id, session_token, expires_at) VALUES (?,?,DATE_ADD(NOW(), INTERVAL ? HOUR))',
|
||||
[clientId, token, SESSION_TTL_HOURS]
|
||||
);
|
||||
return { session_token: token, expires_in: SESSION_TTL_HOURS * 3600 };
|
||||
}
|
||||
|
||||
async function clientAuth(req, res, next) {
|
||||
const cid = req.headers['x-api-client-id'];
|
||||
const secret = req.headers['x-api-secret'];
|
||||
if (!cid || !secret) return res.status(401).json({ error: '未授权: 缺少 x-api-client-id / x-api-secret' });
|
||||
const h = req.headers.authorization;
|
||||
if (!h || !h.startsWith('Bearer ')) return res.status(401).json({ error: '未授权: 缺少 Authorization: Bearer <SESSION>' });
|
||||
const token = h.split(' ')[1];
|
||||
try {
|
||||
const client = await getRow('SELECT * FROM api_clients WHERE client_id = ?', [cid]);
|
||||
if (!client || !client.active || !bcrypt.compareSync(secret, client.secret_hash)) {
|
||||
return res.status(401).json({ error: '客户端鉴权失败' });
|
||||
}
|
||||
req.apiClient = client;
|
||||
const row = await getRow(
|
||||
`SELECT s.client_id, s.expires_at, c.active AS client_active
|
||||
FROM api_sessions s JOIN api_clients c ON c.client_id = s.client_id
|
||||
WHERE s.session_token = ? AND s.expires_at > NOW()`,
|
||||
[token]
|
||||
);
|
||||
if (!row || !row.client_active) return res.status(401).json({ error: 'SESSION 无效或已过期' });
|
||||
req.apiClient = { client_id: row.client_id };
|
||||
next();
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: '鉴权服务异常' });
|
||||
@@ -47,8 +74,8 @@ router.post('/clients', authenticate, async (req, res) => {
|
||||
if (!['owner','admin'].includes(req.user.role)) return res.status(403).json({ error: '无权限' });
|
||||
const { name } = req.body;
|
||||
if (!name) return res.status(400).json({ error: '名称必填' });
|
||||
const clientId = 'c_' + crypto.randomBytes(12).toString('hex');
|
||||
const secret = 's_' + crypto.randomBytes(24).toString('hex');
|
||||
const clientId = genClientId();
|
||||
const secret = genSecret();
|
||||
await query('INSERT INTO api_clients(name, client_id, secret_hash) VALUES (?,?,?)', [name, clientId, bcrypt.hashSync(secret, 10)]);
|
||||
await logSystem('info', 'api', `创建外部API客户端: ${name}`, { client_id: clientId });
|
||||
res.json({ client_id: clientId, secret, message: '请立即保存 secret(仅显示一次)' });
|
||||
@@ -82,24 +109,43 @@ router.delete('/clients/:id', authenticate, async (req, res) => {
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// ---- 以下接口: 外部鉴权(client_id + secret, 唯一方式) ----
|
||||
// ============ 换取 SESSION(ID + Secret → Bearer SESSION) ============
|
||||
// 仅此一个接口用 ID+Secret 鉴权, 其余接口一律 Bearer SESSION
|
||||
router.post('/auth/session', async (req, res) => {
|
||||
try {
|
||||
const cid = req.headers['x-api-client-id'];
|
||||
const secret = req.headers['x-api-secret'];
|
||||
if (!cid || !secret) return res.status(401).json({ error: '缺少 x-api-client-id / x-api-secret' });
|
||||
const client = await getRow('SELECT * FROM api_clients WHERE client_id = ?', [cid]);
|
||||
if (!client || !client.active || !bcrypt.compareSync(secret, client.secret_hash)) {
|
||||
return res.status(401).json({ error: '客户端鉴权失败' });
|
||||
}
|
||||
// 清理该客户端旧 SESSION(单会话)
|
||||
await query('DELETE FROM api_sessions WHERE client_id = ?', [cid]);
|
||||
const sess = await createSession(cid);
|
||||
await logSystem('info', 'api', `外部API客户端换取SESSION: ${client.name}`, { client_id: cid });
|
||||
res.json({ ...sess, client_id: cid, message: `SESSION 有效期 ${SESSION_TTL_HOURS} 小时, 请用 Authorization: Bearer <session> 访问其余接口` });
|
||||
} catch (e) { res.status(500).json({ error: '服务器错误' }); }
|
||||
});
|
||||
|
||||
// ---- 以下接口: 外部鉴权(Bearer SESSION, 唯一方式) ----
|
||||
router.use(clientAuth);
|
||||
|
||||
// ============ 外部注册(需客户端鉴权) ============
|
||||
router.post('/auth/register', async (req, res) => {
|
||||
try {
|
||||
const { username, password, email, game_name, game_uid, source } = req.body;
|
||||
if (!username || !password || !email || !game_name || !game_uid) return res.status(400).json({ error: '所有字段必填' });
|
||||
if (!username || !password || !email || !game_name) return res.status(400).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 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 || 'skin']);
|
||||
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 || '']);
|
||||
try {
|
||||
await query('INSERT INTO user_identities(user_id, source, game_name, game_uid) VALUES (?,?,?,?)',
|
||||
[r.insertId, source || 'skin', game_name, game_uid]);
|
||||
[r.insertId, source || '', game_name, game_uid || '']);
|
||||
} catch {}
|
||||
|
||||
const site = await getRow("SELECT v FROM settings WHERE k='site_url'");
|
||||
@@ -119,7 +165,7 @@ router.post('/tickets', async (req, res) => {
|
||||
try {
|
||||
const { type, title, reporter_game_name, reporter_game_uid, target_game_name, target_game_uid, reason, description, is_admin_complaint, server } = req.body;
|
||||
if (!type || !['report','suggestion','appeal'].includes(type)) return res.status(400).json({ error: '类型不正确' });
|
||||
if (!title || !reporter_game_name || !reporter_game_uid) return res.status(400).json({ error: '必填字段不完整' });
|
||||
if (!title || !reporter_game_name) return res.status(400).json({ error: '必填字段不完整' });
|
||||
if (type === 'report' && !reason) return res.status(400).json({ error: '请填写举报原因' });
|
||||
if (type === 'suggestion' && !description) return res.status(400).json({ error: '建议内容不能为空' });
|
||||
if (type === 'appeal' && (!reason || !description)) return res.status(400).json({ error: '请填写完整申诉信息' });
|
||||
@@ -130,8 +176,8 @@ router.post('/tickets', async (req, res) => {
|
||||
const sv = await resolveServer(server);
|
||||
const trackingToken = uuid();
|
||||
const r = await query(`INSERT INTO tickets(type,title,reporter_game_name,reporter_game_uid,target_game_name,target_game_uid,reason,description,tracking_token,is_admin_complaint,server_name) VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
[type, title, reporter_game_name, reporter_game_uid, target_game_name||'', target_game_uid||'', reason||'', description||'', trackingToken, is_admin_complaint?1:0, sv.server_name]);
|
||||
await query("INSERT INTO responses(ticket_id,content,is_staff) VALUES (?,?,0)", [r.insertId, `游戏内提交\n类型: ${type}\n提交人: ${reporter_game_name} (UID: ${reporter_game_uid})${sv.server_name?'\n服务器: '+sv.server_name:''}`]);
|
||||
[type, title, reporter_game_name, reporter_game_uid||'', target_game_name||'', target_game_uid||'', reason||'', description||'', trackingToken, is_admin_complaint?1:0, sv.server_name]);
|
||||
await query("INSERT INTO responses(ticket_id,content,is_staff) VALUES (?,?,0)", [r.insertId, `游戏内提交\n类型: ${type}\n提交人: ${reporter_game_name}${reporter_game_uid?' (UID: '+reporter_game_uid+')':''}${sv.server_name?'\n服务器: '+sv.server_name:''}`]);
|
||||
res.status(201).json({ id: r.insertId, tracking_token: trackingToken, server: sv.server_name });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
54
backend/routes/sources.js
Normal file
54
backend/routes/sources.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const express = require('express');
|
||||
const { query, getRow } = require('../db');
|
||||
const { authenticate, requireRole } = require('../middleware/auth');
|
||||
const { logSystem } = require('../logger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// ---- 来源列表(公开: 注册页/绑定身份页需要) ----
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const rows = await query('SELECT code, label, sort_order FROM sources WHERE enabled = 1 ORDER BY sort_order, id');
|
||||
res.json(rows);
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// ---- 管理(owner) ----
|
||||
router.post('/', authenticate, requireRole('owner'), async (req, res) => {
|
||||
try {
|
||||
const { code, label, sort_order } = req.body;
|
||||
if (!code || !label) return res.status(400).json({ error: 'code 和 label 必填' });
|
||||
if (!/^[a-z0-9_]{1,20}$/.test(code)) return res.status(400).json({ error: 'code 仅允许小写字母数字下划线(≤20)' });
|
||||
const dup = await getRow('SELECT id FROM sources WHERE code = ?', [code]);
|
||||
if (dup) return res.status(400).json({ error: '该来源已存在' });
|
||||
const r = await query('INSERT INTO sources(code, label, sort_order) VALUES (?,?,?)', [code, label, parseInt(sort_order) || 0]);
|
||||
await logSystem('info', 'source', `新增来源: ${code}(${label})`);
|
||||
res.status(201).json({ id: r.insertId, message: '已添加' });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
router.put('/:code', authenticate, requireRole('owner'), async (req, res) => {
|
||||
try {
|
||||
const { label, enabled, sort_order } = req.body;
|
||||
const row = await getRow('SELECT id FROM sources WHERE code = ?', [req.params.code]);
|
||||
if (!row) return res.status(404).json({ error: '来源不存在' });
|
||||
await query('UPDATE sources SET label = ?, enabled = ?, sort_order = ? WHERE code = ?',
|
||||
[label || req.params.code, enabled ? 1 : 0, parseInt(sort_order) || 0, req.params.code]);
|
||||
await logSystem('info', 'source', `更新来源: ${req.params.code}`, { label, enabled: !!enabled });
|
||||
res.json({ message: '已更新' });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
router.delete('/:code', authenticate, requireRole('owner'), async (req, res) => {
|
||||
try {
|
||||
// 有关联数据时禁止删除(防误删)
|
||||
const usedUsers = await getRow('SELECT COUNT(*) as c FROM users WHERE source = ?', [req.params.code]);
|
||||
const usedIdent = await getRow('SELECT COUNT(*) as c FROM user_identities WHERE source = ?', [req.params.code]);
|
||||
if (usedUsers.c > 0 || usedIdent.c > 0) return res.status(400).json({ error: `该来源已被 ${usedUsers.c + usedIdent.c} 条身份数据使用, 请先停用` });
|
||||
await query('DELETE FROM sources WHERE code = ?', [req.params.code]);
|
||||
await logSystem('info', 'source', `删除来源: ${req.params.code}`);
|
||||
res.json({ message: '已删除' });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -116,7 +116,7 @@ router.post('/', ticketAnonLimiter, optionalAuth, upload.array('files', 5), fina
|
||||
}
|
||||
if (!rgn) { rgn = req.user.game_name; rgu = req.user.game_uid; }
|
||||
}
|
||||
if (!rgn || !rgu) return res.status(400).json({ error: '请填写游戏名称和UID' });
|
||||
if (!rgn) return res.status(400).json({ error: '请填写游戏名称' });
|
||||
if (type === 'report') { if (!target_game_name && !target_game_uid) return res.status(400).json({ error: '举报需至少填写对方游戏名或UID之一' }); if (!reason) return res.status(400).json({ error: '请填写举报原因' }); }
|
||||
if (type === 'suggestion' && !description) return res.status(400).json({ error: '建议内容不能为空' });
|
||||
if (type === 'appeal') { if (!reason) return res.status(400).json({ error: '请填写申诉理由' }); if (!description) return res.status(400).json({ error: '请填写详细申诉内容' }); }
|
||||
@@ -143,7 +143,7 @@ router.post('/', ticketAnonLimiter, optionalAuth, upload.array('files', 5), fina
|
||||
await conn.beginTransaction();
|
||||
const [r] = await conn.execute(`INSERT INTO tickets(type,title,user_id,reporter_game_name,reporter_game_uid,
|
||||
target_game_name,target_game_uid,reason,description,tracking_token,is_admin_complaint,parent_ticket_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
[type, title, req.user?.id||null, rgn, rgu,
|
||||
[type, title, req.user?.id||null, rgn, rgu||'',
|
||||
type==='report'?(target_game_name||''):null, type==='report'?(target_game_uid||''):null,
|
||||
(type==='report'||type==='appeal'||type==='result_appeal')?reason:null,
|
||||
(type==='suggestion'||type==='appeal'||type==='result_appeal')?description:'',
|
||||
|
||||
@@ -33,18 +33,23 @@ router.post('/:id/identities', authenticate, requireRole('owner','admin'), async
|
||||
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' });
|
||||
// 来源动态校验
|
||||
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]);
|
||||
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]);
|
||||
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 || '']);
|
||||
[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: '绑定成功' });
|
||||
@@ -78,17 +83,17 @@ router.delete('/:id/identities/:identityId', authenticate, requireRole('owner','
|
||||
|
||||
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: '所有字段必填' });
|
||||
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 || 'netease']);
|
||||
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 || 'netease', game_name, game_uid]);
|
||||
[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: '创建成功' });
|
||||
@@ -102,7 +107,16 @@ router.put('/:id', authenticate, requireRole('owner','admin'), async (req, res)
|
||||
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) { if (!['netease','skin'].includes(req.body.source)) return res.status(400).json({ error: '无效来源' }); fields.source = req.body.source; }
|
||||
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); }
|
||||
@@ -116,7 +130,7 @@ router.put('/:id', authenticate, requireRole('owner','admin'), async (req, res)
|
||||
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 || 'netease', cur.game_name, cur.game_uid || '']);
|
||||
[req.params.id, cur.source || '', cur.game_name, cur.game_uid || '']);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user