diff --git a/backend/routes/external.js b/backend/routes/external.js new file mode 100644 index 0000000..109c19c --- /dev/null +++ b/backend/routes/external.js @@ -0,0 +1,79 @@ +const express = require('express'); +const { query, getRow } = require('../db'); +const { authenticate } = require('../middleware/auth'); + +const router = express.Router(); + +const { getConfig } = require('../db'); + +function getExternalKey() { + try { return getConfig()?.external_api_key || process.env.EXTERNAL_API_KEY || ''; } catch { return process.env.EXTERNAL_API_KEY || ''; } +} + +function externalAuth(req, res, next) { + const required = getExternalKey(); + if (!required) return next(); + if (req.headers['x-external-key'] === required) return next(); + return res.status(401).json({ error: '未授权' }); +} + +router.use(externalAuth); + +router.get('/tickets', async (req, res) => { + const { type, status, page, limit } = req.query; + let q = `SELECT t.id, t.type, t.title, t.status, t.priority, + t.reporter_game_name, t.reporter_game_uid, + t.target_game_name, t.target_game_uid, + t.reason, t.description, t.assigned_to, t.claim_note, + t.created_at, t.updated_at + FROM tickets t WHERE 1=1`; + const p = []; + if (type) { q += ' AND t.type = ?'; p.push(type); } + if (status) { q += ' AND t.status = ?'; p.push(status); } + const pg = parseInt(page) || 1; + const lm = Math.min(parseInt(limit) || 50, 200); + q += ` ORDER BY t.updated_at DESC LIMIT ${(pg-1)*lm}, ${lm}`; + res.json(await query(q, p)); +}); + +router.get('/tickets/:id', async (req, res) => { + const t = await getRow(`SELECT id, type, title, status, priority, + reporter_game_name, reporter_game_uid, target_game_name, target_game_uid, + reason, description, assigned_to, claim_note, + created_at, updated_at FROM tickets WHERE id = ?`, [req.params.id]); + if (!t) return res.status(404).json({ error: '工单不存在' }); + t.responses = await query('SELECT content, is_staff, created_at FROM responses WHERE ticket_id = ? ORDER BY created_at ASC', [req.params.id]); + t.attachments = await query('SELECT id, original_name, mime_type, size FROM attachments WHERE ticket_id = ?', [req.params.id]); + res.json(t); +}); + +router.get('/reported-players', async (req, res) => { + const rows = await query(`SELECT target_game_name, target_game_uid, + COUNT(*) as total, SUM(CASE WHEN status IN ('pending','processing','awaiting_info','appealing') THEN 1 ELSE 0 END) as active, + SUM(CASE WHEN status='resolved' THEN 1 ELSE 0 END) as resolved, + MAX(created_at) as last_report + FROM tickets WHERE type='report' AND target_game_name IS NOT NULL AND target_game_name != '' + GROUP BY target_game_name, target_game_uid ORDER BY total DESC LIMIT 500`); + res.json(rows); +}); + +router.get('/stats', async (req, res) => { + const [[{c:total}], byType, byStatus] = await Promise.all([ + query('SELECT COUNT(*) as c FROM tickets'), + query('SELECT type, COUNT(*) as c FROM tickets GROUP BY type'), + query('SELECT status, COUNT(*) as c FROM tickets GROUP BY status'), + ]); + res.json({ total, byType, byStatus }); +}); + +router.get('/users', async (req, res) => { + res.json(await query('SELECT id, username, email, game_name, game_uid, role, active, created_at FROM users ORDER BY id')); +}); + +router.get('/user/:id', async (req, res) => { + const u = await getRow('SELECT id, username, email, game_name, game_uid, role, active, created_at FROM users WHERE id = ?', [req.params.id]); + if (!u) return res.status(404).json({ error: '用户不存在' }); + res.json(u); +}); + +module.exports = router; diff --git a/backend/routes/install.js b/backend/routes/install.js index 107dc1c..c051a18 100644 --- a/backend/routes/install.js +++ b/backend/routes/install.js @@ -69,6 +69,7 @@ router.post('/complete', async (req, res) => { const jwtSecret = crypto.randomBytes(32).toString('hex'); const apiKey = crypto.randomBytes(24).toString('hex'); + const externalKey = crypto.randomBytes(24).toString('hex'); for (const [k, v, l] of [ ['site_name', site_name || 'MC举报系统', '站点名称'], @@ -86,6 +87,7 @@ router.post('/complete', async (req, res) => { db_user, db_pass: db_pass || '', db_name, jwt_secret: jwtSecret, api_key: apiKey, + external_api_key: externalKey, }); res.json({ ok: true, message: '安装完成,请重新启动服务器' }); diff --git a/backend/server.js b/backend/server.js index 726d1b4..3bdacd3 100644 --- a/backend/server.js +++ b/backend/server.js @@ -98,6 +98,7 @@ if (isInstalled()) { app.use('/api/export', methodGuard(['GET']), generalLimiter, require('./routes/export')); app.use('/api/uploads', methodGuard(['GET']), generalLimiter, require('./routes/uploads')); app.use('/api/notifications', methodGuard(['GET','POST','PUT','DELETE']), generalLimiter, require('./routes/notifications')); + app.use('/api/external', require('./routes/external')); app.get('/api/verify', (req, res) => res.redirect(`/#/verify?token=${req.query.token}`)); }