Files
rainblogweb/routes/admin-links.js
T

138 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const db = require('../db');
const { authMiddleware, adminOnly } = require('../middleware/auth');
const router = express.Router();
// slug 生成:name 小写、空格转连字符、去特殊字符(仅 [a-z0-9-]),空则随机后缀
function makeSlug(name) {
const base = String(name || '')
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
.slice(0, 32);
return base || ('panel-' + Math.random().toString(36).slice(2, 8));
}
// 确保 slug 唯一:存在同名则追加 -2/-3/...
function uniqueSlug(slug, excludeId) {
let s = slug;
let i = 1;
const exists = (x) => {
if (excludeId) return db.get('SELECT id FROM admin_links WHERE slug = ? AND id != ?', [x, excludeId]);
return db.get('SELECT id FROM admin_links WHERE slug = ?', [x]);
};
while (exists(s)) { s = slug + '-' + (++i); }
return s;
}
// 校验面板代理配置字段(返回 { ok, data?, error? }
function validateProxyFields(body, current) {
const data = {};
// proxy_headers:合法 JSON 对象(空串允许)
if (body.proxy_headers !== undefined) {
const raw = String(body.proxy_headers);
if (raw.trim() !== '') {
try {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return { ok: false, error: 'proxy_headers 需为 JSON 对象' };
data.proxy_headers = raw;
} catch { return { ok: false, error: 'proxy_headers 不是合法 JSON' }; }
} else {
data.proxy_headers = '';
}
}
// permissionsJSON 数组
if (body.permissions !== undefined) {
const raw = String(body.permissions);
if (raw.trim() !== '') {
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return { ok: false, error: 'permissions 需为 JSON 数组' };
data.permissions = raw;
} catch { return { ok: false, error: 'permissions 不是合法 JSON' }; }
} else {
data.permissions = '';
}
}
// scale0.1-3
if (body.scale !== undefined) {
const s = parseFloat(body.scale);
if (!Number.isFinite(s) || s < 0.1 || s > 3) return { ok: false, error: 'scale 需在 0.1-3 之间' };
data.scale = s;
}
// trusted0/1
if (body.trusted !== undefined) {
const t = Number(body.trusted);
if (t !== 0 && t !== 1) return { ok: false, error: 'trusted 需为 0 或 1' };
data.trusted = t;
}
// proxy_skip_tls_verify0/1
if (body.proxy_skip_tls_verify !== undefined) {
data.proxy_skip_tls_verify = Number(body.proxy_skip_tls_verify) ? 1 : 0;
}
// slug:唯一([a-z0-9-]),空则自动生成
if (body.slug !== undefined) {
const raw = String(body.slug || '').trim().toLowerCase();
if (raw !== '') {
if (!/^[a-z0-9-]{1,32}$/.test(raw)) return { ok: false, error: 'slug 仅限小写字母/数字/连字符,≤32 字符' };
if (['token', 'fetch'].includes(raw)) return { ok: false, error: 'slug 为保留名,请更换' };
data.slug = uniqueSlug(raw, current && current.id);
} else {
data.slug = uniqueSlug(makeSlug((body.title || (current && current.title) || '')), current && current.id);
}
}
return { ok: true, data };
}
router.get('/', authMiddleware, adminOnly, (req, res) => {
res.json(db.all('SELECT * FROM admin_links ORDER BY sort_order ASC, id ASC'));
});
router.post('/', authMiddleware, adminOnly, (req, res) => {
const { title, url, embed_url, description, icon, category, sort_order, use_proxy, version } = req.body;
if (!title || !url) return res.status(400).json({ error: '标题和链接不能为空' });
const vf = validateProxyFields(req.body);
if (!vf.ok) return res.status(400).json({ error: vf.error });
const d = vf.data;
const id = db.run(
`INSERT INTO admin_links (title, url, embed_url, description, icon, category, sort_order, use_proxy, version,
proxy_headers, proxy_skip_tls_verify, permissions, scale, trusted, slug) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[title, url, embed_url || '', description || '', icon || '', category || '默认', sort_order || 0, use_proxy ? 1 : 0, version || '',
d.proxy_headers || '', d.proxy_skip_tls_verify || 0, d.permissions || '', d.scale !== undefined ? d.scale : 1.0, d.trusted !== undefined ? d.trusted : 1,
d.slug || uniqueSlug(makeSlug(title))]);
res.json(db.get('SELECT * FROM admin_links WHERE id = ?', [id]));
});
router.put('/:id', authMiddleware, adminOnly, (req, res) => {
const existing = db.get('SELECT * FROM admin_links WHERE id = ?', [req.params.id]);
if (!existing) return res.status(404).json({ error: '链接不存在' });
const { title, url, embed_url, description, icon, category, sort_order, use_proxy, version } = req.body;
const vf = validateProxyFields(req.body, existing);
if (!vf.ok) return res.status(400).json({ error: vf.error });
const d = vf.data;
db.run(
`UPDATE admin_links SET title=?, url=?, embed_url=?, description=?, icon=?, category=?, sort_order=?, use_proxy=?, version=?,
proxy_headers=?, proxy_skip_tls_verify=?, permissions=?, scale=?, trusted=?, slug=? WHERE id=?`,
[title || '', url || '', embed_url || '', description || '', icon || '', category || '默认', sort_order || 0, use_proxy ? 1 : 0, version || '',
d.proxy_headers !== undefined ? d.proxy_headers : existing.proxy_headers,
d.proxy_skip_tls_verify !== undefined ? d.proxy_skip_tls_verify : existing.proxy_skip_tls_verify,
d.permissions !== undefined ? d.permissions : existing.permissions,
d.scale !== undefined ? d.scale : existing.scale,
d.trusted !== undefined ? d.trusted : existing.trusted,
d.slug !== undefined ? d.slug : (existing.slug || uniqueSlug(makeSlug(existing.title))),
req.params.id]);
res.json(db.get('SELECT * FROM admin_links WHERE id = ?', [req.params.id]));
});
router.delete('/:id', authMiddleware, adminOnly, (req, res) => {
if (!db.get('SELECT id FROM admin_links WHERE id = ?', [req.params.id]))
return res.status(404).json({ error: '链接不存在' });
db.run('DELETE FROM admin_links WHERE id = ?', [req.params.id]);
res.json({ message: '删除成功' });
});
module.exports = router;