WIP: Wireguard web dashboard #1

Draft
Sirherobrine23 wants to merge 4 commits from nextjs into main
17 changed files with 227 additions and 854 deletions
Showing only changes of commit a4a58beae0 - Show all commits

36
app/clients.tsx Normal file

@ -0,0 +1,36 @@
"use client";
import DynamicInput from "../component/DynamicInput";
import { CreateInterface, CreatePeer, DeleteInterface } from "./wg";
export interface CreateInterfaceProps {
WGcustomName: boolean;
randomIp: string
}
export async function WGInterfaceForm({ WGcustomName, randomIp }: CreateInterfaceProps) {
return <form action={(e) => CreateInterface(e)}>
{WGcustomName && <div><span>Interface name: </span><DynamicInput type="text" name="name" /></div>}
<div>
{/* <span>IPv4 Address: </span> */}
<DynamicInput width="10px" type="text" name="ipv4" required placeholder="IPv4 address, example: 10.0.0.1" defaultValue={randomIp} />
</div>
<DynamicInput type="submit" value="Create interface" />
</form>;
}
export interface WDDeleteInterfaceProps {
idInterface: number;
}
export function WDDeleteInterface({ idInterface }: WDDeleteInterfaceProps) {
return <form action={DeleteInterface}>
<DynamicInput type="hidden" name="interfaceid" defaultValue={idInterface} />
<DynamicInput type="submit" value="Delete interface" />
</form>;
}
export function WgCreatePeer() {
return <form action={CreatePeer}>
<DynamicInput type="text" />
</form>
}

0
app/global.css Normal file

19
app/layout.tsx Normal file

@ -0,0 +1,19 @@
import "./global.css";
const {
title = "WIreguard dashboard",
description = "Maneger wireguard interface from Web"
} = process.env;
export const metadata = { title, description };
export default async function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<div className="bodyCamp">
{children}
</div>
</body>
</html>
)
}

15
app/not-found.tsx Normal file

@ -0,0 +1,15 @@
import Link from "next/link";
export default function NotFound() {
return <div style={{
display: "grid",
justifyContent: "space-around",
alignContent: "space-evenly",
justifyItems: "center",
}}>
<h1>Oops page not found</h1>
<p>
<Link href="/"><h2>Home</h2></Link>
</p>
</div>;
}

52
app/page.tsx Normal file

@ -0,0 +1,52 @@
"use server";
import { wg_interfaces, wg_peer } from "@/database";
import { WDDeleteInterface, WGInterfaceForm, WgCreatePeer } from "./clients";
import { key_experimental } from "wireguard-tools.js";
import DynamicInput from "../component/DynamicInput";
import { randomIp } from "@sirherobrine23/extends/src/net";
export default async function Home() {
const wgInterfaces = (await wg_interfaces.findAll()).map(s => s.toJSON());
const wgPeers = (await wg_peer.findAll()).map(s => s.toJSON());
const customName = process.platform === "win32" || process.platform === "linux" || process.platform === "android";
return <div>
<div>
<h1>Create interface</h1>
<WGInterfaceForm WGcustomName={customName} randomIp={await randomIp("10.0.0.0/8")} />
</div>
<hr />
<div>
{wgInterfaces.map(wg => {
const peers = wgPeers.filter(s => s.interfaceID === wg.interfaceID);
return <div id={String.prototype.concat("wgInterface_", String(wg.interfaceID))}>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div>
<h1>{wg.name}</h1>
</div>
<div>
<WDDeleteInterface idInterface={wg.interfaceID} />
</div>
</div>
<div style={{ margin: "12px" }}>
<div>
{wg.privateKey && <div><DynamicInput placeholder="Private key" type="text" disabled defaultValue={wg.privateKey} /></div>}
{wg.portListen && <div><DynamicInput placeholder="Port listen" type="number" defaultValue={wg.portListen} /></div>}
{wg.ipv4 && <div><DynamicInput placeholder="IPv4" type="text" defaultValue={wg.ipv4} /></div>}
{wg.ipv6 && <div><DynamicInput placeholder="IPv6" type="text" defaultValue={wg.ipv6} /></div>}
</div>
<div>
<WgCreatePeer />
</div>
{peers.length > 0 && <div>
{peers.map((peer, pIndex) => {
return <div key={String.prototype.concat("peer_", String(wg.interfaceID), "_", String(peer.peerID))}>
<span>{key_experimental.publicKey(peer.privateKey)}</span>
</div>;
})}
</div>}
</div>
</div>
})}
</div>
</div>;
}

34
app/wg.tsx Normal file

