WIP: Wireguard web dashboard #1
36
app/clients.tsx
Normal file
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
0
app/global.css
Normal file
19
app/layout.tsx
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
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
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
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("/");
|
||||
}
|
26
component/DynamicInput.module.css
Normal file
26
component/DynamicInput.module.css
Normal file
@ -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;
|
||||
}
|
19
component/DynamicInput.tsx
Normal file
19
component/DynamicInput.tsx
Normal file
@ -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>;
|
||||
}
|
181
lib/key.ts
181
lib/key.ts
@ -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"],
|
||||
|
11
package.json
11
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
102
pages/_app.css
102
pages/_app.css
@ -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;
|
||||
}
|
||||
}
|
238
pages/index.tsx
238
pages/index.tsx
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user