138 lines
6.1 KiB
JavaScript
138 lines
6.1 KiB
JavaScript
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 = '';
|
||
}
|
||
}
|
||
// permissions:JSON 数组
|
||
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 = '';
|
||
}
|
||
}
|
||
// scale:0.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;
|
||
}
|
||
// trusted:0/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_verify:0/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;
|