WIP: Wireguard web dashboard #1
1
.env
Normal file
1
.env
Normal file
@ -0,0 +1 @@
|
||||
DB_CONNECTION="sqlite:database.db"
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -9,4 +9,7 @@ src/**/*.d.ts
|
||||
src/**/*.js
|
||||
|
||||
# Project
|
||||
*.db
|
||||
*.db
|
||||
|
||||
# Nextjs
|
||||
.next/
|
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.autoSaveDelay": 2000,
|
||||
"files.exclude": {
|
||||
"**/.next/": true,
|
||||
"**/node_modules/": true,
|
||||
"**/*-lock*/": true,
|
||||
},
|
||||
}
|
71
app/layout.tsx
Normal file
71
app/layout.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { Cookies, Users } from "@db/users";
|
||||
import DataUsageIcon from "@mui/icons-material/DataUsage";
|
||||
import GroupIcon from "@mui/icons-material/Group";
|
||||
import PersonIcon from "@mui/icons-material/Person";
|
||||
import StorageIcon from "@mui/icons-material/Storage";
|
||||
import { cookies } from "next/headers";
|
||||
import Link from "next/link";
|
||||
import "./layout_global.css";
|
||||
import { LoginPage, LogoutPage } from "./login";
|
||||
import { Home } from "@mui/icons-material";
|
||||
|
||||
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 }) {
|
||||
const cookiesSessions = cookies();
|
||||
let loginPage = true;
|
||||
if (cookiesSessions.has("sessionsLogin")) {
|
||||
const ck = cookiesSessions.get("sessionsLogin").value;
|
||||
const user = await Cookies.findOne({ where: { value: ck } });
|
||||
if (user && (await Users.findOne({ where: { userID: user.userID } })).activate) loginPage = false;
|
||||
}
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<div className="container">
|
||||
<div className="navbar">
|
||||
<div className="homeDiv">
|
||||
<Home />
|
||||
<a href="/">Home</a>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<span>Main</span>
|
||||
<li>
|
||||
<PersonIcon />
|
||||
<Link href="/servers">Wireguard Servers</Link>
|
||||
</li>
|
||||
<li>
|
||||
<StorageIcon />
|
||||
<Link href="/peers">Wireguard peers</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<span>Utils</span>
|
||||
<li>
|
||||
<GroupIcon />
|
||||
<Link href="/users">Users</Link>
|
||||
</li>
|
||||
<li>
|
||||
<DataUsageIcon />
|
||||
<Link href="/stats">Status and Stats</Link>
|
||||
</li>
|
||||
</ul>
|
||||
{!loginPage && <ul>
|
||||
<LogoutPage />
|
||||
</ul>}
|
||||
</nav>
|
||||
</div>
|
||||
<div className="bodyCamp">
|
||||
{loginPage && <LoginPage />}
|
||||
{!loginPage && children}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
92
app/layout_global.css
Normal file
92
app/layout_global.css
Normal file
@ -0,0 +1,92 @@
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 800px) {
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
.navbar ul {
|
||||
display: grid;
|
||||
}
|
||||
.navbar ul > span {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 799px) {
|
||||
.navbar {
|
||||
/* display: flex; */
|
||||
align-items: baseline;
|
||||
}
|
||||
.navbar > nav {
|
||||
display: flex;
|
||||
}
|
||||
.navbar ul {
|
||||
display: flex;
|
||||
}
|
||||
.navbar ul > li {
|
||||
padding-left: 10px;
|
||||
flex-direction: column;
|
||||
}
|
||||
.navbar ul > span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: rgb(0, 19, 50);
|
||||
margin-right: 12px;
|
||||
padding-right: 8px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.navbar ul {
|
||||
padding-left: 14px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.navbar ul > span {
|
||||
background-color: grey;
|
||||
color: black;
|
||||
border-radius: 12rem;
|
||||
width: 42%;
|
||||
margin-bottom: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.navbar ul > li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.navbar ul > li > svg {
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.navbar li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.navbar a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logoutButton {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.homeDiv {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 26px;
|
||||
padding-top: 15px;
|
||||
}
|
30
app/login.tsx
Normal file
30
app/login.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
import { Login, Logout } from "./login_back";
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
|
||||
export function LoginPage() {
|
||||
return <div className="loginArea">
|
||||
<h1>Login</h1>
|
||||
<form action={Login}>
|
||||
<div>
|
||||
<label htmlFor="username">Username: </label>
|
||||
<input type="text" name="username" placeholder="Username" />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password">Password: </label>
|
||||
<input type="password" name="password" placeholder="Password" />
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" value="Login" />
|
||||
</div>
|
||||
</form>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export function LogoutPage() {
|
||||
return <form action={Logout}>
|
||||
<button>
|
||||
<LogoutIcon className="logoutButton" />
|
||||
</button>
|
||||
</form>
|
||||
}
|
34
app/login_back.ts
Normal file
34
app/login_back.ts
Normal file
@ -0,0 +1,34 @@
|
||||
"use server";
|
||||
import { Cookies, Users } from "@db/users";
|
||||
import { decrypt } from "@pass";
|
||||
import { randomBytes } from "crypto";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export async function Login(form: FormData) {
|
||||
"use server";
|
||||
const username = String(form.get("username")||""), password = String(form.get("password")||"");
|
||||
const user = await Users.findOne({ where: { username } });
|
||||
if (user) {
|
||||
if (await decrypt(user.password) === password) {
|
||||
let cookieVal: string;
|
||||
do {
|
||||
cookieVal = randomBytes(32).toString("hex");
|
||||
} while (await Cookies.findOne({ where: { value: cookieVal } }));
|
||||
|
||||
await Cookies.create({
|
||||
userID: user.userID,
|
||||
value: cookieVal
|
||||
});
|
||||
cookies().set("sessionsLogin", cookieVal);
|
||||
}
|
||||
}
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
export async function Logout() {
|
||||
const ck = cookies().get("sessionsLogin")?.value;
|
||||
const bk = await Cookies.findOne({ where: { value: ck } });
|
||||
await bk.destroy();
|
||||
redirect("/");
|
||||
}
|
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>;
|
||||
}
|
8
app/page.tsx
Normal file
8
app/page.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
export default function Home() {
|
||||
return (<div>
|
||||
<h1>Welcome to Wireguard dashboard</h1>
|
||||
<p>
|
||||
<span>Click in any options in navbar to start navigation</span>
|
||||
</p>
|
||||
</div>);
|
||||
}
|
43
app/peers/[id]/config/api/route.ts
Normal file
43
app/peers/[id]/config/api/route.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { wgServer } from "@db/server";
|
||||
import { wgPeer } from "@db/peers";
|
||||
import { key, wgQuick } from "wireguard-tools.js";
|
||||
import { isIPv6 } from "net";
|
||||
|
||||
const {
|
||||
WG_HOSTNAME = "localhost",
|
||||
} = process.env;
|
||||
|
||||
export async function GET(req: NextRequest, {params: { id }}) {
|
||||
const peer = await wgPeer.findOne({ where: { id: parseInt(id) } });
|
||||
if (!peer) return new Response(JSON.stringify({ message: "Peer not exists" }, null, 2), { status: 400, headers: { "Content-Type": "application/json" } });
|
||||
const interfaceInfo = await wgServer.findOne({ where: { id: (await peer).interfaceOwner } });
|
||||
if (!interfaceInfo) return new Response(JSON.stringify({ message: "Interface not exists" }, null, 2), { status: 400, headers: { "Content-Type": "application/json" } });
|
||||
|
||||
const type = String(req.nextUrl.searchParams.get("type")||"quick").toLowerCase();
|
||||
const wgConfig: wgQuick.QuickConfig = {
|
||||
privateKey: peer.privateKey,
|
||||
DNS: [ "8.8.8.8", "1.1.1.1", "8.8.4.4", "1.0.0.1" ],
|
||||
Address: [
|
||||
peer.IPv4,
|
||||
peer.IPv6
|
||||
],
|
||||
peers: {
|
||||
[key.publicKey(interfaceInfo.privateKey)]: {
|
||||
presharedKey: peer.presharedKey,
|
||||
endpoint: `${isIPv6(WG_HOSTNAME) ? String.prototype.concat("[", WG_HOSTNAME,"]") : WG_HOSTNAME}:${interfaceInfo.portListen}`,
|
||||
allowedIPs: ["0.0.0.0/0", "::/0"]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (type === "json") return Response.json(wgConfig);
|
||||
|
||||
return new Response(wgQuick.stringify(wgConfig), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Cotent-Type": "text/plain",
|
||||
"Content-Disposition": `inline; filename="peer_${peer.interfaceOwner}_${peer.id}.conf"`
|
||||
}
|
||||
});
|
||||
}
|
5
app/peers/api/route.ts
Normal file
5
app/peers/api/route.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { wgPeer } from "@db/peers";
|
||||
|
||||
export async function GET() {
|
||||
return Response.json(await wgPeer.findAll());
|
||||
}
|
96
app/peers/page.tsx
Normal file
96
app/peers/page.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { Peer, wgPeer } from "@db/peers";
|
||||
import { wgServer } from "@db/server";
|
||||
import { Users } from "@db/users";
|
||||
import { Download } from "@mui/icons-material";
|
||||
import { extendNet } from "@sirherobrine23/extends";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { key } from "wireguard-tools.js";
|
||||
|
||||
function PeerComponent(props: Peer) {
|
||||
return (<div>
|
||||
<hr />
|
||||
<div>
|
||||
<div>Owner: {props.owner}</div>
|
||||
<div>interface owner: {props.interfaceOwner}</div>
|
||||
<div>Private key: {props.privateKey}</div>
|
||||
{props.presharedKey && <div>Preshared key: {props.presharedKey}</div>}
|
||||
<div>IPv4: {props.IPv4}</div>
|
||||
<div>IPv6: {props.IPv6}</div>
|
||||
<a href={`peers/${props.id}/config/api?type=quick`} download>
|
||||
<Download />
|
||||
<span>Download quick config file</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
function NukePeers() {
|
||||
"use client";
|
||||
async function Nuke() {
|
||||
"use server";
|
||||
await wgPeer.destroy({ where: {} });
|
||||
redirect("/peers");
|
||||
}
|
||||
return <form action={Nuke}>
|
||||
<input type="submit" value="Nuke peers" />
|
||||
</form>
|
||||
}
|
||||
|
||||
export default async function Home() {
|
||||
const peers = (await wgPeer.findAll()).map(s => s.toJSON());
|
||||
const servers = (await wgServer.findAll({ attributes: [ "id", "name" ] })).map(s => s.toJSON());
|
||||
const owners = (await Users.findAll({ attributes: [ "username", "userID" ] })).map(s => s.toJSON());
|
||||
|
||||
async function createPeer(form: FormData) {
|
||||
"use server";
|
||||
const owner = parseInt(form.get("owner") as any), interfaceOwner = parseInt(form.get("interfaceOwner") as any), presharedKeyGen = Boolean(form.get("presharedKeyGen"));
|
||||
const interfaceInfo = await wgServer.findOne({ where: { id: interfaceOwner } });
|
||||
const base: Partial<Peer> = {
|
||||
owner,
|
||||
interfaceOwner,
|
||||
privateKey: await key.privateKey(),
|
||||
presharedKey: presharedKeyGen ? await key.presharedKey() : null,
|
||||
};
|
||||
|
||||
do {
|
||||
base.IPv4 = await extendNet.randomIp(interfaceInfo.IPv4);
|
||||
} while (await wgPeer.findOne({ where: { IPv4: base.IPv4 } }));
|
||||
base.IPv6 = extendNet.toString(extendNet.toInt(base.IPv4), true);
|
||||
await wgPeer.create(base);
|
||||
revalidatePath("/peers");
|
||||
}
|
||||
|
||||
return <div>
|
||||
<div>
|
||||
<h1>Wireguard peers</h1>
|
||||
</div>
|
||||
<div>
|
||||
<form action={createPeer}>
|
||||
<div>
|
||||
<span>Select peer owner: </span>
|
||||
<select name="owner">
|
||||
{owners.map(s => <option value={s.userID}>{s.username}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<span>Interface peer target: </span>
|
||||
<select name="interfaceOwner">
|
||||
{servers.map(s => <option value={s.id}>{s.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<span>Generate preshared key?</span>
|
||||
<input type="checkbox" defaultChecked name="presharedKeyGen" />
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" value="Create peer" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<NukePeers />
|
||||
<div>
|
||||
{peers.map(s => PeerComponent(s))}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
10
app/servers/[id]/api/route.ts
Normal file
10
app/servers/[id]/api/route.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(Request: NextRequest, {params: { id }}) {
|
||||
return NextResponse.json({
|
||||
url: Request.url,
|
||||
headers: Request.headers,
|
||||
id
|
||||
});
|
||||
}
|
42
app/servers/[id]/edit/page.tsx
Normal file
42
app/servers/[id]/edit/page.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { wgServer, Server } from "@db/server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
function FormEdit(props: Server) {
|
||||
"use client";
|
||||
async function updateDb(form: FormData) {
|
||||
"use server";
|
||||
const wgInt = await wgServer.findOne({ where: { id: props.id } });
|
||||
wgInt.name = String(form.get("name")||props.name);
|
||||
wgInt.IPv4 = String(form.get("IPv4")||props.IPv4);
|
||||
wgInt.IPv6 = String(form.get("IPv6")||props.IPv6);
|
||||
await wgInt.validate();
|
||||
await wgInt.save();
|
||||
revalidatePath(`/servers/${props.id}/edit`)
|
||||
}
|
||||
return (<form action={updateDb}>
|
||||
<div>
|
||||
Interface name: <input type="text" name="name" defaultValue={props.name} />
|
||||
</div>
|
||||
<div>
|
||||
IPv4: <input type="text" name="IPv4" defaultValue={props.IPv4} />
|
||||
</div>
|
||||
<div>
|
||||
IPv6: <input type="text" name="IPv6" defaultValue={props.IPv6} />
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "space-evenly" }}>
|
||||
<input type="submit" value="Update" />
|
||||
<a href="..">Back to home</a>
|
||||
</div>
|
||||
</form>);
|
||||
}
|
||||
|
||||
export default async function EditServer({ params: { id } }) {
|
||||
const info = await wgServer.findOne({ where: { id: parseInt(id) } });
|
||||
if (!info) return redirect("/servers");
|
||||
|
||||
return (<div>
|
||||
<h1>Editing {info.name}</h1>
|
||||
<FormEdit {...(info.toJSON())} id={id} />
|
||||
</div>);
|
||||
}
|
6
app/servers/api/route.ts
Normal file
6
app/servers/api/route.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { wgServer } from "@db/server";
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
return Response.json((await wgServer.findAll()).map(s => s.toJSON()))
|
||||
}
|
61
app/servers/page.tsx
Normal file
61
app/servers/page.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import Size from "@components/size";
|
||||
import { Server, wgServer, createInterface } from "@db/server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { key } from "wireguard-tools.js";
|
||||
|
||||
function DeleterSever({id}: Server) {
|
||||
async function deleteServer() {
|
||||
"use server";
|
||||
await (await wgServer.findOne({ where: { id } })).destroy();
|
||||
revalidatePath("/servers");
|
||||
}
|
||||
return (<form action={deleteServer}>
|
||||
<input type="submit" value="Delete interface" />
|
||||
</form>)
|
||||
}
|
||||
|
||||
function CreateSever() {
|
||||
async function deleteServer() {
|
||||
"use server";
|
||||
await createInterface();
|
||||
revalidatePath("/servers");
|
||||
}
|
||||
return (<form action={deleteServer}>
|
||||
<input type="submit" value="Create interface" />
|
||||
</form>)
|
||||
}
|
||||
|
||||
// Make server info
|
||||
function ServerShow({ serverInfo }: { serverInfo: Server }) {
|
||||
return (<div>
|
||||
<h3 id={serverInfo.id.toString()}>Interface name: {serverInfo.name}</h3>
|
||||
<hr />
|
||||
<div style={{ display: "flex", justifyContent: "space-evenly" }}>
|
||||
<DeleterSever {...serverInfo} />
|
||||
<a href={`/servers/${serverInfo.id}/edit`}>Edit interface</a>
|
||||
</div>
|
||||
<p>Private key: {serverInfo.privateKey}</p>
|
||||
<p>Public key: {key.publicKey(serverInfo.privateKey)}</p>
|
||||
{serverInfo.IPv4 && <p>IPv4: {serverInfo.IPv4}</p>}
|
||||
{serverInfo.IPv6 && <p>IPv4: "{serverInfo.IPv6}"</p>}
|
||||
<p style={{ marginLeft: "18px" }}>
|
||||
<span>Stats</span>
|
||||
<div style={{ display: "flex", justifyContent: "space-evenly" }}>
|
||||
<span>Upload: <Size fileSize={serverInfo.uploadStats}/></span>
|
||||
<span>Download: <Size fileSize={serverInfo.downloadStats}/></span>
|
||||
</div>
|
||||
</p>
|
||||
</div>);
|
||||
}
|
||||
|
||||
// Export page
|
||||
export default async function Servers() {
|
||||
const servers = (await wgServer.findAll()).map(s => s.toJSON());
|
||||
return (<div>
|
||||
<h1>Wireguard interfaces</h1>
|
||||
<CreateSever />
|
||||
<div>
|
||||
{servers.map(s => <ServerShow key={"wgInterface"+s.id.toString()} serverInfo={s}/>)}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
5
app/stats/page.tsx
Normal file
5
app/stats/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export default async function Stats() {
|
||||
return <div>
|
||||
Stats
|
||||
</div>;
|
||||
}
|
5
app/users/api/route.ts
Normal file
5
app/users/api/route.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Users } from "@db/users";
|
||||
|
||||
export async function GET() {
|
||||
return Response.json((await Users.findAll({ attributes: [ "userID", "username" ], where: { activate: true } })).map(s => s.toJSON()));
|
||||
}
|
5
app/users/page.tsx
Normal file
5
app/users/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export default async function Users() {
|
||||
return <div>
|
||||
Users
|
||||
</div>
|
||||
}
|
4
components/size.tsx
Normal file
4
components/size.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
export default Size;
|
||||
export function Size({ fileSize }: { fileSize: number }) {
|
||||
return (<span>{fileSize} B</span>);
|
||||
}
|
4
lib/database/db.ts
Normal file
4
lib/database/db.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { Sequelize } from "sequelize";
|
||||
|
||||
const { DB_CONNECTION = "sqlite:memory:" } = process.env;
|
||||
export const dbConection: Sequelize = global["DB_CONNECTION"] || (global["DB_CONNECTION"] = new Sequelize(DB_CONNECTION));
|
64
lib/database/peers.ts
Normal file
64
lib/database/peers.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { DataTypes, InferAttributes, InferCreationAttributes, Model } from "sequelize";
|
||||
import { dbConection } from "./db.js";
|
||||
|
||||
export const modelName = "wg_peer";
|
||||
export type Peer = InferAttributes<wgPeer, { omit: never; }>
|
||||
export class wgPeer extends Model<InferAttributes<wgPeer>, InferCreationAttributes<wgPeer>> {
|
||||
declare id?: number;
|
||||
interfaceOwner: number;
|
||||
owner: number;
|
||||
privateKey: string;
|
||||
presharedKey?: string;
|
||||
uploadStats: number;
|
||||
downloadStats: number;
|
||||
IPv4?: string;
|
||||
IPv6?: string;
|
||||
};
|
||||
|
||||
wgPeer.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
interfaceOwner: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
owner: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
privateKey: {
|
||||
type: DataTypes.CHAR(44),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
presharedKey: {
|
||||
type: DataTypes.CHAR(44),
|
||||
allowNull: true,
|
||||
unique: true
|
||||
},
|
||||
uploadStats: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
downloadStats: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
IPv4: {
|
||||
type: DataTypes.CHAR,
|
||||
unique: true,
|
||||
},
|
||||
IPv6: {
|
||||
type: DataTypes.CHAR,
|
||||
unique: true,
|
||||
},
|
||||
}, { sequelize: dbConection, modelName });
|
||||
|
||||
wgPeer.sync().then(async () => {
|
||||
if (await wgPeer.count() === 0) {}
|
||||
});
|
80
lib/database/server.ts
Normal file
80
lib/database/server.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import * as net_e from "@sirherobrine23/extends/src/net.js";
|
||||
import { DataTypes, InferAttributes, InferCreationAttributes, Model } from "sequelize";
|
||||
import * as wg from "wireguard-tools.js";
|
||||
import { dbConection } from "./db.js";
|
||||
import { randomBytes, randomInt } from "crypto";
|
||||
|
||||
export const modelName = "wg_servers";
|
||||
export type Server = InferAttributes<wgServer, { omit: never; }>
|
||||
export class wgServer extends Model<InferAttributes<wgServer>, InferCreationAttributes<wgServer>> {
|
||||
declare id?: number;
|
||||
name: string;
|
||||
privateKey: string;
|
||||
portListen: number;
|
||||
IPv4: string;
|
||||
IPv6: string;
|
||||
uploadStats: number;
|
||||
downloadStats: number;
|
||||
};
|
||||
|
||||
wgServer.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
privateKey: {
|
||||
type: DataTypes.CHAR(44),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
portListen: {
|
||||
type: DataTypes.INTEGER,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
set(val) {
|
||||
if (val === 0) val = randomInt(2024, 65525);
|
||||
this.setDataValue("portListen", Number(val));
|
||||
},
|
||||
},
|
||||
uploadStats: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
downloadStats: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
IPv4: {
|
||||
type: DataTypes.CHAR,
|
||||
unique: true,
|
||||
},
|
||||
IPv6: {
|
||||
type: DataTypes.CHAR,
|
||||
unique: true,
|
||||
},
|
||||
}, { sequelize: dbConection, modelName });
|
||||
|
||||
wgServer.sync().then(async () => {
|
||||
if (await wgServer.count() === 0) await createInterface();
|
||||
});
|
||||
|
||||
export async function createInterface() {
|
||||
const wgInt = new wgServer;
|
||||
wgInt.name = randomBytes(4).toString("hex");
|
||||
wgInt.privateKey = await wg.key.privateKey();
|
||||
wgInt.IPv4 = await net_e.randomIp("10.10.0.0/16");
|
||||
wgInt.IPv6 = net_e.toString(net_e.toInt(wgInt.IPv4), true);
|
||||
do { wgInt.portListen = randomInt(2024, 65525); } while (await wgServer.findOne({ where: { portListen: wgInt.portListen } }));
|
||||
await wgInt.validate();
|
||||
await wgInt.save();
|
||||
return wgInt.toJSON();
|
||||
}
|
70
lib/database/users.ts
Normal file
70
lib/database/users.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { encrypt } from "@pass";
|
||||
import { DataTypes, InferAttributes, InferCreationAttributes, Model } from "sequelize";
|
||||
import { dbConection } from "./db";
|
||||
|
||||
export type User = InferAttributes<Users, { omit: never; }>
|
||||
export class Users extends Model<InferAttributes<Users>, InferCreationAttributes<Users>> {
|
||||
userID?: number;
|
||||
activate: boolean;
|
||||
name: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export class Cookies extends Model<InferAttributes<Cookies>, InferCreationAttributes<Cookies>> {
|
||||
declare id: number;
|
||||
userID: number;
|
||||
value: string;
|
||||
};
|
||||
|
||||
Cookies.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
unique: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
userID: {
|
||||
type: DataTypes.INTEGER,
|
||||
},
|
||||
value: {
|
||||
type: DataTypes.STRING,
|
||||
unique: true
|
||||
}
|
||||
}, { sequelize: dbConection });
|
||||
|
||||
Users.init({
|
||||
userID: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
unique: true,
|
||||
},
|
||||
activate: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.CHAR(46),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
}
|
||||
}, { sequelize: dbConection });
|
||||
|
||||
Cookies.sync();
|
||||
Users.sync().then(async () => {
|
||||
(await Users.count() === 0) && await Users.create({
|
||||
activate: true,
|
||||
name: "Root admin",
|
||||
username: "admin",
|
||||
password: await encrypt("admin"),
|
||||
})
|
||||
});
|
18
lib/pass.ts
Normal file
18
lib/pass.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import crypto from "node:crypto";
|
||||
import util from "node:util";
|
||||
|
||||
const scrypt = util.promisify(crypto.scrypt) as (password: crypto.BinaryLike, salt: crypto.BinaryLike, keylen: number) => Promise<Buffer>;
|
||||
|
||||
export async function encrypt(text: string): Promise<string> {
|
||||
const iv = crypto.randomBytes(16), secret = crypto.randomBytes(24);
|
||||
const key = await scrypt(secret, "salt", 24);
|
||||
const cipher = crypto.createCipheriv("aes-192-cbc", key, iv); cipher.on("error", () => {});
|
||||
return (String()).concat(iv.toString("hex"), secret.toString("hex"), cipher.update(text, "utf8", "hex"), cipher.final("hex"));
|
||||
}
|
||||
|
||||
export async function decrypt(hash: string): Promise<string> {
|
||||
const iv = Buffer.from(hash.substring(0, 32), "hex"), secret = Buffer.from(hash.substring(32, 80), "hex"), cipher = hash.substring(80);
|
||||
const key = await scrypt(secret, "salt", 24);
|
||||
const decipher = crypto.createDecipheriv("aes-192-cbc", key, iv); decipher.on("error", () => {});
|
||||
return (String()).concat(decipher.update(cipher, "hex", "utf8"), decipher.final("utf8"));
|
||||
}
|
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
18
next.config.mjs
Normal file
18
next.config.mjs
Normal file
@ -0,0 +1,18 @@
|
||||
/** @type {import("next").NextConfig} */
|
||||
export default {
|
||||
experimental: {
|
||||
serverComponentsExternalPackages: [
|
||||
"sequelize",
|
||||
"sqlite3",
|
||||
"wireguard-tools.js"
|
||||
],
|
||||
},
|
||||
webpack(webpackConfig, { webpack }) {
|
||||
webpackConfig.resolve.extensionAlias = {
|
||||
".js": [".ts", ".tsx", ".js", ".jsx"],
|
||||
".mjs": [".mts", ".mjs"],
|
||||
".cjs": [".cts", ".cjs"],
|
||||
};
|
||||
return webpackConfig;
|
||||
},
|
||||
};
|
28
package.json
28
package.json
@ -1,25 +1,31 @@
|
||||
{
|
||||
"name": "wg-dashboard",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.5",
|
||||
"@types/node": "^20.11.16",
|
||||
"@types/react": "18.2.46",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sirherobrine23/extends": "^3.7.1",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.7",
|
||||
"@mui/material": "^5.15.7",
|
||||
"@sirherobrine23/extends": "^3.7.4",
|
||||
"neste": "^3.1.2",
|
||||
"sequelize": "^6.35.2",
|
||||
"sqlite3": "^5.1.6",
|
||||
"wireguard-tools.js": "^1.8.1"
|
||||
"next": "^14.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-charts": "^3.0.0-beta.57",
|
||||
"react-dom": "^18.2.0",
|
||||
"sequelize": "^6.36.0",
|
||||
"sqlite3": "^5.1.7",
|
||||
"wireguard-tools.js": "^1.8.3"
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
161
src/database.ts
161
src/database.ts
@ -1,161 +0,0 @@
|
||||
import path from "node:path";
|
||||
import sequelize from "sequelize";
|
||||
|
||||
export enum UserType {
|
||||
root = "wg::root",
|
||||
user = "wg::user"
|
||||
}
|
||||
|
||||
export interface User {
|
||||
userID: string;
|
||||
username: string;
|
||||
password: string;
|
||||
type?: UserType;
|
||||
}
|
||||
|
||||
/** Interface struct */
|
||||
export interface Wg {
|
||||
interfaceID: number;
|
||||
name: string;
|
||||
privateKey: string;
|
||||
portListen: number;
|
||||
ipv4: string;
|
||||
ipv6: string|null;
|
||||
}
|
||||
|
||||
/** Peer base struct */
|
||||
export interface PeerWg {
|
||||
interfaceID: number;
|
||||
peerID: number;
|
||||
privateKey: string;
|
||||
presharedKey?: string;
|
||||
ipv4: string;
|
||||
ipv6: string|null;
|
||||
}
|
||||
|
||||
export const databasePath = path.join(process.cwd(), "database.db");
|
||||
export const db = new sequelize.Sequelize({
|
||||
dialect: "sqlite",
|
||||
storage: databasePath,
|
||||
logging(sql, _timing) {
|
||||
console.error(sql);
|
||||
},
|
||||
});
|
||||
|
||||
export interface UserSequelize extends sequelize.Model<sequelize.InferAttributes<UserSequelize>, sequelize.InferCreationAttributes<UserSequelize>>, User { };
|
||||
export interface WgSequelize extends sequelize.Model<sequelize.InferAttributes<WgSequelize>, sequelize.InferCreationAttributes<WgSequelize>>, Wg { };
|
||||
export interface PeerWgSequelize extends sequelize.Model<sequelize.InferAttributes<PeerWgSequelize>, sequelize.InferCreationAttributes<PeerWgSequelize>>, PeerWg { };
|
||||
|
||||
export const user = db.define<UserSequelize>("user", {
|
||||
userID: {
|
||||
type: sequelize.DataTypes.STRING,
|
||||
autoIncrement: false,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
username: {
|
||||
type: sequelize.DataTypes.STRING,
|
||||
autoIncrement: false,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
password: {
|
||||
type: sequelize.DataTypes.STRING,
|
||||
autoIncrement: false,
|
||||
allowNull: false,
|
||||
unique: false,
|
||||
},
|
||||
type: {
|
||||
type: sequelize.DataTypes.STRING,
|
||||
defaultValue: UserType.user,
|
||||
autoIncrement: false,
|
||||
allowNull: false,
|
||||
validate: {
|
||||
is: [UserType.root, UserType.user]
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const wg_interfaces = db.define<WgSequelize>("wg", {
|
||||
interfaceID: {
|
||||
type: sequelize.DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
name: {
|
||||
type: sequelize.DataTypes.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
},
|
||||
privateKey: {
|
||||
type: sequelize.DataTypes.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
},
|
||||
portListen: {
|
||||
type: sequelize.DataTypes.NUMBER,
|
||||
defaultValue: 0,
|
||||
allowNull: false,
|
||||
set(val: number) {
|
||||
if (isNaN(val)) throw new TypeError("Invalid port listen!");
|
||||
else if (!(isFinite(val))) throw new TypeError("Invalid port listen!");
|
||||
else if (val < 0) throw new TypeError("Invalid port listen!");
|
||||
this.setDataValue("portListen", val);
|
||||
},
|
||||
},
|
||||
ipv4: {
|
||||
type: sequelize.DataTypes.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
set(val: string) {
|
||||
this.setDataValue("ipv4", String(val).split("/")[0]);
|
||||
},
|
||||
},
|
||||
ipv6: {
|
||||
type: sequelize.DataTypes.STRING,
|
||||
unique: true,
|
||||
allowNull: true,
|
||||
set(val: string) {
|
||||
this.setDataValue("ipv6", String(val).split("/")[0]);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const wg_peer = db.define<PeerWgSequelize>("peers", {
|
||||
interfaceID: {
|
||||
type: sequelize.DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
peerID: {
|
||||
type: sequelize.DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
privateKey: {
|
||||
type: sequelize.DataTypes.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
},
|
||||
presharedKey: {
|
||||
type: sequelize.DataTypes.STRING,
|
||||
unique: true,
|
||||
allowNull: true,
|
||||
},
|
||||
ipv4: {
|
||||
type: sequelize.DataTypes.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
},
|
||||
ipv6: {
|
||||
type: sequelize.DataTypes.STRING,
|
||||
unique: true,
|
||||
allowNull: true,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
user.sync(),
|
||||
wg_interfaces.sync(),
|
||||
wg_peer.sync(),
|
||||
]);
|
226
src/index.ts
226
src/index.ts
@ -1,226 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { extendNet } from "@sirherobrine23/extends";
|
||||
import * as neste from "neste";
|
||||
import { tmpdir } from "node:os";
|
||||
import wg from "wireguard-tools.js";
|
||||
import * as db from "./database.js";
|
||||
|
||||
const app = neste.default();
|
||||
app.listen(process.env.PORT||3000, () => console.log("Listen on %s", app.address()));
|
||||
|
||||
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(200).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);
|
||||
});
|
||||
|
||||
// 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: extendNet.toString(extendNet.toInt(body.ipv4), true)
|
||||
});
|
||||
return res.status(201).json(userPeer.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,30 +1,51 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": false,
|
||||
"jsx": "preserve",
|
||||
"target": "ESNext",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"strict": false,
|
||||
"noUnusedLocals": true,
|
||||
"isolatedModules": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
"composite": true,
|
||||
"lib": [
|
||||
"ESNext"
|
||||
"paths": {
|
||||
"@components/*": [
|
||||
"./components/*"
|
||||
],
|
||||
"@pass": [
|
||||
"./lib/pass.ts"
|
||||
],
|
||||
"@db/*": [
|
||||
"./lib/database/*"
|
||||
],
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"**/node_modules/**",
|
||||
"**/*.test.*"
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"ts-node": {
|
||||
"files": true,
|
||||
"esm": true,
|
||||
"transpileOnly": true
|
||||
}
|
||||
}
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user