feat: uuid for sub link

This commit is contained in:
2026-03-31 14:25:24 +08:00
parent 21c91fc213
commit ec2c5bab80
4 changed files with 68 additions and 22 deletions

View File

@@ -1,5 +1,6 @@
import express from 'express'; import express from 'express';
import path from 'path'; import path from 'path';
import crypto from 'crypto';
import './db.js'; // Initialize database import './db.js'; // Initialize database
import subscriptionsRouter from './routes/subscriptions.js'; import subscriptionsRouter from './routes/subscriptions.js';
import nodesRouter from './routes/nodes.js'; import nodesRouter from './routes/nodes.js';
@@ -8,16 +9,30 @@ import surgeRouter from './routes/surge.js';
import db from './db.js'; import db from './db.js';
import { generateSurgeConfig } from './services/generator.js'; import { generateSurgeConfig } from './services/generator.js';
// Ensure surge_token exists
function ensureSurgeToken(): string {
const row = db.prepare("SELECT value FROM config WHERE key = 'surge_token'").get() as any;
if (row?.value) return row.value;
const token = crypto.randomUUID();
db.prepare("INSERT OR REPLACE INTO config (key, value) VALUES ('surge_token', ?)").run(token);
return token;
}
ensureSurgeToken();
const app = express(); const app = express();
const PORT = parseInt(process.env.PORT || '3456', 10); const PORT = parseInt(process.env.PORT || '3456', 10);
app.use(express.json()); app.use(express.json());
// Surge endpoint (no auth, before everything) // Surge endpoint (no auth, token-protected path)
app.get('/surge', (req, res) => { app.get('/surge/:token', (req, res) => {
const row = db.prepare("SELECT value FROM config WHERE key = 'surge_token'").get() as any;
if (!row?.value || req.params.token !== row.value) {
return res.status(404).send('Not Found');
}
const host = req.headers.host || 'localhost:3456'; const host = req.headers.host || 'localhost:3456';
const protocol = req.secure ? 'https' : 'http'; const protocol = req.secure ? 'https' : 'http';
const hostUrl = `${protocol}://${host}/surge`; const hostUrl = `${protocol}://${host}/surge/${row.value}`;
const config = generateSurgeConfig(hostUrl); const config = generateSurgeConfig(hostUrl);
res.set({ res.set({
'Content-Type': 'text/plain; charset=utf-8', 'Content-Type': 'text/plain; charset=utf-8',
@@ -89,12 +104,13 @@ app.get('/api/stats', (_req, res) => {
const webDist = path.join(__dirname, '..', '..', 'web', 'dist'); const webDist = path.join(__dirname, '..', '..', 'web', 'dist');
app.use(express.static(webDist)); app.use(express.static(webDist));
app.get('*', (req, res, next) => { app.get('*', (req, res, next) => {
if (req.path.startsWith('/api') || req.path === '/surge') return next(); if (req.path.startsWith('/api') || req.path.startsWith('/surge')) return next();
res.sendFile(path.join(webDist, 'index.html')); res.sendFile(path.join(webDist, 'index.html'));
}); });
app.listen(PORT, () => { app.listen(PORT, () => {
const token = ensureSurgeToken();
console.log(`Sub Router running at http://127.0.0.1:${PORT}`); console.log(`Sub Router running at http://127.0.0.1:${PORT}`);
console.log(`Surge subscription: http://127.0.0.1:${PORT}/surge`); console.log(`Surge subscription: http://127.0.0.1:${PORT}/surge/${token}`);
console.log(`Admin panel: http://127.0.0.1:${PORT}`); console.log(`Admin panel: http://127.0.0.1:${PORT}`);
}); });

View File

@@ -1,28 +1,30 @@
import { Router } from 'express'; import { Router } from 'express';
import crypto from 'crypto';
import db from '../db.js';
import { generateSurgeConfig } from '../services/generator.js'; import { generateSurgeConfig } from '../services/generator.js';
const router = Router(); const router = Router();
// GET /surge - Surge client subscription endpoint (no auth required) // GET /api/config/surge-token - get current surge token
router.get('/', (req, res) => { router.get('/surge-token', (_req, res) => {
const host = req.headers.host || 'localhost:3456'; const row = db.prepare("SELECT value FROM config WHERE key = 'surge_token'").get() as any;
const protocol = req.secure ? 'https' : 'http'; res.json({ token: row?.value || null });
const hostUrl = `${protocol}://${host}/surge`;
const config = generateSurgeConfig(hostUrl);
res.set({
'Content-Type': 'text/plain; charset=utf-8',
'Content-Disposition': 'attachment; filename=sub-router.conf',
}); });
res.send(config);
// POST /api/config/surge-token - regenerate surge token
router.post('/surge-token', (_req, res) => {
const token = crypto.randomUUID();
db.prepare("INSERT OR REPLACE INTO config (key, value) VALUES ('surge_token', ?)").run(token);
res.json({ token });
}); });
// GET /api/config/preview - preview generated config // GET /api/config/preview - preview generated config
router.get('/preview', (req, res) => { router.get('/preview', (req, res) => {
const host = req.headers.host || 'localhost:3456'; const host = req.headers.host || 'localhost:3456';
const protocol = req.secure ? 'https' : 'http'; const protocol = req.secure ? 'https' : 'http';
const hostUrl = `${protocol}://${host}/surge`; const row = db.prepare("SELECT value FROM config WHERE key = 'surge_token'").get() as any;
const token = row?.value || '';
const hostUrl = `${protocol}://${host}/surge/${token}`;
const config = generateSurgeConfig(hostUrl); const config = generateSurgeConfig(hostUrl);
res.json({ config }); res.json({ config });

View File

@@ -114,6 +114,8 @@ export const rules = {
// Config // Config
export const config = { export const config = {
preview: () => request<{ config: string }>('/config/preview'), preview: () => request<{ config: string }>('/config/preview'),
getSurgeToken: () => request<{ token: string }>('/config/surge-token'),
regenerateSurgeToken: () => request<{ token: string }>('/config/surge-token', { method: 'POST' }),
}; };
// Stats // Stats

View File

@@ -5,8 +5,25 @@ export default function Output() {
const [preview, setPreview] = useState(''); const [preview, setPreview] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [surgeToken, setSurgeToken] = useState('');
const surgeUrl = `${window.location.origin}/surge`; const surgeUrl = surgeToken ? `${window.location.origin}/surge/${surgeToken}` : '';
const loadToken = async () => {
try {
const data = await configApi.getSurgeToken();
setSurgeToken(data.token || '');
} catch {}
};
const handleRegenerate = async () => {
if (!confirm('重新生成后旧的订阅链接将失效Surge 客户端需要更新订阅地址。确定继续?')) return;
try {
const data = await configApi.regenerateSurgeToken();
setSurgeToken(data.token);
loadPreview();
} catch {}
};
const loadPreview = async () => { const loadPreview = async () => {
setLoading(true); setLoading(true);
@@ -20,7 +37,10 @@ export default function Output() {
} }
}; };
useEffect(() => { loadPreview(); }, []); useEffect(() => {
loadToken();
loadPreview();
}, []);
const handleCopy = () => { const handleCopy = () => {
navigator.clipboard.writeText(surgeUrl); navigator.clipboard.writeText(surgeUrl);
@@ -58,12 +78,18 @@ export default function Output() {
fontSize: 13, fontSize: 13,
color: 'var(--accent)', color: 'var(--accent)',
userSelect: 'all', userSelect: 'all',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}> }}>
{surgeUrl} {surgeUrl || '加载中...'}
</code> </code>
<button className="small" onClick={handleCopy}> <button className="small" onClick={handleCopy} disabled={!surgeUrl}>
{copied ? '已复制' : '复制'} {copied ? '已复制' : '复制'}
</button> </button>
<button className="small" onClick={handleRegenerate} disabled={!surgeUrl}>
</button>
</div> </div>
</div> </div>