fix: resolve 404 on server creation and 500 on client creation and reposition buttons to tables

This commit is contained in:
xtcnet 2026-03-08 10:39:17 +07:00
parent 5f4acb755e
commit 3960d6025f
3 changed files with 237 additions and 37 deletions

View file

@ -233,7 +233,9 @@ const internalWireguard = {
* Create a new WireGuard client
*/
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
const privateKey = await wgHelpers.generatePrivateKey();
@ -241,7 +243,7 @@ const internalWireguard = {
const preSharedKey = await wgHelpers.generatePreSharedKey();
// 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 ipv4Address = wgHelpers.findNextAvailableIP(iface.ipv4_cidr, allocatedIPs);
@ -255,6 +257,7 @@ const internalWireguard = {
allowed_ips: data.allowed_ips || WG_DEFAULT_ALLOWED_IPS,
persistent_keepalive: data.persistent_keepalive || WG_DEFAULT_PERSISTENT_KEEPALIVE,
expires_at: data.expires_at || null,
interface_id: iface.id,
created_on: knex.fn.now(),
modified_on: knex.fn.now(),
};
@ -356,6 +359,122 @@ const internalWireguard = {
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
*/

View file

@ -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
* List all WireGuard clients with live status

View file

@ -63,6 +63,7 @@ function WireGuard() {
const toggleClient = useToggleWgClient();
const [clientFilter, setClientFilter] = useState("");
const [serverFilter, setServerFilter] = useState("");
if (clientsLoading || ifacesLoading) {
return <Loading />;
@ -76,6 +77,14 @@ function WireGuard() {
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
const handleNewServer = async () => {
const result = (await EasyModal.show(WireGuardServerModal)) as any;
@ -147,30 +156,6 @@ function WireGuard() {
WireGuard VPN
</h2>
</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>
@ -197,15 +182,30 @@ function WireGuard() {
{activeTab === "clients" && (
<div className="table-responsive">
<div className="p-3 border-bottom d-flex align-items-center justify-content-between">
<h3 className="card-title mb-0">Clients</h3>
<input
type="text"
className="form-control form-control-sm"
placeholder="Filter clients..."
value={clientFilter}
onChange={(e) => setClientFilter(e.target.value)}
style={{ width: 250 }}
/>
<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
type="text"
className="form-control form-control-sm"
placeholder="Search clients..."
value={clientFilter}
onChange={(e) => setClientFilter(e.target.value)}
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>
<table className="table table-vcenter table-nowrap card-table">
<thead>
@ -327,6 +327,31 @@ function WireGuard() {
{activeTab === "servers" && (
<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">
<thead>
<tr>
@ -340,7 +365,7 @@ function WireGuard() {
</tr>
</thead>
<tbody>
{interfaces?.map((iface) => (
{filteredInterfaces?.map((iface) => (
<tr key={iface.id}>
<td className="fw-bold">{iface.name}</td>
<td>
@ -358,7 +383,7 @@ function WireGuard() {
<td>
<div className="d-flex align-items-center">
<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">
({interfaces.filter(i => iface.linkedServers.includes(i.id)).map(i => i.name).join(", ")})
</span>