@ -0,0 +1,34 @@
"use server";
import { key_experimental } from "wireguard-tools.js";
import { extendNet } from "@sirherobrine23/extends";
import { revalidatePath } from "next/cache";
import { wg_interfaces, wg_peer } from "@/database";
import { randomBytes } from "crypto";
import { randomBigint } from "@sirherobrine23/extends/src/crypto";
export async function CreateInterface(form: FormData) {
const ports = (await wg_interfaces.findAll({ attributes: [ "portListen" ] })).map(s => s.toJSON().portListen);
let port: number;
do {
port = Number(await randomBigint(1, 25565));
} while (ports.includes(port));
await wg_interfaces.create({
name: randomBytes(4).toString("hex"),
portListen: port,
privateKey: await key_experimental.privateKey(),
ipv4: form.get("ipv4").toString(),
ipv6: extendNet.toString(extendNet.toInt(form.get("ipv4").toString()), true),
});
revalidatePath("/");
}
export async function DeleteInterface(form: FormData) {
await wg_peer.destroy({ where: { interfaceID: Number(form.get("interfaceid")) } });
await wg_interfaces.destroy({ where: { interfaceID: Number(form.get("interfaceid")) } });
revalidatePath("/");
}
export async function CreatePeer(form: FormData) {
revalidatePath("/");
}

@ -0,0 +1,26 @@
.resizeContainer, .resizeText {
margin: 0;
}
.resizeContainer {
padding: 2px 0px;
display: inline-block;
position: relative;
}
.resizeText {
display: inline-block;
visibility: hidden;
white-space: pre;
padding: 2px 0px 4px 0px;
min-width: 180px;
}
.resizeInput {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
}

@ -0,0 +1,19 @@
"use client";
import { DetailedHTMLProps, InputHTMLAttributes } from "react";
import CssMd from "./DynamicInput.module.css";
export interface DynamicInputProps extends DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> {}
export default DynamicInput;
export function DynamicInput(props: DynamicInputProps) {
return <div className={CssMd["input-label"]}>
{ props.placeholder && <span>{props.placeholder}: </span> }
<div className={CssMd["resizeContainer"]}>
<span className={CssMd["resizeText"]}>{props.defaultValue||props.value}</span>
<input {...props} className={CssMd["resizeInput"]}
onInput={e => (e.currentTarget && e.currentTarget.previousElementSibling) && (e.currentTarget.previousElementSibling.textContent = e.currentTarget.value)}
onChange={e => (e.currentTarget && e.currentTarget.previousElementSibling) && (e.currentTarget.previousElementSibling.textContent = e.currentTarget.value)}
/>
</div>
</div>;
}

