diff --git a/.env b/.env
new file mode 100644
index 0000000..59b63da
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+DB_CONNECTION="sqlite:database.db"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 24ef24a..213c4ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,7 @@ src/**/*.d.ts
src/**/*.js
# Project
-*.db
\ No newline at end of file
+*.db
+
+# Nextjs
+.next/
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..58dd0c9
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,10 @@
+{
+ "editor.tabSize": 2,
+ "files.autoSave": "afterDelay",
+ "files.autoSaveDelay": 2000,
+ "files.exclude": {
+ "**/.next/": true,
+ "**/node_modules/": true,
+ "**/*-lock*/": true,
+ },
+}
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..1f65437
--- /dev/null
+++ b/app/layout.tsx
@@ -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 (
+
+
+
+
+
+
+
+ Main
+
+
+ Wireguard Servers
+
+
+
+ Wireguard peers
+
+
+
+ Utils
+
+
+ Users
+
+
+
+ Status and Stats
+
+
+ {!loginPage && }
+
+
+
+ {loginPage && }
+ {!loginPage && children}
+
+
+
+
+ )
+}
diff --git a/app/layout_global.css b/app/layout_global.css
new file mode 100644
index 0000000..d37394e
--- /dev/null
+++ b/app/layout_global.css
@@ -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;
+}
\ No newline at end of file
diff --git a/app/login.tsx b/app/login.tsx
new file mode 100644
index 0000000..861c94d
--- /dev/null
+++ b/app/login.tsx
@@ -0,0 +1,30 @@
+"use client";
+import { Login, Logout } from "./login_back";
+import LogoutIcon from '@mui/icons-material/Logout';
+
+export function LoginPage() {
+ return ;
+}
+
+export function LogoutPage() {
+ return
+}
\ No newline at end of file
diff --git a/app/login_back.ts b/app/login_back.ts
new file mode 100644
index 0000000..b307ecb
--- /dev/null
+++ b/app/login_back.ts
@@ -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("/");
+}
\ No newline at end of file
diff --git a/app/not-found.tsx b/app/not-found.tsx
new file mode 100644
index 0000000..cfa1095
--- /dev/null
+++ b/app/not-found.tsx
@@ -0,0 +1,15 @@
+import Link from "next/link";
+
+export default function NotFound() {
+ return
+
Oops page not found
+
+
Home
+
+
;
+}
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..7a58268
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,8 @@
+export default function Home() {
+ return (
+
Welcome to Wireguard dashboard
+
+ Click in any options in navbar to start navigation
+
+
);
+}
\ No newline at end of file
diff --git a/app/peers/[id]/config/api/route.ts b/app/peers/[id]/config/api/route.ts
new file mode 100644
index 0000000..652d725
--- /dev/null
+++ b/app/peers/[id]/config/api/route.ts
@@ -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"`
+ }
+ });
+}
\ No newline at end of file
diff --git a/app/peers/api/route.ts b/app/peers/api/route.ts
new file mode 100644
index 0000000..563f802
--- /dev/null
+++ b/app/peers/api/route.ts
@@ -0,0 +1,5 @@
+import { wgPeer } from "@db/peers";
+
+export async function GET() {
+ return Response.json(await wgPeer.findAll());
+}
\ No newline at end of file
diff --git a/app/peers/page.tsx b/app/peers/page.tsx
new file mode 100644
index 0000000..1318b59
--- /dev/null
+++ b/app/peers/page.tsx
@@ -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 (
+
+
+
Owner: {props.owner}
+
interface owner: {props.interfaceOwner}
+
Private key: {props.privateKey}
+ {props.presharedKey &&
Preshared key: {props.presharedKey}
}
+
IPv4: {props.IPv4}
+
IPv6: {props.IPv6}
+
+
+ Download quick config file
+
+
+
);
+}
+
+function NukePeers() {
+ "use client";
+ async function Nuke() {
+ "use server";
+ await wgPeer.destroy({ where: {} });
+ redirect("/peers");
+ }
+ return
+}
+
+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 = {
+ 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
+
+
Wireguard peers
+
+
+
+
+ {peers.map(s => PeerComponent(s))}
+
+
;
+}
\ No newline at end of file
diff --git a/app/servers/[id]/api/route.ts b/app/servers/[id]/api/route.ts
new file mode 100644
index 0000000..f2781a2
--- /dev/null
+++ b/app/servers/[id]/api/route.ts
@@ -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
+ });
+}
\ No newline at end of file
diff --git a/app/servers/[id]/edit/page.tsx b/app/servers/[id]/edit/page.tsx
new file mode 100644
index 0000000..bdf3b38
--- /dev/null
+++ b/app/servers/[id]/edit/page.tsx
@@ -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 ();
+}
+
+export default async function EditServer({ params: { id } }) {
+ const info = await wgServer.findOne({ where: { id: parseInt(id) } });
+ if (!info) return redirect("/servers");
+
+ return (
+
Editing {info.name}
+
+ );
+}
\ No newline at end of file
diff --git a/app/servers/api/route.ts b/app/servers/api/route.ts
new file mode 100644
index 0000000..a0a4766
--- /dev/null
+++ b/app/servers/api/route.ts
@@ -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()))
+}
\ No newline at end of file
diff --git a/app/servers/page.tsx b/app/servers/page.tsx
new file mode 100644
index 0000000..734f097
--- /dev/null
+++ b/app/servers/page.tsx
@@ -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 ()
+}
+
+function CreateSever() {
+ async function deleteServer() {
+ "use server";
+ await createInterface();
+ revalidatePath("/servers");
+ }
+ return ()
+}
+
+// Make server info
+function ServerShow({ serverInfo }: { serverInfo: Server }) {
+ return (
+
Interface name: {serverInfo.name}
+
+
+
Private key: {serverInfo.privateKey}
+
Public key: {key.publicKey(serverInfo.privateKey)}
+ {serverInfo.IPv4 &&
IPv4: {serverInfo.IPv4}
}
+ {serverInfo.IPv6 &&
IPv4: "{serverInfo.IPv6}"
}
+
+ Stats
+
+ Upload:
+ Download:
+
+
+
);
+}
+
+// Export page
+export default async function Servers() {
+ const servers = (await wgServer.findAll()).map(s => s.toJSON());
+ return (
+
Wireguard interfaces
+
+
+ {servers.map(s => )}
+
+
);
+}
\ No newline at end of file
diff --git a/app/stats/page.tsx b/app/stats/page.tsx
new file mode 100644
index 0000000..a31d380
--- /dev/null
+++ b/app/stats/page.tsx
@@ -0,0 +1,5 @@
+export default async function Stats() {
+ return
+ Stats
+
;
+}
\ No newline at end of file
diff --git a/app/users/api/route.ts b/app/users/api/route.ts
new file mode 100644
index 0000000..4cd1c49
--- /dev/null
+++ b/app/users/api/route.ts
@@ -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()));
+}
\ No newline at end of file
diff --git a/app/users/page.tsx b/app/users/page.tsx
new file mode 100644
index 0000000..6eefa28
--- /dev/null
+++ b/app/users/page.tsx
@@ -0,0 +1,5 @@
+export default async function Users() {
+ return
+ Users
+
+}
\ No newline at end of file
diff --git a/components/size.tsx b/components/size.tsx
new file mode 100644
index 0000000..145f227
--- /dev/null
+++ b/components/size.tsx
@@ -0,0 +1,4 @@
+export default Size;
+export function Size({ fileSize }: { fileSize: number }) {
+ return ({fileSize} B );
+}
\ No newline at end of file
diff --git a/lib/database/db.ts b/lib/database/db.ts
new file mode 100644
index 0000000..6350674
--- /dev/null
+++ b/lib/database/db.ts
@@ -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));
\ No newline at end of file
diff --git a/lib/database/peers.ts b/lib/database/peers.ts
new file mode 100644
index 0000000..4e46a37
--- /dev/null
+++ b/lib/database/peers.ts
@@ -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
+export class wgPeer extends Model, InferCreationAttributes> {
+ 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) {}
+});
\ No newline at end of file
diff --git a/lib/database/server.ts b/lib/database/server.ts
new file mode 100644
index 0000000..3e3623d
--- /dev/null
+++ b/lib/database/server.ts
@@ -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
+export class wgServer extends Model, InferCreationAttributes> {
+ 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();
+}
\ No newline at end of file
diff --git a/lib/database/users.ts b/lib/database/users.ts
new file mode 100644
index 0000000..f74575a
--- /dev/null
+++ b/lib/database/users.ts
@@ -0,0 +1,70 @@
+import { encrypt } from "@pass";
+import { DataTypes, InferAttributes, InferCreationAttributes, Model } from "sequelize";
+import { dbConection } from "./db";
+
+export type User = InferAttributes
+export class Users extends Model, InferCreationAttributes> {
+ userID?: number;
+ activate: boolean;
+ name: string;
+ username: string;
+ password: string;
+};
+
+export class Cookies extends Model, InferCreationAttributes> {
+ 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"),
+ })
+});
\ No newline at end of file
diff --git a/lib/pass.ts b/lib/pass.ts
new file mode 100644
index 0000000..99a2554
--- /dev/null
+++ b/lib/pass.ts
@@ -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;
+
+export async function encrypt(text: string): Promise {
+ 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 {
+ 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"));
+}
\ No newline at end of file
diff --git a/next-env.d.ts b/next-env.d.ts
new file mode 100644
index 0000000..4f11a03
--- /dev/null
+++ b/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/next.config.mjs b/next.config.mjs
new file mode 100644
index 0000000..a4c0b47
--- /dev/null
+++ b/next.config.mjs
@@ -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;
+ },
+};
\ No newline at end of file
diff --git a/package.json b/package.json
index 66c7fe4..4111860 100644
--- a/package.json
+++ b/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"
}
}
diff --git a/public/index.html b/public/index.html
deleted file mode 100644
index 69763a7..0000000
--- a/public/index.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
- Document
-
-
-
-
-
\ No newline at end of file
diff --git a/public/login.html b/public/login.html
deleted file mode 100644
index 69763a7..0000000
--- a/public/login.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
- Document
-
-
-
-
-
\ No newline at end of file
diff --git a/src/database.ts b/src/database.ts
deleted file mode 100644
index 02eefa3..0000000
--- a/src/database.ts
+++ /dev/null
@@ -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.InferCreationAttributes>, User { };
-export interface WgSequelize extends sequelize.Model, sequelize.InferCreationAttributes>, Wg { };
-export interface PeerWgSequelize extends sequelize.Model, sequelize.InferCreationAttributes>, PeerWg { };
-
-export const user = db.define("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("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("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(),
-]);
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
deleted file mode 100644
index ee67159..0000000
--- a/src/index.ts
+++ /dev/null
@@ -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(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);
-});
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 713c647..fac10ae 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -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
- }
-}
\ No newline at end of file
+ "exclude": [
+ "node_modules"
+ ]
+}