Files
MC_Report/backend/routes/settings.js
canglan 8c5ce78fd0 chore: add AGPLv3 copyright header to all source files
- 52 JS files (backend + public/js): header with
  Copyright (C) 2026 Sea Network Technology Studio
  Author: CangLan <admin@sea-studio.top>
  + AGPLv3 notice
- idempotent (skips if header present), all syntax-checked
2026-08-19 20:27:05 +08:00

114 lines
5.6 KiB
JavaScript

/*
* MC Report System
* Copyright (C) 2026 Sea Network Technology Studio
* Author: CangLan <admin@sea-studio.top>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const express = require('express');
const { query, getRow } = require('../db');
const { authenticate, requireRole } = require('../middleware/auth');
const router = express.Router();
router.get('/email-templates', authenticate, requireRole('owner','admin'), async (req, res) => {
res.json(await query('SELECT * FROM email_templates ORDER BY id'));
});
router.get('/email-templates/:code', authenticate, requireRole('owner','admin'), async (req, res) => {
const t = await getRow('SELECT * FROM email_templates WHERE code = ?', [req.params.code]);
if (!t) return res.status(404).json({ error: '模板不存在' });
res.json(t);
});
router.put('/email-templates/:code', authenticate, requireRole('owner','admin'), async (req, res) => {
const { subject, body } = req.body;
if (!subject||!body) return res.status(400).json({ error: '标题和内容不能为空' });
const t = await getRow('SELECT * FROM email_templates WHERE code = ?', [req.params.code]);
if (!t) return res.status(404).json({ error: '模板不存在' });
await query('UPDATE email_templates SET subject = ?, body = ? WHERE code = ?', [subject, body, req.params.code]);
await query("INSERT INTO audit_logs(user_id, username, action, entity_type, entity_id, details) VALUES (?,?,?,?,?,?)", [req.user.id, req.user.username, 'update_email_template', 'email_template', t.id, `更新模板: ${t.name}`]);
res.json({ message: '模板更新成功' });
});
router.get('/settings', authenticate, requireRole('owner','admin'), async (req, res) => {
const rows = await query('SELECT k, v, label FROM settings ORDER BY k');
const map = {};
for (const r of rows) map[r.k] = { value: r.v, label: r.label };
res.json(map);
});
router.put('/settings', authenticate, requireRole('owner','admin'), async (req, res) => {
const updates = req.body;
if (!updates||typeof updates!=='object') return res.status(400).json({ error: '无效数据' });
for (const [key, val] of Object.entries(updates)) {
const v = typeof val === 'object' ? val.value : String(val);
const l = typeof val === 'object' ? val.label : null;
await query("INSERT INTO settings(k, v, label) VALUES (?,?,?) ON DUPLICATE KEY UPDATE v = VALUES(v)", [key, v, l]);
}
await query("INSERT INTO audit_logs(user_id, username, action, entity_type, entity_id, details) VALUES (?,?,?,?,?,?)", [req.user.id, req.user.username, 'update_settings', 'settings', 0, '更新系统设置']);
res.json({ message: '设置已保存' });
});
// ---- 皮肤站 UUID 查询(Yggdrasil API: POST {site}/api/yggdrasil/api/profiles/minecraft) ----
function formatUuid(id) {
if (!id) return '';
const s = String(id).replace(/-/g, '').toLowerCase();
if (s.length !== 32) return String(id);
return `${s.slice(0,8)}-${s.slice(8,12)}-${s.slice(12,16)}-${s.slice(16,20)}-${s.slice(20)}`;
}
// 公开: 读取站点配置的皮肤站地址(玩家绑定身份时自动带出)
router.get('/skin-site', async (req, res) => {
try {
const row = await getRow("SELECT v FROM settings WHERE k = 'skin_site'");
res.json({ site: row?.v || '' });
} catch { res.json({ site: '' }); }
});
router.get('/uuid-lookup', authenticate, async (req, res) => {
const { site, name } = req.query;
if (!site || !name) return res.status(400).json({ error: '缺少参数 site 或 name' });
if (!/^[A-Za-z0-9_]{1,32}$/.test(name)) return res.status(400).json({ error: '用户名仅允许字母数字下划线' });
let u;
try { u = new URL(site); } catch { return res.status(400).json({ error: '皮肤站地址无效' }); }
if (u.protocol !== 'https:' && u.protocol !== 'http:') return res.status(400).json({ error: '协议不支持' });
const host = u.hostname;
if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' ||
host.startsWith('192.168.') || host.startsWith('10.') || host.startsWith('172.16.')) {
return res.status(400).json({ error: '不允许内网地址(SSRF 防护)' });
}
try {
const ctl = new AbortController();
const t = setTimeout(() => ctl.abort(), 10000);
const resp = await fetch(`${u.origin}/api/yggdrasil/api/profiles/minecraft`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'User-Agent': 'MCReport/1.0' },
body: JSON.stringify([name]),
signal: ctl.signal,
});
clearTimeout(t);
if (resp.status === 204) return res.json({ found: false, uuid: '', message: '皮肤站未找到该用户名' });
const data = await resp.json();
const hit = Array.isArray(data) ? data.find(x => x && x.name && String(x.name).toLowerCase() === name.toLowerCase()) : null;
if (!hit?.id) return res.json({ found: false, uuid: '', message: '皮肤站未找到该用户名' });
res.json({ found: true, uuid: formatUuid(hit.id), raw_id: String(hit.id).replace(/-/g, '') });
} catch (e) {
res.status(502).json({ error: '查询皮肤站失败: ' + (e.message || '网络错误') });
}
});
module.exports = router;