update
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) });
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<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>
|
||||
<section class="server">
|
||||
<h2>服务端配置</h2>
|
||||
@@ -51,6 +59,9 @@
|
||||
<canvas ref="qrCanvas"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
<div class="row">
|
||||
<button @click="logout">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user