43349579e1
* feat(wireguard): WireGuard peer manager UI with QR, .conf download, and per-client exit node selection
- models/wg_peer.js — Redis-backed peer store with auto IP allocation (10.100.0.x)
- models/wg_site.js — Redis-backed exit node store (admin-managed sites)
- utils/wg_keys.js — X25519 keypair gen via Node crypto (no wg binary needed)
- utils/wg_conf.js — client wg0.conf renderer
- routes/wireguard.js — REST API: CRUD sites/peers, GET /conf, GET /qr (QRCode PNG)
- views/wireguard.ejs — full dark-mode UI: exit node table, peer table, QR modal,
.conf download, exit node picker per client
- conf/base.js — conf.wireguard block (serverPublicKey, serverEndpoint, dns, poolBase)
- Nav: WireGuard link added (admin-gated)
- No wg binary dep in Node process — key gen is pure JS X25519
* fix(test): update test script to run unit tests without native bcrypt binary dependency in CI
* fix(ui): replace native browser alert/confirm with app.messages in WireGuard view
85 lines
2.3 KiB
JavaScript
85 lines
2.3 KiB
JavaScript
'use strict';
|
|
|
|
// WireGuard exit-node / site model — raw Redis (same pattern as audit_event.js).
|
|
//
|
|
// Each "site" is a location clients can route through. Admins add/remove sites
|
|
// dynamically via the Theta Gateway UI.
|
|
//
|
|
// Redis keys:
|
|
// wg_site:<id> — hash of site fields
|
|
// wg_site_index — sorted set (score = createdAt, value = id)
|
|
|
|
const crypto = require('crypto');
|
|
const conf = require('@simpleworkjs/conf');
|
|
const { getRedis } = require('./index');
|
|
|
|
const P = () => conf.redis.prefix;
|
|
const idxKey = () => `${P()}wg_site_index`;
|
|
const siteKey = (id) => `${P()}wg_site:${id}`;
|
|
|
|
function serialize(obj) {
|
|
const out = {};
|
|
for (const [k, v] of Object.entries(obj)) {
|
|
out[k] = typeof v === 'boolean' ? (v ? '1' : '0') : String(v == null ? '' : v);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function deserialize(h) {
|
|
if (!h || !h.id) return null;
|
|
return {
|
|
...h,
|
|
createdAt: Number(h.createdAt || 0),
|
|
exitAll: h.exitAll === '1',
|
|
};
|
|
}
|
|
|
|
async function create(data, createdBy) {
|
|
const id = crypto.randomBytes(8).toString('hex');
|
|
const site = {
|
|
id,
|
|
name: data.name || 'Unnamed Site',
|
|
endpoint: data.endpoint || '',
|
|
publicKey: data.publicKey || '',
|
|
subnet: data.subnet || '0.0.0.0/0',
|
|
exitAll: !!data.exitAll,
|
|
siteId: data.siteId || '',
|
|
note: data.note || '',
|
|
createdBy: createdBy || '',
|
|
createdAt: Date.now(),
|
|
};
|
|
const redis = await getRedis();
|
|
await redis.hSet(siteKey(id), serialize(site));
|
|
await redis.zAdd(idxKey(), { score: site.createdAt, value: id });
|
|
return site;
|
|
}
|
|
|
|
async function get(id) {
|
|
const redis = await getRedis();
|
|
return deserialize(await redis.hGetAll(siteKey(id)));
|
|
}
|
|
|
|
async function update(id, patch) {
|
|
const redis = await getRedis();
|
|
const existing = await get(id);
|
|
if (!existing) throw Object.assign(new Error('Site not found'), { status: 404 });
|
|
const merged = { ...existing, ...patch, id }; // id is immutable
|
|
await redis.hSet(siteKey(id), serialize(merged));
|
|
return merged;
|
|
}
|
|
|
|
async function remove(id) {
|
|
const redis = await getRedis();
|
|
await redis.del(siteKey(id));
|
|
await redis.zRem(idxKey(), id);
|
|
}
|
|
|
|
async function list() {
|
|
const redis = await getRedis();
|
|
const ids = await redis.zRange(idxKey(), 0, -1);
|
|
const sites = await Promise.all(ids.map(get));
|
|
return sites.filter(Boolean);
|
|
}
|
|
|
|
module.exports = { create, get, update, remove, list };
|