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;