fix: resolve 404 on server creation and 500 on client creation and reposition buttons to tables
This commit is contained in:
parent
5f4acb755e
commit
3960d6025f
3 changed files with 237 additions and 37 deletions
|
|
@ -233,7 +233,9 @@ const internalWireguard = {
|
||||||
* Create a new WireGuard client
|
* Create a new WireGuard client
|
||||||
*/
|
*/
|
||||||
async createClient(knex, data) {
|
async createClient(knex, data) {
|
||||||
const iface = await this.getOrCreateInterface(knex);
|
const iface = data.interface_id
|
||||||
|
? await knex("wg_interface").where("id", data.interface_id).first()
|
||||||
|
: await this.getOrCreateInterface(knex);
|
||||||
|
|
||||||
// Generate keys
|
// Generate keys
|
||||||
const privateKey = await wgHelpers.generatePrivateKey();
|
const privateKey = await wgHelpers.generatePrivateKey();
|
||||||
|
|
@ -241,7 +243,7 @@ const internalWireguard = {
|
||||||
const preSharedKey = await wgHelpers.generatePreSharedKey();
|
const preSharedKey = await wgHelpers.generatePreSharedKey();
|
||||||
|
|
||||||
// Allocate IP
|
// Allocate IP
|
||||||
const existingClients = await knex("wg_client").select("ipv4_address");
|
const existingClients = await knex("wg_client").select("ipv4_address").where("interface_id", iface.id);
|
||||||
const allocatedIPs = existingClients.map((c) => c.ipv4_address);
|
const allocatedIPs = existingClients.map((c) => c.ipv4_address);
|
||||||
const ipv4Address = wgHelpers.findNextAvailableIP(iface.ipv4_cidr, allocatedIPs);
|
const ipv4Address = wgHelpers.findNextAvailableIP(iface.ipv4_cidr, allocatedIPs);
|
||||||
|
|
||||||
|
|
@ -255,6 +257,7 @@ const internalWireguard = {
|
||||||
allowed_ips: data.allowed_ips || WG_DEFAULT_ALLOWED_IPS,
|
allowed_ips: data.allowed_ips || WG_DEFAULT_ALLOWED_IPS,
|
||||||
persistent_keepalive: data.persistent_keepalive || WG_DEFAULT_PERSISTENT_KEEPALIVE,
|
persistent_keepalive: data.persistent_keepalive || WG_DEFAULT_PERSISTENT_KEEPALIVE,
|
||||||
expires_at: data.expires_at || null,
|
expires_at: data.expires_at || null,
|
||||||
|
interface_id: iface.id,
|
||||||
created_on: knex.fn.now(),
|
created_on: knex.fn.now(),
|
||||||
modified_on: knex.fn.now(),
|
modified_on: knex.fn.now(),
|
||||||
};
|
};
|
||||||
|
|
@ -356,6 +359,122 @@ const internalWireguard = {
|
||||||
return wgHelpers.generateQRCodeSVG(config);
|
return wgHelpers.generateQRCodeSVG(config);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new WireGuard Interface Endpoint
|
||||||
|
*/
|
||||||
|
async createInterface(knex, data) {
|
||||||
|
const existingIfaces = await knex("wg_interface").select("name", "listen_port");
|
||||||
|
const newIndex = existingIfaces.length;
|
||||||
|
|
||||||
|
const name = `wg${newIndex}`;
|
||||||
|
const listen_port = 51820 + newIndex;
|
||||||
|
|
||||||
|
// Attempt to grab /24 subnets, ex 10.8.0.0/24 -> 10.8.1.0/24
|
||||||
|
const ipv4_cidr = `10.8.${newIndex}.1/24`;
|
||||||
|
|
||||||
|
// Generate keys
|
||||||
|
const privateKey = await wgHelpers.generatePrivateKey();
|
||||||
|
const publicKey = await wgHelpers.getPublicKey(privateKey);
|
||||||
|
|
||||||
|
const insertData = {
|
||||||
|
name,
|
||||||
|
private_key: privateKey,
|
||||||
|
public_key: publicKey,
|
||||||
|
listen_port,
|
||||||
|
ipv4_cidr,
|
||||||
|
ipv6_cidr: null,
|
||||||
|
mtu: data.mtu || WG_DEFAULT_MTU,
|
||||||
|
dns: data.dns || WG_DEFAULT_DNS,
|
||||||
|
host: data.host || WG_HOST,
|
||||||
|
isolate_clients: data.isolate_clients || false,
|
||||||
|
post_up: "iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE",
|
||||||
|
post_down: "iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE",
|
||||||
|
created_on: knex.fn.now(),
|
||||||
|
modified_on: knex.fn.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const [id] = await knex("wg_interface").insert(insertData);
|
||||||
|
|
||||||
|
const newIface = await knex("wg_interface").where("id", id).first();
|
||||||
|
|
||||||
|
// Regenerate config and restart the new interface seamlessly
|
||||||
|
const parsed = wgHelpers.parseCIDR(newIface.ipv4_cidr);
|
||||||
|
let configContent = wgHelpers.generateServerInterface({
|
||||||
|
privateKey: newIface.private_key,
|
||||||
|
address: `${parsed.firstHost}/${parsed.prefix}`,
|
||||||
|
listenPort: newIface.listen_port,
|
||||||
|
mtu: newIface.mtu,
|
||||||
|
dns: null,
|
||||||
|
postUp: newIface.post_up,
|
||||||
|
postDown: newIface.post_down,
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(`${WG_CONFIG_DIR}/${name}.conf`, configContent, { mode: 0o600 });
|
||||||
|
await wgHelpers.wgUp(name);
|
||||||
|
|
||||||
|
return newIface;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing Interface
|
||||||
|
*/
|
||||||
|
async updateInterface(knex, id, data) {
|
||||||
|
const iface = await knex("wg_interface").where("id", id).first();
|
||||||
|
if (!iface) throw new Error("Interface not found");
|
||||||
|
|
||||||
|
const updateData = { modified_on: knex.fn.now() };
|
||||||
|
if (data.host !== undefined) updateData.host = data.host;
|
||||||
|
if (data.dns !== undefined) updateData.dns = data.dns;
|
||||||
|
if (data.mtu !== undefined) updateData.mtu = data.mtu;
|
||||||
|
if (data.isolate_clients !== undefined) updateData.isolate_clients = data.isolate_clients;
|
||||||
|
|
||||||
|
await knex("wg_interface").where("id", id).update(updateData);
|
||||||
|
|
||||||
|
await this.saveConfig(knex); // This will re-render IPTables and sync
|
||||||
|
return knex("wg_interface").where("id", id).first();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an interface
|
||||||
|
*/
|
||||||
|
async deleteInterface(knex, id) {
|
||||||
|
const iface = await knex("wg_interface").where("id", id).first();
|
||||||
|
if (!iface) throw new Error("Interface not found");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await wgHelpers.wgDown(iface.name);
|
||||||
|
if (fs.existsSync(`${WG_CONFIG_DIR}/${iface.name}.conf`)) {
|
||||||
|
fs.unlinkSync(`${WG_CONFIG_DIR}/${iface.name}.conf`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Failed to teardown WG interface ${iface.name}: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cascading deletion handles clients and links in DB schema
|
||||||
|
await knex("wg_interface").where("id", id).del();
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Peering Links between WireGuard Interfaces
|
||||||
|
*/
|
||||||
|
async updateInterfaceLinks(knex, id, linkedServers) {
|
||||||
|
// Clean up existing links where this interface is involved
|
||||||
|
await knex("wg_server_link").where("interface_id_1", id).orWhere("interface_id_2", id).del();
|
||||||
|
|
||||||
|
// Insert new ones
|
||||||
|
for (const peerId of linkedServers) {
|
||||||
|
if (peerId !== Number(id)) {
|
||||||
|
await knex("wg_server_link").insert({
|
||||||
|
interface_id_1: id,
|
||||||
|
interface_id_2: peerId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.saveConfig(knex);
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the WireGuard interfaces info
|
* Get the WireGuard interfaces info
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,62 @@ router.get("/", async (_req, res, next) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/wireguard
|
||||||
|
* Create a new WireGuard interface
|
||||||
|
*/
|
||||||
|
router.post("/", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const knex = db();
|
||||||
|
const iface = await internalWireguard.createInterface(knex, req.body);
|
||||||
|
res.status(201).json(iface);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/wireguard/:id
|
||||||
|
* Update a WireGuard interface
|
||||||
|
*/
|
||||||
|
router.put("/:id", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const knex = db();
|
||||||
|
const iface = await internalWireguard.updateInterface(knex, req.params.id, req.body);
|
||||||
|
res.status(200).json(iface);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/wireguard/:id
|
||||||
|
* Delete a WireGuard interface
|
||||||
|
*/
|
||||||
|
router.delete("/:id", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const knex = db();
|
||||||
|
const result = await internalWireguard.deleteInterface(knex, req.params.id);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/wireguard/:id/links
|
||||||
|
* Update peering links for a WireGuard interface
|
||||||
|
*/
|
||||||
|
router.post("/:id/links", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const knex = db();
|
||||||
|
const result = await internalWireguard.updateInterfaceLinks(knex, req.params.id, req.body.linked_servers || []);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/wireguard/client
|
* GET /api/wireguard/client
|
||||||
* List all WireGuard clients with live status
|
* List all WireGuard clients with live status
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ function WireGuard() {
|
||||||
const toggleClient = useToggleWgClient();
|
const toggleClient = useToggleWgClient();
|
||||||
|
|
||||||
const [clientFilter, setClientFilter] = useState("");
|
const [clientFilter, setClientFilter] = useState("");
|
||||||
|
const [serverFilter, setServerFilter] = useState("");
|
||||||
|
|
||||||
if (clientsLoading || ifacesLoading) {
|
if (clientsLoading || ifacesLoading) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
|
|
@ -76,6 +77,14 @@ function WireGuard() {
|
||||||
c.interfaceName?.toLowerCase().includes(clientFilter.toLowerCase()),
|
c.interfaceName?.toLowerCase().includes(clientFilter.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const filteredInterfaces = interfaces?.filter(
|
||||||
|
(i) =>
|
||||||
|
!serverFilter ||
|
||||||
|
i.name.toLowerCase().includes(serverFilter.toLowerCase()) ||
|
||||||
|
i.ipv4Cidr.includes(serverFilter) ||
|
||||||
|
(i.host && i.host.toLowerCase().includes(serverFilter.toLowerCase()))
|
||||||
|
);
|
||||||
|
|
||||||
// Server Handlers
|
// Server Handlers
|
||||||
const handleNewServer = async () => {
|
const handleNewServer = async () => {
|
||||||
const result = (await EasyModal.show(WireGuardServerModal)) as any;
|
const result = (await EasyModal.show(WireGuardServerModal)) as any;
|
||||||
|
|
@ -147,30 +156,6 @@ function WireGuard() {
|
||||||
WireGuard VPN
|
WireGuard VPN
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-auto ms-auto d-print-none">
|
|
||||||
<div className="btn-list">
|
|
||||||
{activeTab === "servers" ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary d-none d-sm-inline-block"
|
|
||||||
onClick={handleNewServer}
|
|
||||||
>
|
|
||||||
<IconPlus size={16} className="me-1" />
|
|
||||||
New Server
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary d-none d-sm-inline-block"
|
|
||||||
onClick={handleNewClient}
|
|
||||||
id="wg-new-client-btn"
|
|
||||||
>
|
|
||||||
<IconPlus size={16} className="me-1" />
|
|
||||||
New Client
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -197,15 +182,30 @@ function WireGuard() {
|
||||||
{activeTab === "clients" && (
|
{activeTab === "clients" && (
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
<div className="p-3 border-bottom d-flex align-items-center justify-content-between">
|
<div className="p-3 border-bottom d-flex align-items-center justify-content-between">
|
||||||
<h3 className="card-title mb-0">Clients</h3>
|
<div className="d-flex w-100 flex-column flex-md-row justify-content-between align-items-center">
|
||||||
|
<div className="text-muted d-none d-md-block">
|
||||||
|
Listing WireGuard Clients
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-wrap gap-2 justify-content-md-end w-100 w-md-auto align-items-center">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
placeholder="Filter clients..."
|
placeholder="Search clients..."
|
||||||
value={clientFilter}
|
value={clientFilter}
|
||||||
onChange={(e) => setClientFilter(e.target.value)}
|
onChange={(e) => setClientFilter(e.target.value)}
|
||||||
style={{ width: 250 }}
|
style={{ width: 250 }}
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
onClick={handleNewClient}
|
||||||
|
id="wg-new-client-btn"
|
||||||
|
>
|
||||||
|
<IconPlus size={16} className="me-1" />
|
||||||
|
New Client
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table className="table table-vcenter table-nowrap card-table">
|
<table className="table table-vcenter table-nowrap card-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -327,6 +327,31 @@ function WireGuard() {
|
||||||
|
|
||||||
{activeTab === "servers" && (
|
{activeTab === "servers" && (
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
|
<div className="p-3 border-bottom d-flex align-items-center justify-content-between">
|
||||||
|
<div className="d-flex w-100 flex-column flex-md-row justify-content-between align-items-center">
|
||||||
|
<div className="text-muted d-none d-md-block">
|
||||||
|
Listing WireGuard Servers
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-wrap gap-2 justify-content-md-end w-100 w-md-auto align-items-center">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
placeholder="Search servers..."
|
||||||
|
value={serverFilter}
|
||||||
|
onChange={(e) => setServerFilter(e.target.value)}
|
||||||
|
style={{ width: 250 }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
onClick={handleNewServer}
|
||||||
|
>
|
||||||
|
<IconPlus size={16} className="me-1" />
|
||||||
|
New Server
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<table className="table table-vcenter table-nowrap card-table">
|
<table className="table table-vcenter table-nowrap card-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -340,7 +365,7 @@ function WireGuard() {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{interfaces?.map((iface) => (
|
{filteredInterfaces?.map((iface) => (
|
||||||
<tr key={iface.id}>
|
<tr key={iface.id}>
|
||||||
<td className="fw-bold">{iface.name}</td>
|
<td className="fw-bold">{iface.name}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -358,7 +383,7 @@ function WireGuard() {
|
||||||
<td>
|
<td>
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<span className="badge bg-azure me-2">{iface.linkedServers?.length || 0}</span>
|
<span className="badge bg-azure me-2">{iface.linkedServers?.length || 0}</span>
|
||||||
{iface.linkedServers?.length > 0 && (
|
{iface.linkedServers?.length > 0 && interfaces && (
|
||||||
<span className="text-muted small">
|
<span className="text-muted small">
|
||||||
({interfaces.filter(i => iface.linkedServers.includes(i.id)).map(i => i.name).join(", ")})
|
({interfaces.filter(i => iface.linkedServers.includes(i.id)).map(i => i.name).join(", ")})
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue