统一前后台主题颜色管理

This commit is contained in:
2026-09-05 23:12:17 +08:00
parent 69ab5676b4
commit 390c4ac53f
11 changed files with 643 additions and 88 deletions
+54 -1
View File
@@ -4,6 +4,11 @@ const { authMiddleware, adminOnly } = require('../middleware/auth');
const router = express.Router();
router.use((req, res, next) => {
res.setHeader('Cache-Control', 'no-store');
next();
});
const PUBLIC_KEYS = ['site_name','site_description','site_url','primary_color',
'recaptcha_site_key','turnstile_site_key',
'theme_wallpaper','theme_wallpaper_scale','theme_wallpaper_enabled','nav_style','card_style',
@@ -33,6 +38,50 @@ const ALL_KEYS = ['site_name','site_description','site_url','primary_color','rec
const ALLOWED_SET = [...ALL_KEYS, 'recaptcha_secret_key', 'smtp_pass', 'turnstile_secret_key', 'rainid_client_secret'];
const THEME_RULES = {
glass_blur: (value) => {
const raw = String(value).trim();
const number = Number(raw);
return /^\d+$/.test(raw) && Number.isInteger(number) && number >= 5 && number <= 40;
},
glass_opacity: (value) => {
const raw = String(value).trim();
const number = Number(raw);
return raw !== '' && Number.isFinite(number) && number >= 0.1 && number <= 0.95;
},
theme_force_dark: (value) => ['0', '1'].includes(String(value)),
theme_wallpaper_scale: (value) => ['cover', 'contain', 'repeat', 'stretch'].includes(String(value)),
nav_style: (value) => ['default', 'glass', 'capsule'].includes(String(value)),
card_style: (value) => ['default', 'glass'].includes(String(value)),
};
function normalizePrimaryColor(value) {
const raw = String(value).trim();
if (/^#[0-9a-f]{3}$/i.test(raw)) {
return '#' + raw.slice(1).split('').map((c) => c + c).join('').toLowerCase();
}
if (/^#[0-9a-f]{6}$/i.test(raw)) return raw.toLowerCase();
return null;
}
function validateThemeSettings(body) {
const validated = {};
if (body.primary_color !== undefined) {
const color = normalizePrimaryColor(body.primary_color);
if (!color) return { error: '主色调格式无效:仅支持 #RGB 或 #RRGGBB' };
validated.primary_color = color;
}
for (const [key, validate] of Object.entries(THEME_RULES)) {
if (body[key] === undefined) continue;
if (!validate(body[key])) return { error: `主题设置 ${key} 的值无效` };
validated[key] = String(body[key]).trim();
}
return { validated };
}
// footer_columns JSON 校验:数组(最多 3 栏,并行展示上限);每项 {title: string ≤20, links: [{label ≤50, url ≤200}]}。
// url 白名单:站内相对路径以 / 开头(排除 // 协议相对),或 http(s):// 外链;拒绝 javascript:/data:/vbscript: 等危险协议。
// 返回解析后的数组,非法返回 null。
@@ -71,6 +120,9 @@ router.get('/', authMiddleware, adminOnly, (req, res) => {
});
router.put('/', authMiddleware, adminOnly, (req, res) => {
const themeValidation = validateThemeSettings(req.body);
if (themeValidation.error) return res.status(400).json({ error: themeValidation.error });
// footer_columns 特判校验:非法 JSON/结构/危险协议 → 400 不落库(空串表示回退硬编码,放行)
if (req.body.footer_columns !== undefined && String(req.body.footer_columns) !== '') {
if (!validateFooterColumns(String(req.body.footer_columns))) {
@@ -79,8 +131,9 @@ router.put('/', authMiddleware, adminOnly, (req, res) => {
});
}
}
const values = { ...req.body, ...themeValidation.validated };
for (const [key, value] of Object.entries(req.body)) {
if (ALLOWED_SET.includes(key)) db.setSetting(key, String(value));
if (ALLOWED_SET.includes(key)) db.setSetting(key, values[key] === undefined ? String(value) : values[key]);
}
res.json({ message: '设置已保存' });
});