update
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) });
|
||||||
|
|||||||
@@ -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 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user