diff --git a/backend/routes/wireguard.js b/backend/routes/wireguard.js index e6c876a..ea7f1ab 100644 --- a/backend/routes/wireguard.js +++ b/backend/routes/wireguard.js @@ -1,4 +1,5 @@ import express from "express"; +import archiver from "archiver"; import internalWireguard from "../internal/wireguard.js"; import internalAuditLog from "../internal/audit-log.js"; import jwtdecode from "../lib/express/jwt-decode.js"; @@ -274,4 +275,36 @@ router.get("/client/:id/qrcode.svg", async (req, res, next) => { } }); +/** + * GET /api/wireguard/client/:id/configuration.zip + * Download WireGuard client configuration as a ZIP archive + */ +router.get("/client/:id/configuration.zip", async (req, res, next) => { + try { + const knex = db(); + const client = await knex("wg_client").where("id", req.params.id).first(); + if (!client) { + return res.status(404).json({ error: { message: "Client not found" } }); + } + + const configStr = await internalWireguard.getClientConfiguration(knex, req.params.id); + const svgStr = await internalWireguard.getClientQRCode(knex, req.params.id); + const safeName = client.name.replace(/[^a-zA-Z0-9_.-]/g, "-").substring(0, 32); + + res.set("Content-Disposition", `attachment; filename="${safeName}.zip"`); + res.set("Content-Type", "application/zip"); + + const archive = archiver("zip", { zlib: { level: 9 } }); + archive.on("error", (err) => next(err)); + archive.pipe(res); + + archive.append(configStr, { name: `${safeName}.conf` }); + archive.append(svgStr, { name: `${safeName}-qrcode.svg` }); + + await archive.finalize(); + } catch (err) { + next(err); + } +}); + export default router; diff --git a/frontend/src/api/backend/wireguard.ts b/frontend/src/api/backend/wireguard.ts index d023773..a3b9d4f 100644 --- a/frontend/src/api/backend/wireguard.ts +++ b/frontend/src/api/backend/wireguard.ts @@ -79,3 +79,7 @@ export async function getWgClientConfig(id: number): Promise { export function downloadWgConfig(id: number, name: string) { return api.download({ url: `/wireguard/client/${id}/configuration` }, `${name}.conf`); } + +export function downloadWgConfigZip(id: number, name: string) { + return api.download({ url: `/wireguard/client/${id}/configuration.zip` }, `${name}.zip`); +} diff --git a/frontend/src/pages/WireGuard/index.tsx b/frontend/src/pages/WireGuard/index.tsx index 1769699..fc5e27c 100644 --- a/frontend/src/pages/WireGuard/index.tsx +++ b/frontend/src/pages/WireGuard/index.tsx @@ -9,10 +9,11 @@ import { IconServer, IconEdit, IconLink, + IconZip, } from "@tabler/icons-react"; import EasyModal from "ez-modal-react"; import { useState } from "react"; -import { downloadWgConfig } from "src/api/backend/wireguard"; +import { downloadWgConfig, downloadWgConfigZip } from "src/api/backend/wireguard"; import { Loading } from "src/components"; import { useWgClients, @@ -154,6 +155,11 @@ function WireGuard() { downloadWgConfig(id, cleanName); }; + const handleDownloadZip = (id: number, name: string) => { + const cleanName = name.replace(/[^a-zA-Z0-9_.-]/g, "-").substring(0, 32); + downloadWgConfigZip(id, cleanName); + }; + return (
{/* Page Header */} @@ -411,6 +417,16 @@ function WireGuard() { > +