@ -1,181 +0,0 @@
function gf(init?: number[]) {
var r = new Float64Array(16);
if (init) {
for (var i = 0; i < init.length; ++i)
r[i] = init[i];
}
return r;
}
function pack(o, n) {
var b, m = gf(), t = gf();
for (var i = 0; i < 16; ++i)
t[i] = n[i];
carry(t);
carry(t);
carry(t);
for (var j = 0; j < 2; ++j) {
m[0] = t[0] - 0xffed;
for (var i = 1; i < 15; ++i) {
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
m[i - 1] &= 0xffff;
}
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
b = (m[15] >> 16) & 1;
m[14] &= 0xffff;
cswap(t, m, 1 - b);
}
for (var i = 0; i < 16; ++i) {
o[2 * i] = t[i] & 0xff;
o[2 * i + 1] = t[i] >> 8;
}
}
function carry(o) {
var c;
for (var i = 0; i < 16; ++i) {
o[(i + 1) % 16] += (i < 15 ? 1 : 38) * Math.floor(o[i] / 65536);
o[i] &= 0xffff;
}
}
function cswap(p, q, b) {
var t, c = ~(b - 1);
for (var i = 0; i < 16; ++i) {
t = c & (p[i] ^ q[i]);
p[i] ^= t;
q[i] ^= t;
}
}
function add(o, a, b) {
for (var i = 0; i < 16; ++i)
o[i] = (a[i] + b[i]) | 0;
}
function subtract(o, a, b) {
for (var i = 0; i < 16; ++i)
o[i] = (a[i] - b[i]) | 0;
}
function multmod(o, a, b) {
var t = new Float64Array(31);
for (var i = 0; i < 16; ++i) {
for (var j = 0; j < 16; ++j)
t[i + j] += a[i] * b[j];
}
for (var i = 0; i < 15; ++i)
t[i] += 38 * t[i + 16];
for (var i = 0; i < 16; ++i)
o[i] = t[i];
carry(o);
carry(o);
}
function invert(o, i) {
var c = gf();
for (var a = 0; a < 16; ++a)
c[a] = i[a];
for (var a = 253; a >= 0; --a) {
multmod(c, c, c);
if (a !== 2 && a !== 4)
multmod(c, c, i);
}
for (var a = 0; a < 16; ++a)
o[a] = c[a];
}
function clamp(z) {
z[31] = (z[31] & 127) | 64;
z[0] &= 248;
}
export function generatePublicKey(privateKey: Uint8Array) {
var r, z = new Uint8Array(32);
var a = gf([1]),
b = gf([9]),
c = gf(),
d = gf([1]),
e = gf(),
f = gf(),
_121665 = gf([0xdb41, 1]),
_9 = gf([9]);
for (var i = 0; i < 32; ++i)
z[i] = privateKey[i];
clamp(z);
for (var i = 254; i >= 0; --i) {
r = (z[i >>> 3] >>> (i & 7)) & 1;
cswap(a, b, r);
cswap(c, d, r);
add(e, a, c);
subtract(a, a, c);
add(c, b, d);
subtract(b, b, d);
multmod(d, e, e);
multmod(f, a, a);
multmod(a, c, a);
multmod(c, b, e);
add(e, a, c);
subtract(a, a, c);
multmod(b, a, a);
subtract(c, d, f);
multmod(a, c, _121665);
add(a, a, d);
multmod(c, c, a);
multmod(a, d, f);
multmod(d, b, _9);
multmod(b, e, e);
cswap(a, b, r);
cswap(c, d, r);
}
invert(c, c);
multmod(a, a, c);
pack(z, a);
return z;
}
export function generatePresharedKey() {
var privateKey = new Uint8Array(32);
window.crypto.getRandomValues(privateKey);
return privateKey;
}
export function generatePrivateKey() {
var privateKey = generatePresharedKey();
clamp(privateKey);
return privateKey;
}
function encodeBase64(dest, src) {
var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63]);
for (var i = 0; i < 4; ++i)
dest[i] = input[i] + 65 +
(((25 - input[i]) >> 8) & 6) -
(((51 - input[i]) >> 8) & 75) -
(((61 - input[i]) >> 8) & 15) +
(((62 - input[i]) >> 8) & 3);
}
export function keyToBase64(key: Uint8Array): string {
var i, base64 = new Uint8Array(44);
for (i = 0; i < 32 / 3; ++i) encodeBase64(base64.subarray(i * 4), key.subarray(i * 3));
encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0]));
base64[43] = 61;
return String.fromCharCode.apply(null, base64);
}
export function Base64ToKey(key: string): Uint8Array {
var binaryString = atob(key);
var bytes = new Uint8Array(binaryString.length);
for (var i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
return bytes;
}
export function generateKeypair() {
var privateKey = generatePrivateKey();
var publicKey = generatePublicKey(privateKey);
return {
publicKey: keyToBase64(publicKey),
privateKey: keyToBase64(privateKey)
};
}

@ -1,5 +1,12 @@
/** @type {import("next").NextConfig} */
export default {
experimental: {
serverComponentsExternalPackages: [
"sequelize",
"sqlite3",
"wireguard-tools.js"
],
},
webpack(webpackConfig, { webpack }) {
webpackConfig.resolve.extensionAlias = {
".js": [".ts", ".tsx", ".js", ".jsx"],

@ -6,19 +6,20 @@
"dev": "next dev"
},
"devDependencies": {
"@types/node": "^20.10.5",
"@types/node": "^20.11.5",
"@types/react": "18.2.46",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"dependencies": {
"@sirherobrine23/extends": "^3.7.1",
"@sirherobrine23/extends": "^3.7.4",
"neste": "^3.1.2",
"next": "^14.0.4",
"next": "^14.1.0",
"react": "^18.2.0",
"react-charts": "^3.0.0-beta.57",
"react-dom": "^18.2.0",
"sequelize": "^6.35.2",
"sqlite3": "^5.1.6",
"wireguard-tools.js": "^1.8.1"
"sqlite3": "^5.1.7",
"wireguard-tools.js": "^1.8.2"
}
}

@ -1,102 +0,0 @@
a,
h1,
h2,
h3,
h4,
h5,
h6,
div,
span,
select,
option,
button
{
font-family: "IdreesIncMonocraft";
color: #000000db;
font-weight: 500;
}
input[type="text"],
input[type="buttom"]
{
border-radius: 5px;
margin: 2px;
}
code {
font-family: "IdreesIncMonocraft";
background-color: #000000db;
color: #f0ece2;
padding: 0.2rem;
border-radius: 0.2rem;
}
select {
width: 100%;
min-width: 15ch;
max-width: 25vmax;
border: 1px solid #777;
border-radius: 0.25em;
padding: 0.25em 0.5em;
cursor: pointer;
line-height: 1.1;
background-color: #fff;
background-image: linear-gradient(to top, #f9f9f9, #fff 33%);
}
body {
background-color: #ababab;
color: #213e3b68;
margin: 0;
}
h1, h2, h3, h4, h5, h6 {
color: #14274e;
}
h3 {
color: #7579e7;
}
a {
color: #3862d4;
}
@media (prefers-color-scheme: dark) {
a,
h1,
h2,
h3,
h4,
h5,
h6,
div,
span,
select,
option,
button {
color: #f0ece2;
}
button {
background-color: #fff;
background-image: linear-gradient(to top, #f9f9f9, #fff 33%);
color: #0a0a0a;
}
body {
background-color: #000000e8;
color: #f0ece2;
}
h1 {
color: #7579e7;
}
h3 {
color: #69779b;
}
img {
filter: brightness(0.9);
}
code {
background-color: #ffffffde;
color: #000000db;
}
}

@ -1,23 +0,0 @@
import { AppProps } from "next/app";
import Head from "next/head";
import { useEffect, useState } from "react";
import "./_app.css";
export default function App({ Component, pageProps }: AppProps) {
// Render on front side not back
const [ front, setFront ] = useState(false);
useEffect(() => {
if (!front) setFront(true);
}, []);
if (!front) return <> Loading </>;
// Render default page style
return <>
<Head>
<title>Wireguard Dashboard</title>
</Head>
<div className="bodyCamp">
<Component {...pageProps}/>
</div>
</>
}

@ -1,275 +0,0 @@
import { extendNet } from "@sirherobrine23/extends";
import * as neste from "neste";
import { tmpdir } from "node:os";
import * as wg from "wireguard-tools.js";
import * as db from "../../lib/database.js";
const app = new neste.Router({ "app path": "/api" });
export default app;
export const config = {
api: {
bodyParser: false,
responseLimit: false,
}
};
app.get("/favicon(.*)", (_, res) => res.sendStatus(404));
// Parse body
app.use(neste.parseBody({ formEncoded: { limit: Infinity, limitResponse: true }, formData: { tmpFolder: tmpdir(), } }));
// Gen keys
app.get("/keys", async (_, res) => res.json(await wg.key.genKey(true)));
// Get all interfaces
app.get("/interfaces", async (_, res) => {
const inter = (await db.wg_interfaces.findAll()).map<db.Wg & { peers: db.PeerWg[] }>(s => Object.assign(s.toJSON()));
for (const ind in inter) {
const { interfaceID } = inter[ind];
inter[ind].peers = (await db.wg_peer.findAll({ where: { interfaceID } })).map(s => s.toJSON());
}
return res.json(inter);
});
// Create interface
app.post("/interfaces", async ({body: { interfaceName, portListen = 0, ipv4 }}, res) => {
const data = await db.wg_interfaces.create({
privateKey: await wg.key.privateKey(),
name: interfaceName,
portListen: parseInt(portListen),
ipv4,
ipv6: extendNet.toString(extendNet.toInt(ipv4), true),
});
return res.status(201).json(data.dataValues);
});
// Update interface
app.patch("/:id", async ({params: {id}, body: { interfaceName, portListen, ipv4, ipv6 } = {}}, res) => {
const inter = await db.wg_interfaces.findOne({ where: { interfaceID: parseInt(id) } });
if (!inter) return res.status(404).json({ errors: [ { type: "interface", message: "Interface not configured" } ] });
if (interfaceName) inter.setDataValue("name", interfaceName);
if (portListen) inter.setDataValue("portListen", parseInt(portListen));
if (ipv4) {
inter.setDataValue("ipv4", ipv4);
inter.setDataValue("ipv6", extendNet.toString(extendNet.toInt(ipv4), true));
}
if (ipv6) inter.setDataValue("ipv6", ipv6);
await inter.validate();
await inter.save();
return res.status(200).json(inter.toJSON());
});
// Deploy interface
app.put("/:id", async ({params: {id}}, res) => {
const inter = await db.wg_interfaces.findOne({ where: { interfaceID: parseInt(id) } });
if (!inter) return res.status(404).json({ errors: [ { type: "interface", message: "Interface not configured" } ] });
const interfaceConfig: wg.WgConfig = {
replacePeers: true,
portListen: inter.portListen,
privateKey: inter.privateKey,
Address: [
String().concat(inter.ipv4, "/32"),
String().concat(inter.ipv6, "/128"),
],
peers: {}
};
// Insert peers
const peers = await db.wg_peer.findAll({ where: { interfaceID: inter.interfaceID } });
for (const peer of peers) {
interfaceConfig.peers[await wg.key.publicKey(peer.privateKey)] = {
presharedKey: peer.presharedKey,
allowedIPs: [
String().concat(peer.ipv4, "/32"),
// String().concat(peer.ipv6, "/128"),
]
}
}
await wg.setConfig(inter.name, interfaceConfig);
return res.status(201).json(interfaceConfig);
});
// Delete interface
app.delete("/:id", async ({params: {id}}, res) => {
const inter = await db.wg_interfaces.findOne({ where: { interfaceID: parseInt(id) } });
if (!inter) return res.status(404).json({ errors: [ { type: "interface", message: "Interface not configured" } ] });
if ((await wg.listDevices()).some(s => s.name === inter.name)) await wg.deleteInterface(inter.name);
const peers = await db.wg_peer.findAll({ where: { interfaceID: inter.interfaceID } });
await Promise.all(peers.map(async s => s.destroy()));
await inter.destroy();
return res.status(202).json({
interface: inter.toJSON(),
peers: peers.map(s => s.toJSON()),
});
});
// Get peers
app.get("/:id", async ({params: {id}}, res) => {
const inter = await db.wg_interfaces.findOne({ where: { interfaceID: parseInt(id) } });
if (!inter) return res.status(404).json({ errors: [ { type: "interface", message: "Interface not configured" } ] });
return res.status(200).json(await db.wg_peer.findAll({ where: { interfaceID: inter.interfaceID } }));
});
// Deploy interface
app.get("/:id/config", async ({query, params: {id}}, res) => {
const inter = await db.wg_interfaces.findOne({ where: { interfaceID: parseInt(id) } });
if (!inter) return res.status(404).json({ errors: [ { type: "interface", message: "Interface not configured" } ] });
const interfaceConfig: wg.WgConfig = {
replacePeers: true,
portListen: inter.portListen,
privateKey: inter.privateKey,
Address: [
String().concat(inter.ipv4, "/32"),
String().concat(inter.ipv6, "/128"),
],
peers: {}
};
// Insert peers
const peers = await db.wg_peer.findAll({ where: { interfaceID: inter.interfaceID } });
for (const peer of peers) {
interfaceConfig.peers[await wg.key.publicKey(peer.privateKey)] = {
presharedKey: peer.presharedKey,
allowedIPs: [
String().concat(peer.ipv4, "/32"),
String().concat(peer.ipv6, "/128"),
]
}
}
if (query.type === "quick" || query.type === "wg") return res.set("Content-Type", "text/plain").send(wg.wgQuick.stringify(interfaceConfig));
else if (query.type === "yaml" || query.type === "yml") return res.yaml(interfaceConfig);
return res.json(interfaceConfig);
});
// Get random IPs
app.get("/:id/peer/ips", async ({params: {id}}, res) => {
const inter = await db.wg_interfaces.findOne({ where: { interfaceID: parseInt(id) } });
if (!inter) return res.status(404).json({ errors: [ { type: "interface", message: "Interface not configured" } ] });
let ipv4: string = null;
do {
ipv4 = await extendNet.randomIp(inter.ipv4.concat("/24"), (await db.wg_peer.findAll({ where: { interfaceID: inter.interfaceID } })).map(s => s.ipv4))
if (await db.wg_peer.findOne({ where: { ipv4 } })) ipv4 = null;
} while (!ipv4);
return res.json({
ipv4,
ipv6: extendNet.toString(extendNet.toInt(ipv4), true)
});
});
// Create peers
app.post("/:id/peer", async ({params: {id}, query, body}, res) => {
const inter = await db.wg_interfaces.findOne({ where: { interfaceID: parseInt(id) } });
if (!inter) return res.status(404).json({ errors: [ { type: "interface", message: "Interface not configured" } ] });
if (!(body && body.privateKey)) {
const genpre = query.preshared !== undefined ? query.preshared === "true" : true;
const ipv4 = await extendNet.randomIp(inter.ipv4.concat("/24"), (await db.wg_peer.findAll({ where: { interfaceID: inter.interfaceID } })).map(s => s.ipv4));
const autoPeer = await db.wg_peer.create({
interfaceID: inter.interfaceID,
privateKey: await wg.key.privateKey(),
presharedKey: genpre ? await wg.key.presharedKey() : null,
ipv4: ipv4,
ipv6: extendNet.toString(extendNet.toInt(ipv4), true)
});
return res.status(201).json(autoPeer.toJSON());
}
const userPeer = await db.wg_peer.create({
interfaceID: inter.interfaceID,
privateKey: body.privateKey,
presharedKey: body.presharedKey,
ipv4: body.ipv4,
ipv6: body.ipv6 || extendNet.toString(extendNet.toInt(body.ipv4), true)
});
return res.status(201).json(userPeer.toJSON());
});
// Delete peer v2
app.delete("/:id/peer", async ({body, params: {id}}, res) => {
const inter = await db.wg_interfaces.findOne({ where: { interfaceID: parseInt(id) } });
if (!inter) return res.status(404).json({ errors: [ { type: "interface", message: "Interface not configured" } ] });
if (!(Array.isArray(body) || typeof body?.peerID === "string")) return res.status(400).json({ erros: [ { type: "body", message: "require Array or `peerID` in body" } ] });
// Multi delete
if (Array.isArray(body)) {
const destroys = await db.wg_peer.findAll({ where: { interfaceID: inter.interfaceID, peerID: body.map(Number) } });
await Promise.all(destroys.map(s => s.destroy()));
return res.status(202).json(destroys.map(s => s.toJSON()));
}
// Delete peer
const peer = await db.wg_peer.findOne({ where: { peerID: Number(body.peerID) } });
await peer.destroy();
return res.status(202).json(peer.toJSON());
});
// Delete peer
app.delete("/:id/:peerID", async ({params: { id, peerID }, body}, res) => {
const inter = await db.wg_interfaces.findOne({ where: { interfaceID: parseInt(id) } });
if (!inter) return res.status(404).json({ errors: [ { type: "interface", message: "Interface not configured" } ] });
const peer = await db.wg_peer.findOne({ where: { interfaceID: inter.interfaceID, peerID: parseInt(peerID) } });
if (!peer) return res.status(404).json({ errors: [ { type: "peer", message: "peer not exists" } ] });
await peer.destroy();
return res.status(202).json(peer.toJSON());
});
// Edit peer
app.patch("/:id/:peerID", async ({params: { id, peerID }, body}, res) => {
const inter = await db.wg_interfaces.findOne({ where: { interfaceID: parseInt(id) } });
if (!inter) return res.status(404).json({ errors: [ { type: "interface", message: "Interface not configured" } ] });
const peer = await db.wg_peer.findOne({ where: { interfaceID: inter.interfaceID, peerID: parseInt(peerID) } });
if (!peer) return res.status(404).json({ errors: [ { type: "peer", message: "peer not exists" } ] });
if (body && Object.keys(body).length > 0) {
if (body.presharedKey) peer.setDataValue("presharedKey", body.presharedKey);
if (body.ipv4) {
peer.setDataValue("ipv4", body.ipv4);
peer.setDataValue("ipv6", extendNet.toString(extendNet.toInt(body.ipv4), true));
}
}
await peer.validate();
await peer.save();
return res.status(200).json(peer.toJSON());
});
// Create peer config
app.get("/:id/:peerID/config", async ({query, headers, params: { id, peerID }}, res) => {
const inter = await db.wg_interfaces.findOne({ where: { interfaceID: parseInt(id) } });
if (!inter) return res.status(404).json({ errors: [ { type: "interface", message: "Interface not configured" } ] });
const peer = await db.wg_peer.findOne({ where: { interfaceID: inter.interfaceID, peerID: parseInt(peerID) } });
if (!peer) return res.status(404).json({ errors: [ { type: "peer", message: "Peer not exists" } ] });
const peerConfig: wg.wgQuick.QuickConfig = {
privateKey: peer.privateKey,
Address: [
String().concat(peer.ipv4, "/32"),
// String().concat(peer.ipv6, "/128"),
],
DNS: [
"1.1.1.1",
"8.8.8.8",
"1.0.0.1",
"8.8.4.4"
],
peers: {
[await wg.key.publicKey(inter.privateKey)]: {
endpoint: String().concat((query.endpoint||headers.host.slice(0, headers.host.indexOf(":"))), ":", String(inter.portListen)),
presharedKey: peer.presharedKey,
allowedIPs: [
"0.0.0.0/0",
"::/0"
],
}
}
};
if (query.type === "quick" || query.type === "wg") return res.set("Content-Type", "text/plain").send(wg.wgQuick.stringify(peerConfig));
else if (query.type === "yaml" || query.type === "yml") return res.yaml(peerConfig);
return res.json(peerConfig);
});
app.use((_req, res, _next) => res.status(404).json({ error: [ { type: "api", message: "endpoint not exists!" } ] }));
app.useError((err, _req, res, _next) => {
console.error(err);
return res.status(500).json(err);
});

@ -1,28 +0,0 @@
.interface {
padding: 12px;
}
.interface > .inName {
font-size: 3.5ch;
}
.PeerContainer {
flex-wrap: wrap;
justify-content: space-around;
justify-items: stretch;
}
.Peer {
padding: 12px;
margin: 6px;
margin-top: 12px;
width: 410px;
backdrop-filter: invert(6%);
flex: 1 1 490px;
}
@media screen and (min-width: 600px) {
.PeerContainer {
display: flex;
}
}

@ -1,238 +0,0 @@
import { CSSProperties, DetailedHTMLProps, FormEvent, InputHTMLAttributes, useState } from "react";
import type { PeerWg, Wg } from "../lib/database.js";
import * as key from "../lib/key.js";
import HomeStyle from "./index.module.css";
import { GetServerSidePropsContext, InferGetServerSidePropsType, PreviewData } from "next";
import { ParsedUrlQuery } from "querystring";
function DynamicInput(props: DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>) {
const
tbase = {
margin: 0,
},
resizeContainer: CSSProperties = {
...tbase,
padding: "2px 0px",
display: "inline-block",
position: "relative"
},
resizeText: CSSProperties = {
...tbase,
display: "inline-block",
visibility: "hidden",
whiteSpace: "pre",
padding: "2px 0px 4px 0px",
minWidth: "40px",
},
resizeInput: CSSProperties = {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
margin: 0
};
return <div className="resizeContainer" style={resizeContainer}>
<span style={resizeText}>{props.defaultValue||props.value}</span>
<input {...props} style={{...props.style, ...resizeInput}}
onInput={e => e.currentTarget.previousElementSibling.textContent = e.currentTarget.value}
onChange={e => e.currentTarget.previousElementSibling.textContent = e.currentTarget.value}
/>
</div>;
}
interface CreatePeerProps {
interfaceID: number;
done(): void;
}
function CreatePeer(prop: CreatePeerProps) {
async function submit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
const body = new FormData(e.currentTarget);
const res = await fetch(String.prototype.concat("/api/", String(prop.interfaceID), "/peer"), {
method: "POST",
body
});
if (res.ok) prop.done();
}
const [genPresharedKey, updaterPresharedKey] = useState(false);
const [ips, setIps] = useState<{ipv4: string, ipv6: string}>(null);
const [{ privateKey, publicKey }, genKey] = useState(key.generateKeypair());
const [presharedKey, UpdatePresharedKey] = useState(key.keyToBase64(key.generatePresharedKey()));
return <div className={HomeStyle["Peer"]}>
<div style={{ paddingBottom: "5px", display: "flex" }}>
<div>
<input type="button" style={{ backgroundColor: "red" }} onClick={prop.done} value="Cancel" />
</div>
<div>
<input type="button" onClick={async () => {
try {
const res = await fetch(String.prototype.concat("/api/", String(prop.interfaceID), "/peer/ips"));
if (res.ok) return setIps(await res.json());
} catch {
}
}} value="Get Random IP" />
</div>
<div>
<input type="button" onClick={() => genKey(key.generateKeypair())} value="Recrate key" />
</div>
{
genPresharedKey && <div>
<input type="button" onClick={() => UpdatePresharedKey(key.keyToBase64(key.generatePresharedKey()))} value="Recrate preshared key" />
</div>
}
</div>
<div>
<form onSubmit={submit}>
<div>
Private Key:
<DynamicInput type="text" name="privateKey" defaultValue={privateKey} value={privateKey} />
</div>
<div>
Public Key:
<DynamicInput readOnly type="text" name="" defaultValue={publicKey} value={publicKey} />
</div>
<div>
Include Preshared key:
<input type="checkbox" onClick={e => updaterPresharedKey(e.currentTarget.checked)} defaultChecked={genPresharedKey} />
</div>
{genPresharedKey && <div>
Preshared Key:
<DynamicInput type="text" name="presharedKey" defaultValue={presharedKey} value={presharedKey} />
</div>}
<div>
IPv4:
<DynamicInput readOnly type="text" name="ipv4" value={ips?.ipv4} />
</div>
<div>
IPv6:
<DynamicInput readOnly type="text" name="ipv6" value={ips?.ipv6} />
</div>
<div>
<input type="submit" value="Create peer" />
</div>
</form>
</div>
</div>;
}
export const getServerSideProps = async (req: GetServerSidePropsContext<ParsedUrlQuery, PreviewData>) => {
// console.log(process.env);
const res = await fetch(`http://${req.req.headers.host}/api/interfaces`);
if (!res.ok) return { noFound: true };
return {
props: {
interface: await res.json() as any,
}
};
}
export default function Home(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
const [ interfaces, setInterface ] = useState<(Wg & { peers: PeerWg[] })[]>(props.interface);
const [ peersCreate, UpdatePeerCreate ] = useState<{interfaceID: number, id: string}[]>([]);
const updateFunc = async () => {
try {
const res = await fetch("/api/interfaces");
setInterface(await res.json() as any);
} catch (err) {
}
}
return <div>
<div>
<button onClick={updateFunc}>Refresh</button>
<button>Create interface</button>
<div>
{
interfaces && interfaces.map((wg) => {
return <div key={String.prototype.concat("interfaceHome", String(wg.interfaceID))} className={HomeStyle["interface"]}>
<div className={HomeStyle["inName"]} style={{ display: "flex", justifyContent: "space-between" }}>
<div>{wg.name}</div>
<div>
<button>Delete interface</button>
</div>
</div>
<hr/>
<div style={{marginLeft: "1%"}}>
{wg.privateKey && <div>Private key: {wg.privateKey}</div>}
{wg.ipv4 && <div>Interface IPv4: {wg.ipv4}</div>}
{wg.ipv6 && <div>Interface IPv6: {wg.ipv6}</div>}
{wg.portListen && <div>Port Listen: <input type="number" min={0} max={65535} defaultValue={wg.portListen} onChange={e => {
if (e.currentTarget.valueAsNumber > 65535) e.currentTarget.value = String((e.currentTarget.valueAsNumber = 65535));
else if (e.currentTarget.valueAsNumber < 0) e.currentTarget.value = String((e.currentTarget.valueAsNumber = 0));
}}/></div>}
<div style={{paddingTop: "5px"}}>
<button onClick={() => UpdatePeerCreate(e => e.concat({ interfaceID: wg.interfaceID, id: crypto.randomUUID() }))}>Create peer</button>
<button onClick={async () => {
const { ok } = await fetch(String.prototype.concat("/api/", String(wg.interfaceID), "/peer"), { method: "POST" });
if (ok) await updateFunc();
}}>Create random peer</button>
{wg.peers.length > 1 && <button onClick={async () => {
const { ok } = await fetch(String.prototype.concat("/api/", String(wg.interfaceID), "/peer"), {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(wg.peers.map(s => s.peerID)),
});
if (ok) await updateFunc();
}}>Delete all peers</button>}
{peersCreate.filter(s => s.interfaceID === wg.interfaceID).length > 0 && <div>
Peers to create:
<div style={{marginLeft: "2%", marginRight: "2%"}} className={HomeStyle["PeerContainer"]}>
{peersCreate.filter(s => s.interfaceID === wg.interfaceID).map(({id: peerID}) =>
<CreatePeer interfaceID={wg.interfaceID} done={() => {
UpdatePeerCreate(e => e.filter(({id}) => id !== peerID))
updateFunc();
}
} />
)}
</div>
</div>}
{
wg.peers.length > 0 && <div>
<div style={{ paddingTop: "2px" }}>Peers:</div>
<div style={{marginLeft: "2%", marginRight: "2%"}} className={HomeStyle["PeerContainer"]}>
{wg.peers.reverse().map(peer => {
async function deletePeer() {
try {
const res = await fetch(("/api/").concat(String(wg.interfaceID), "/", String(peer.peerID)), {
method: "DELETE",
});
if (res.ok) setInterface(e => e.map(s => { s.peers = s.peers.filter(s => peer.peerID !== s.peerID); return s; }));
} catch {
}
}
return <div className={HomeStyle["Peer"]} key={String.prototype.concat("peerHome", String(wg.interfaceID), "peer", String(peer.peerID))}>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div>
{key.keyToBase64(key.generatePublicKey(key.Base64ToKey(peer.privateKey)))}
</div>
<div>
<button onClick={deletePeer}>Delete peer</button>
</div>
</div>
<hr/>
<div style={{marginLeft: "2%", paddingTop: "8px"}}>
{peer.presharedKey && <div>Preshared Key: <span>{peer.presharedKey}</span></div>}
{peer.ipv4 && <div>Peer IPv4: <span>{peer.ipv4}</span></div>}
{peer.ipv6 && <div>Peer IPv6: <span>{peer.ipv6}</span></div>}
</div>
</div>;
})}
</div>
</div>
}
</div>
</div>
</div>;
})
}
</div>
</div>
</div>;
}

@ -21,12 +21,23 @@
"forceConsistentCasingInFileNames": true,
"noUnusedLocals": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": [
"./lib/*"
]
},
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"