From 8534441d59f8d4b51f0c833626937048ccefd92e Mon Sep 17 00:00:00 2001 From: AnranYus Date: Wed, 10 Sep 2025 17:27:53 +0800 Subject: [PATCH] update --- backend/package.json | 3 ++- backend/src/index.js | 28 +++++++++++++++++++++++ backend/src/middleware/auth.js | 25 ++++++++++++++++++++ backend/src/services/wgService.js | 18 ++++++++++++++- backend/src/utils/configUtils.js | 12 ++++++++++ frontend/src/App.vue | 38 +++++++++++++++++++++++++++++++ 6 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 backend/src/middleware/auth.js diff --git a/backend/package.json b/backend/package.json index 76e92d8..2955d05 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,7 +10,8 @@ "dependencies": { "body-parser": "^1.20.2", "cors": "^2.8.5", - "express": "^4.19.2" + "express": "^4.19.2", + "jsonwebtoken": "^9.0.2" } } diff --git a/backend/src/index.js b/backend/src/index.js index 5954322..50d5e0c 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -3,6 +3,7 @@ import cors from 'cors'; import bodyParser from 'body-parser'; import { createRequire } from 'module'; import wgService from './services/wgService.js'; +import auth from './middleware/auth.js'; const require = createRequire(import.meta.url); @@ -15,6 +16,23 @@ app.get('/api/health', (_req, res) => { res.json({ ok: true }); }); +// Auth +app.post('/api/login', (req, res) => { + const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; + const { password } = req.body || {}; + if (!ADMIN_PASSWORD) { + return res.status(500).json({ error: '未配置 ADMIN_PASSWORD 环境变量' }); + } + if (password !== ADMIN_PASSWORD) { + return res.status(401).json({ error: '密码错误' }); + } + const token = auth.issueToken(); + res.json({ token }); +}); + +// 保护以下接口 +app.use('/api', auth.verifyToken); + // Server config app.get('/api/server/config', async (req, res) => { try { @@ -114,6 +132,16 @@ app.post('/api/clients/generate', async (req, res) => { usePresharedKey, }); + // 生成后自动写入服务端Peer + await wgService.appendPeerToServerConfig({ + interfaceName: iface, + clientName: name, + clientPublicKey: result.publicKey, + clientAddress: result.address, + presharedKey: result.presharedKey, + allowedIps, + }); + res.json(result); } catch (err) { res.status(500).json({ error: String(err?.message || err) }); diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js new file mode 100644 index 0000000..9f07401 --- /dev/null +++ b/backend/src/middleware/auth.js @@ -0,0 +1,25 @@ +import jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET || 'CHANGE_ME_DEV_ONLY'; +const EXPIRES_IN = '8h'; + +function issueToken() { + return jwt.sign({ typ: 'admin' }, JWT_SECRET, { expiresIn: EXPIRES_IN }); +} + +function verifyToken(req, res, next) { + if (req.path === '/login' || req.path === '/health') return next(); + const header = req.headers['authorization'] || ''; + const token = header.startsWith('Bearer ') ? header.slice(7) : null; + if (!token) return res.status(401).json({ error: '未认证' }); + try { + jwt.verify(token, JWT_SECRET); + next(); + } catch (e) { + return res.status(401).json({ error: '令牌无效或已过期' }); + } +} + +export default { issueToken, verifyToken }; + + diff --git a/backend/src/services/wgService.js b/backend/src/services/wgService.js index 626d8b6..0aa8388 100644 --- a/backend/src/services/wgService.js +++ b/backend/src/services/wgService.js @@ -4,7 +4,7 @@ import { promisify } from 'util'; import path from 'path'; import os from 'os'; import { fileURLToPath } from 'url'; -import { parseServerConfig, pickNextClientIp } from '../utils/configUtils.js'; +import { parseServerConfig, pickNextClientIp, appendPeerText } from '../utils/configUtils.js'; const exec = promisify(execCb); @@ -143,6 +143,21 @@ async function generateClientConfig({ interfaceName, clientName, clientAddress, }; } +async function appendPeerToServerConfig({ interfaceName, clientName, clientPublicKey, clientAddress, presharedKey, allowedIps }) { + const cfgPath = resolveConfigPath(interfaceName); + const serverConfigText = await readServerConfig(interfaceName); + const peerText = appendPeerText({ + clientName, + clientPublicKey, + clientAddress, + presharedKey, + allowedIps, + }); + const updated = serverConfigText.trimEnd() + "\n\n" + peerText; + await writeServerConfig(interfaceName, updated); + return { path: cfgPath }; +} + export default { readServerConfig, writeServerConfig, @@ -151,6 +166,7 @@ export default { generateKeyPair, generatePresharedKey, generateClientConfig, + appendPeerToServerConfig, }; diff --git a/backend/src/utils/configUtils.js b/backend/src/utils/configUtils.js index d52121b..2cd54b0 100644 --- a/backend/src/utils/configUtils.js +++ b/backend/src/utils/configUtils.js @@ -96,4 +96,16 @@ export function pickNextClientIp(parsed) { return null; } +export function appendPeerText({ clientName, clientPublicKey, clientAddress, presharedKey, allowedIps }) { + const nameComment = clientName ? `# ${clientName}` : ''; + const lines = [ + nameComment, + '[Peer]', + `PublicKey = ${clientPublicKey}`, + presharedKey ? `PresharedKey = ${presharedKey}` : null, + `AllowedIPs = ${clientAddress || allowedIps}`, + ].filter(Boolean); + return lines.join('\n') + '\n'; +} + diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 70f46fd..9ca6b27 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,5 +1,13 @@ @@ -59,6 +70,31 @@ import { ref, watch, onMounted, nextTick } from 'vue'; import axios from 'axios'; import QRCode from 'qrcode'; +const token = ref(localStorage.getItem('token') || ''); +const password = ref(''); + +function setAuth(tokenVal) { + if (tokenVal) { + axios.defaults.headers.common['Authorization'] = `Bearer ${tokenVal}`; + } else { + delete axios.defaults.headers.common['Authorization']; + } +} +setAuth(token.value); + +async function login() { + const { data } = await axios.post('/api/login', { password: password.value }); + token.value = data.token; + localStorage.setItem('token', token.value); + setAuth(token.value); +} + +function logout() { + token.value = ''; + localStorage.removeItem('token'); + setAuth(''); +} + const iface = ref('wg0'); const serverConfig = ref(''); const status = ref(''); @@ -140,6 +176,8 @@ textarea { width: 100%; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco .qrcode { margin-top: 12px; } button { cursor: pointer; } input[type="text"], input:not([type]) { width: 100%; } +.login { border: 1px solid #eee; padding: 16px; border-radius: 8px; margin-bottom: 24px; } +.tip { color: #888; font-size: 12px; }