feat: MC Report System - MySQL + Express + Vanilla JS SPA

This commit is contained in:
2026-07-12 01:23:19 +08:00
commit 247a4e851d
52 changed files with 5710 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
const xss = require('xss');
const rateLimit = require('express-rate-limit');
const fs = require('fs');
const path = require('path');
const CONFIG_PATH = path.join(__dirname, '..', '..', 'data', 'config.json');
function readApiKey() {
try {
if (fs.existsSync(CONFIG_PATH)) {
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')).api_key || null;
}
} catch {}
return null;
}
let cachedApiKey = null;
function getApiKey() {
if (cachedApiKey === null) cachedApiKey = readApiKey();
return cachedApiKey;
}
function clearApiKeyCache() { cachedApiKey = null; }
const xssOptions = {
whiteList: {},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style', 'xml', 'iframe', 'object', 'embed'],
};
function sanitize(value) {
if (typeof value !== 'string') return value;
return xss(value, xssOptions).trim();
}
function sanitizeBody(req, res, next) {
if (req.body) {
for (const key of Object.keys(req.body)) {
if (typeof req.body[key] === 'string') {
req.body[key] = sanitize(req.body[key]);
}
}
}
next();
}
function validateLengths(rules) {
return (req, res, next) => {
for (const [field, maxLen] of Object.entries(rules)) {
const val = req.body[field];
if (val && typeof val === 'string' && val.length > maxLen) {
return res.status(400).json({ error: `${field} 长度不能超过 ${maxLen} 个字符` });
}
}
next();
};
}
const loginLimiter = rateLimit({
windowMs: 60 * 1000,
max: 8,
message: { error: '登录尝试过于频繁请1分钟后再试' },
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.ip,
});
const registerLimiter = rateLimit({
windowMs: 60 * 1000,
max: 3,
message: { error: '注册请求过于频繁请1分钟后再试' },
standardHeaders: true,
legacyHeaders: false,
});
const ticketLimiter = rateLimit({
windowMs: 60 * 1000,
max: 10,
message: { error: '提交过于频繁,请稍后再试' },
standardHeaders: true,
legacyHeaders: false,
});
const ticketAnonLimiter = rateLimit({
windowMs: 60 * 1000,
max: 5,
message: { error: '匿名提交过于频繁,请登录后再试或稍后重试' },
standardHeaders: true,
legacyHeaders: false,
});
const uploadLimiter = rateLimit({
windowMs: 60 * 1000,
max: 5,
message: { error: '上传请求过于频繁' },
standardHeaders: true,
legacyHeaders: false,
});
const generalLimiter = rateLimit({
windowMs: 60 * 1000,
max: 300,
message: { error: '请求过于频繁' },
standardHeaders: true,
legacyHeaders: false,
});
const captchaLimiter = rateLimit({
windowMs: 60 * 1000,
max: 30,
message: { error: '验证码请求过于频繁' },
});
function apiKeyGuard(req, res, next) {
const p = req.originalUrl;
if (p === '/api/health' || p.startsWith('/api/install')) return next();
const key = getApiKey();
if (!key) return next();
if (req.headers['x-api-key'] !== key) return res.status(401).json({ error: '无效的 API 密钥' });
next();
}
function methodGuard(allowed) {
return (req, res, next) => {
if (!allowed.includes(req.method)) {
return res.status(405).json({ error: `不允许 ${req.method} 方法` });
}
next();
};
}
module.exports = {
sanitize,
sanitizeBody,
validateLengths,
loginLimiter,
registerLimiter,
ticketLimiter,
ticketAnonLimiter,
uploadLimiter,
generalLimiter,
captchaLimiter,
apiKeyGuard,
methodGuard,
clearApiKeyCache,
};