This commit is contained in:
AnranYus
2025-09-10 17:27:53 +08:00
parent 29ac1c1581
commit 8534441d59
6 changed files with 122 additions and 2 deletions
+2 -1
View File
@@ -10,7 +10,8 @@
"dependencies": { "dependencies": {
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.19.2" "express": "^4.19.2",
"jsonwebtoken": "^9.0.2"
} }
} }
+28
View File
@@ -3,6 +3,7 @@ import cors from 'cors';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import { createRequire } from 'module'; import { createRequire } from 'module';
import wgService from './services/wgService.js'; import wgService from './services/wgService.js';
import auth from './middleware/auth.js';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
@@ -15,6 +16,23 @@ app.get('/api/health', (_req, res) => {
res.json({ ok: true }); 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 // Server config
app.get('/api/server/config', async (req, res) => { app.get('/api/server/config', async (req, res) => {
try { try {
@@ -114,6 +132,16 @@ app.post('/api/clients/generate', async (req, res) => {
usePresharedKey, usePresharedKey,
}); });
// 生成后自动写入服务端Peer
await wgService.appendPeerToServerConfig({
interfaceName: iface,
clientName: name,
clientPublicKey: result.publicKey,
clientAddress: result.address,
presharedKey: result.presharedKey,
allowedIps,
});
res.json(result); res.json(result);
} catch (err) { } catch (err) {
res.status(500).json({ error: String(err?.message || err) }); res.status(500).json({ error: String(err?.message || err) });
+25
View File
@@ -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 };
+17 -1
View File
@@ -4,7 +4,7 @@ import { promisify } from 'util';
import path from 'path'; import path from 'path';
import os from 'os'; import os from 'os';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { parseServerConfig, pickNextClientIp } from '../utils/configUtils.js'; import { parseServerConfig, pickNextClientIp, appendPeerText } from '../utils/configUtils.js';
const exec = promisify(execCb); 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 { export default {
readServerConfig, readServerConfig,
writeServerConfig, writeServerConfig,
@@ -151,6 +166,7 @@ export default {
generateKeyPair, generateKeyPair,
generatePresharedKey, generatePresharedKey,
generateClientConfig, generateClientConfig,
appendPeerToServerConfig,
}; };
+12
View File
@@ -96,4 +96,16 @@ export function pickNextClientIp(parsed) {
return null; 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';
}
+38
View File
@@ -1,5 +1,13 @@
<template> <template>
<div class="container"> <div class="container">
<div v-if="!token" class="login">
<h2>登录</h2>
<div class="row">
<input v-model="password" placeholder="管理员密码" type="password" />
<button @click="login">登录</button>
</div>
<p class="tip">后端需设置环境变量 ADMIN_PASSWORD JWT_SECRET生产</p>
</div>
<h1>WireGuard 管理面板</h1> <h1>WireGuard 管理面板</h1>
<section class="server"> <section class="server">
<h2>服务端配置</h2> <h2>服务端配置</h2>
@@ -51,6 +59,9 @@
<canvas ref="qrCanvas"></canvas> <canvas ref="qrCanvas"></canvas>
</div> </div>
</section> </section>
<div class="row">
<button @click="logout">退出登录</button>
</div>
</div> </div>
</template> </template>
@@ -59,6 +70,31 @@ import { ref, watch, onMounted, nextTick } from 'vue';
import axios from 'axios'; import axios from 'axios';
import QRCode from 'qrcode'; 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 iface = ref('wg0');
const serverConfig = ref(''); const serverConfig = ref('');
const status = ref(''); const status = ref('');
@@ -140,6 +176,8 @@ textarea { width: 100%; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco
.qrcode { margin-top: 12px; } .qrcode { margin-top: 12px; }
button { cursor: pointer; } button { cursor: pointer; }
input[type="text"], input:not([type]) { width: 100%; } 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; }
</style> </style>