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 ( + + +
+
+
+ + Home +
+ +
+
+ {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
+

Login

+
+
+ + +
+
+ + +
+
+ +
+
+
; +} + +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

+
+
+
+
+ Select peer owner: + +
+
+ Interface peer target: + +
+
+ Generate preshared key? + +
+
+ +
+
+
+ +
+ {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 (
+
+ Interface name: +
+
+ IPv4: +
+
+ IPv6: +
+
+ + Back to home +
+
); +} + +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}

+
+
+ + Edit interface +
+

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" + ] +}