docs: README + optimize auth getSecret, features id fix, token error msg
This commit is contained in:
93
README.md
Normal file
93
README.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# MC Report System
|
||||||
|
|
||||||
|
游戏玩家投诉建议申诉系统。支持 Web 端 + Paper 插件双端提交,MySQL 存储,JWT 鉴权,三级角色(服主/管理员/玩家)。
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
| 模块 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 举报 | 玩家举报违规行为,可指定目标玩家,上传截图/视频 |
|
||||||
|
| 建议 | 玩家提交改进建议 |
|
||||||
|
| 申诉 | 封禁申诉(Web端)+ 结果申诉(对处理不服) |
|
||||||
|
| 接单制 | 管理员认领工单,一人一单,可转单 |
|
||||||
|
| 审批流 | 待处理→处理中→待补充→已解决/已驳回→关闭 |
|
||||||
|
| 投票 | 分组子服定时/不定时投票,盲投制,管理员1.5票权 |
|
||||||
|
| 更新内容 | PCL风格功能投票,按时间/热度排序,分组筛选 |
|
||||||
|
| 通知 | SMTP邮件 + Discord Webhook 双通道 |
|
||||||
|
| 导出 | CSV格式导出工单/被举报人/日志(服主专享) |
|
||||||
|
| 插件 | Paper 1.20+ 插件,游戏内注册/登录/举报/追踪 |
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动
|
||||||
|
node backend/server.js
|
||||||
|
# → 访问 http://localhost:3100/#/install
|
||||||
|
```
|
||||||
|
|
||||||
|
安装向导两步完成:MySQL 连接配置 → 创建服主账号。重启后自动加载业务路由。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
| 层 | 技术 |
|
||||||
|
|----|------|
|
||||||
|
| 后端 | Node.js + Express + mysql2 |
|
||||||
|
| 前端 | Vanilla JS SPA + Hash路由 |
|
||||||
|
| 数据库 | MySQL 5.7+ |
|
||||||
|
| 认证 | JWT(256位随机密钥) |
|
||||||
|
| 安全 | Helmet + Rate Limit + XSS + CSP |
|
||||||
|
| 插件 | Paper API 1.20.4 + java.net.HttpURLConnection |
|
||||||
|
|
||||||
|
## 角色权限
|
||||||
|
|
||||||
|
| 操作 | 玩家 | 管理员 | 服主 |
|
||||||
|
|------|:--:|:--:|:--:|
|
||||||
|
| 提交工单 | ✅ | ✅ | ✅ |
|
||||||
|
| 查看工单 | 自己 | 待处理+自己接的 | 全部 |
|
||||||
|
| 接单 | — | 举报/申诉 | 全部 |
|
||||||
|
| 处理工单 | — | 自己接的 | 全部 |
|
||||||
|
| 用户管理 | — | ✅ | ✅ |
|
||||||
|
| 系统设置 | — | ✅ | ✅ |
|
||||||
|
| 导出数据 | — | — | ✅ |
|
||||||
|
| 投票管理 | — | — | ✅ |
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生产环境变量
|
||||||
|
PORT=3100 CORS_ORIGIN=https://your-domain.com node backend/server.js
|
||||||
|
|
||||||
|
# PM2
|
||||||
|
pm2 start backend/server.js --name mc-report
|
||||||
|
|
||||||
|
# Nginx 反代
|
||||||
|
# include nginx.inc;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
```
|
||||||
|
├── backend/
|
||||||
|
│ ├── server.js # Express入口
|
||||||
|
│ ├── db.js # MySQL连接池 + 建表
|
||||||
|
│ ├── mailer.js # SMTP邮件
|
||||||
|
│ ├── captcha.js # SVG验证码
|
||||||
|
│ ├── webhook.js # Discord Webhook
|
||||||
|
│ ├── middleware/
|
||||||
|
│ │ ├── auth.js # JWT认证
|
||||||
|
│ │ ├── security.js # 限速+XSS+APIKey
|
||||||
|
│ │ └── upload.js # 文件上传安全
|
||||||
|
│ └── routes/
|
||||||
|
│ ├── auth.js # 注册/登录
|
||||||
|
│ ├── tickets.js # 工单核心
|
||||||
|
│ ├── install.js # 安装向导
|
||||||
|
│ ├── external.js # 外部API(插件用)
|
||||||
|
│ └── ... # 其他路由
|
||||||
|
├── public/ # 前端SPA
|
||||||
|
├── mc-report-plugin/ # Paper 1.20+ 插件
|
||||||
|
├── nginx.inc # Nginx额外配置
|
||||||
|
└── data/ # 运行时数据
|
||||||
|
```
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { getConfig } = require('../db');
|
const { getConfig } = require('../db');
|
||||||
|
|
||||||
|
let cachedSecret = null;
|
||||||
|
|
||||||
function getSecret() {
|
function getSecret() {
|
||||||
|
if (cachedSecret) return cachedSecret;
|
||||||
try {
|
try {
|
||||||
const cfg = getConfig();
|
cachedSecret = getConfig()?.jwt_secret || process.env.JWT_SECRET;
|
||||||
const secret = cfg?.jwt_secret || process.env.JWT_SECRET;
|
} catch {}
|
||||||
if (secret) return secret;
|
if (!cachedSecret) throw new Error('JWT_SECRET not configured');
|
||||||
throw new Error('JWT_SECRET not configured');
|
return cachedSecret;
|
||||||
} catch (e) {
|
|
||||||
if (e.message === 'JWT_SECRET not configured') throw e;
|
|
||||||
return process.env.JWT_SECRET || (() => { throw new Error('JWT_SECRET not configured'); })();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateToken(user) {
|
function generateToken(user) {
|
||||||
@@ -22,7 +21,8 @@ function verifyToken(token) { return jwt.verify(token, getSecret()); }
|
|||||||
function authenticate(req, res, next) {
|
function authenticate(req, res, next) {
|
||||||
const h = req.headers.authorization;
|
const h = req.headers.authorization;
|
||||||
if (!h || !h.startsWith('Bearer ')) return res.status(401).json({ error: '请先登录' });
|
if (!h || !h.startsWith('Bearer ')) return res.status(401).json({ error: '请先登录' });
|
||||||
try { req.user = verifyToken(h.split(' ')[1]); next(); } catch { return res.status(401).json({ error: '登录已过期' }); }
|
try { req.user = verifyToken(h.split(' ')[1]); next(); }
|
||||||
|
catch (e) { return res.status(401).json({ error: e.name === 'TokenExpiredError' ? '登录已过期,请重新登录' : '无效的认证信息' }); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireRole(...roles) {
|
function requireRole(...roles) {
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ router.get('/', authenticate, async (req, res) => {
|
|||||||
router.post('/', authenticate, requireRole('owner'), async (req, res) => {
|
router.post('/', authenticate, requireRole('owner'), async (req, res) => {
|
||||||
const { title, description, group_name, server_name } = req.body;
|
const { title, description, group_name, server_name } = req.body;
|
||||||
if (!title) return res.status(400).json({ error: '标题为必填' });
|
if (!title) return res.status(400).json({ error: '标题为必填' });
|
||||||
await query('INSERT INTO feature_items(title, description, group_name, server_name) VALUES (?,?,?,?)', [title, description||'', group_name||'', server_name||'']);
|
const r = await query('INSERT INTO feature_items(title, description, group_name, server_name) VALUES (?,?,?,?)', [title, description||'', group_name||'', server_name||'']);
|
||||||
res.status(201).json({ id: 0 });
|
res.status(201).json({ id: r.insertId });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put('/:id', authenticate, requireRole('owner'), async (req, res) => {
|
router.put('/:id', authenticate, requireRole('owner'), async (req, res) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user