diff --git a/bun.lock b/bun.lock
index 5b9919a..c2d0a8e 100644
--- a/bun.lock
+++ b/bun.lock
@@ -5,7 +5,7 @@
"": {
"name": "horandez-and-detroit",
"dependencies": {
- "@moduletrace/bun-sqlite": "^1.1.10",
+ "@moduletrace/bun-sqlite": "^1.1.12",
"@moduletrace/bunext": "^1.1.0",
"gray-matter": "^4.0.3",
"html-to-react": "^1.7.0",
@@ -17,6 +17,7 @@
"nodemailer": "^9.0.5",
"onnxruntime-node": "^1.27.0",
"openai": "^7.4.0",
+ "qrcode": "^1.5.4",
"react-code-blocks": "^0.1.6",
"react-markdown": "^10.1.0",
"react-responsive-modal": "^7.2.0",
@@ -36,6 +37,7 @@
"@types/mdx": "^2.0.14",
"@types/node": "^26.2.0",
"@types/nodemailer": "^8.0.1",
+ "@types/qrcode": "^1.5.6",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@types/turndown": "^5.0.6",
@@ -220,7 +222,7 @@
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
- "@moduletrace/bun-sqlite": ["@moduletrace/bun-sqlite@1.1.10", "https://git.tben.me/api/packages/Moduletrace/npm/%40moduletrace%2Fbun-sqlite/-/1.1.10/bun-sqlite-1.1.10.tgz", { "dependencies": { "@inquirer/prompts": "^8.3.0", "chalk": "^5.6.2", "commander": "^14.0.3", "inquirer": "^13.3.2", "lodash": "^4.17.23", "mysql": "^2.18.1", "sqlite-vec": "^0.1.7-alpha.2" }, "peerDependencies": { "typescript": "^5" }, "bin": { "bun-sqlite": "dist/commands/index.js" } }, "sha512-Lvx9Om03vbDen/bZoLfcsFLJEHXjkJLD1646uobzbroLxv4OFPBQlsnlz+k5ljsMU/N+L5gvUy5cI13H1rMdFA=="],
+ "@moduletrace/bun-sqlite": ["@moduletrace/bun-sqlite@1.1.12", "https://git.tben.me/api/packages/Moduletrace/npm/%40moduletrace%2Fbun-sqlite/-/1.1.12/bun-sqlite-1.1.12.tgz", { "dependencies": { "@inquirer/prompts": "^8.3.0", "chalk": "^5.6.2", "commander": "^14.0.3", "inquirer": "^13.3.2", "lodash": "^4.17.23", "mysql": "^2.18.1", "sqlite-vec": "^0.1.7-alpha.2" }, "peerDependencies": { "typescript": "^5" }, "bin": { "bun-sqlite": "dist/commands/index.js" } }, "sha512-2/4NFgI/uvdc3SOYd2lPGIni+8x2jHWwEhSLZaCWpG7666Hbg3Yg6eakZmZ43Blm/7hEap8B3Bt1Jo2wCBk1cA=="],
"@moduletrace/bunext": ["@moduletrace/bunext@1.1.0", "https://git.tben.me/api/packages/Moduletrace/npm/%40moduletrace%2Fbunext/-/1.1.0/bunext-1.1.0.tgz", { "dependencies": { "@tailwindcss/postcss": "^4.2.2", "@types/bun": "latest", "@types/node": "^24.10.0", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "bun-plugin-tailwind": "^0.1.2", "chalk": "^5.6.2", "chokidar": "^5.0.0", "commander": "^14.0.2", "esbuild": "^0.27.4", "lightningcss-wasm": "^1.32.0", "lodash": "^4.17.23", "micromatch": "^4.0.8", "ora": "^9.0.0", "postcss": "^8.5.8", "react": "^19.2.4", "react-dom": "^19.2.4", "tailwindcss": "^4.2.2", "typescript": "^5.0.0" }, "bin": { "bunext": "dist/commands/index.js" } }, "sha512-nlqRpO9BmkfTwmh/FAoxDrIdogGxVz91cK/vLt0d5o0A8Pm9VhPdaWnYtJWvPEdMiPwThYpAwVUBjT3bgg+lAA=="],
@@ -314,6 +316,8 @@
"@types/prismjs": ["@types/prismjs@1.26.6", "", {}, "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw=="],
+ "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="],
+
"@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="],
"@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="],
@@ -378,6 +382,8 @@
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"array-find-index": ["array-find-index@1.0.2", "", {}, "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw=="],
@@ -434,10 +440,16 @@
"cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
+ "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
+
"codepage": ["codepage@1.15.0", "", {}, "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="],
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
@@ -456,6 +468,8 @@
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+ "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
+
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
@@ -468,6 +482,8 @@
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
+ "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="],
+
"dingbat-to-unicode": ["dingbat-to-unicode@1.0.1", "", {}, "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
@@ -480,6 +496,8 @@
"duck": ["duck@0.1.12", "", { "dependencies": { "underscore": "^1.13.1" } }, "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg=="],
+ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
@@ -528,7 +546,7 @@
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
- "find-up": ["find-up@1.1.2", "", { "dependencies": { "path-exists": "^2.0.0", "pinkie-promise": "^2.0.0" } }, "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA=="],
+ "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
@@ -536,6 +554,8 @@
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
+
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
"get-stdin": ["get-stdin@4.0.1", "", {}, "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw=="],
@@ -608,6 +628,8 @@
"is-finite": ["is-finite@1.1.0", "", {}, "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w=="],
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
@@ -662,6 +684,8 @@
"load-json-file": ["load-json-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.2", "parse-json": "^2.2.0", "pify": "^2.0.0", "pinkie-promise": "^2.0.0", "strip-bom": "^2.0.0" } }, "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A=="],
+ "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
+
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
@@ -834,6 +858,12 @@
"ora": ["ora@9.4.1", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw=="],
+ "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
+
+ "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
+
+ "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
+
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
@@ -844,7 +874,7 @@
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
- "path-exists": ["path-exists@2.1.0", "", { "dependencies": { "pinkie-promise": "^2.0.0" } }, "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ=="],
+ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
@@ -862,6 +892,8 @@
"pinkie-promise": ["pinkie-promise@2.0.1", "", { "dependencies": { "pinkie": "^2.0.0" } }, "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw=="],
+ "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
+
"postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
@@ -872,6 +904,8 @@
"property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
+ "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
+
"react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
"react-code-blocks": ["react-code-blocks@0.1.6", "", { "dependencies": { "@babel/runtime": "^7.10.4", "react-syntax-highlighter": "^15.5.0", "styled-components": "^6.1.0", "tslib": "^2.6.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-ENNuxG07yO+OuX1ChRje3ieefPRz6yrIpHmebQlaFQgzcAHbUfVeTINpOpoI9bSRSObeYo/OdHsporeToZ7fcg=="],
@@ -924,6 +958,10 @@
"repeating": ["repeating@2.0.1", "", { "dependencies": { "is-finite": "^1.0.0" } }, "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A=="],
+ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
+
+ "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="],
+
"resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
@@ -944,6 +982,8 @@
"serialize-error": ["serialize-error@8.1.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ=="],
+ "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
+
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
"sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" } }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="],
@@ -1068,18 +1108,28 @@
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
+ "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
+
"wmf": ["wmf@1.0.2", "", {}, "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="],
"word": ["word@0.3.0", "", {}, "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="],
+ "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
+
"xlsx": ["xlsx@0.18.5", "", { "dependencies": { "adler-32": "~1.3.0", "cfb": "~1.2.1", "codepage": "~1.15.0", "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" }, "bin": { "xlsx": "bin/xlsx.njs" } }, "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ=="],
"xmlbuilder": ["xmlbuilder@10.1.1", "", {}, "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
+ "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
+
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
+ "yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
+
+ "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
+
"yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
@@ -1100,6 +1150,10 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+ "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
"decode-named-character-reference/character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
"jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
@@ -1118,14 +1172,32 @@
"react-syntax-highlighter/refractor": ["refractor@3.6.0", "", { "dependencies": { "hastscript": "^6.0.0", "parse-entities": "^2.0.0", "prismjs": "~1.27.0" } }, "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA=="],
+ "read-pkg-up/find-up": ["find-up@1.1.2", "", { "dependencies": { "path-exists": "^2.0.0", "pinkie-promise": "^2.0.0" } }, "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA=="],
+
+ "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
+
"@moduletrace/bunext/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+ "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
"react-syntax-highlighter/refractor/hastscript": ["hastscript@6.0.0", "", { "dependencies": { "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", "hast-util-parse-selector": "^2.0.0", "property-information": "^5.0.0", "space-separated-tokens": "^1.0.0" } }, "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w=="],
"react-syntax-highlighter/refractor/parse-entities": ["parse-entities@2.0.0", "", { "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", "character-reference-invalid": "^1.0.0", "is-alphanumerical": "^1.0.0", "is-decimal": "^1.0.0", "is-hexadecimal": "^1.0.0" } }, "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ=="],
"react-syntax-highlighter/refractor/prismjs": ["prismjs@1.27.0", "", {}, "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA=="],
+ "read-pkg-up/find-up/path-exists": ["path-exists@2.1.0", "", { "dependencies": { "pinkie-promise": "^2.0.0" } }, "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ=="],
+
+ "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
"react-syntax-highlighter/refractor/hastscript/@types/hast": ["@types/hast@2.3.10", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="],
"react-syntax-highlighter/refractor/hastscript/comma-separated-tokens": ["comma-separated-tokens@1.0.8", "", {}, "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw=="],
@@ -1146,6 +1218,8 @@
"react-syntax-highlighter/refractor/parse-entities/is-hexadecimal": ["is-hexadecimal@1.0.4", "", {}, "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="],
+ "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
"react-syntax-highlighter/refractor/hastscript/@types/hast/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"react-syntax-highlighter/refractor/parse-entities/is-alphanumerical/is-alphabetical": ["is-alphabetical@1.0.4", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="],
diff --git a/db/schema.ts b/db/schema.ts
index 13d199d..02a6f2c 100644
--- a/db/schema.ts
+++ b/db/schema.ts
@@ -182,6 +182,11 @@ const schema: BUN_SQLITE_DatabaseSchemaType = {
fieldName: "allowed_ips",
dataType: "TEXT",
},
+ {
+ fieldName: "allow_all_ips",
+ dataType: "INTEGER",
+ options: [0, 1],
+ },
{
fieldName: "notes",
dataType: "TEXT",
diff --git a/db/types/db.ts b/db/types/db.ts
index 38150ef..fae67fd 100644
--- a/db/types/db.ts
+++ b/db/types/db.ts
@@ -113,6 +113,7 @@ export type BUN_SQLITE_WGUI_CLIENTS = {
public_ip_address?: string;
public_key?: string;
allowed_ips?: string;
+ allow_all_ips?: 0 | 1 | "";
notes?: string;
}
diff --git a/package.json b/package.json
index 9ef5c25..e71dbef 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
"dev": "sudo bun src/server.ts",
"build": "bunx bunext build",
"db:schema": "bunx bun-sqlite schema -t",
+ "db:admin": "bunx bun-sqlite admin",
"deploy:prod": "./deploy/deploy-prod.sh",
"start": "NODE_ENV=production bun src/server.ts"
},
@@ -18,6 +19,7 @@
"@types/mdx": "^2.0.14",
"@types/node": "^26.2.0",
"@types/nodemailer": "^8.0.1",
+ "@types/qrcode": "^1.5.6",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@types/turndown": "^5.0.6",
@@ -28,7 +30,7 @@
"typescript": "^7.0.2"
},
"dependencies": {
- "@moduletrace/bun-sqlite": "^1.1.10",
+ "@moduletrace/bun-sqlite": "^1.1.12",
"@moduletrace/bunext": "^1.1.0",
"gray-matter": "^4.0.3",
"html-to-react": "^1.7.0",
@@ -40,6 +42,7 @@
"nodemailer": "^9.0.5",
"onnxruntime-node": "^1.27.0",
"openai": "^7.4.0",
+ "qrcode": "^1.5.4",
"react-code-blocks": "^0.1.6",
"react-markdown": "^10.1.0",
"react-responsive-modal": "^7.2.0",
diff --git a/src/components/general/client-row.tsx b/src/components/general/client-row.tsx
index d6d8547..fca74fc 100644
--- a/src/components/general/client-row.tsx
+++ b/src/components/general/client-row.tsx
@@ -1,5 +1,10 @@
+import { useState } from "react";
+import { Pencil, Trash2 } from "lucide-react";
+import Link from "@/src/components/twui/layout/Link";
+import Row from "@/src/components/twui/layout/Row";
import Span from "@/src/components/twui/layout/Span";
import formatUnixTimestamp from "@/src/utils/format-unix-timestamp";
+import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
type Props = {
@@ -7,12 +12,45 @@ type Props = {
};
export default function ClientRow({ client }: Props) {
+ const [deleting, setDeleting] = useState(false);
+
+ const host_id = Number(client.host_id || 0);
+
+ async function handleDelete() {
+ if (
+ !window.confirm(
+ `Delete client "${client?.name || `#${client?.id}`}"? The client will be removed from the host and its config file deleted.`,
+ )
+ ) {
+ return;
+ }
+
+ setDeleting(true);
+
+ const res = await adminCrudHandler({
+ action: "delete",
+ table: "clients",
+ id: client?.id,
+ });
+
+ if (!res.success) {
+ setDeleting(false);
+ window.alert(res.msg || "Could not delete client");
+ return;
+ }
+
+ window.location.reload();
+ }
+
return (
|
-
+
{client.name || "—"}
-
+
|
{client.wg_ip_address || "—"}
@@ -30,6 +68,28 @@ export default function ClientRow({ client }: Props) {
|
{formatUnixTimestamp({ timestamp: client.created_at })}
|
+
+
+
+
+
+
+
+ |
);
}
\ No newline at end of file
diff --git a/src/components/twui/editors/AceEditor.tsx b/src/components/twui/editors/AceEditor.tsx
index 26132a9..8c01afc 100644
--- a/src/components/twui/editors/AceEditor.tsx
+++ b/src/components/twui/editors/AceEditor.tsx
@@ -1,8 +1,8 @@
import React, { type RefObject } from "react";
import { twMerge } from "tailwind-merge";
import AceEditorModes from "./ace-editor-modes";
-// @ts-ignore
-import { AceEditorOptions } from "@moduletrace/datasquirel/dist/package-shared/types";
+import type { AceEditorOptions } from "../types";
+import type { Ace as AceAjax } from "ace-builds";
export type AceEditorComponentType = {
editorRef?: RefObject;
@@ -60,6 +60,9 @@ export default function AceEditor({
React.useEffect(() => {
if (!ready) return;
+ // @ts-ignore
+ const ace = window.ace as AceAjax;
+
if (!ace?.edit || !editorElementRef.current) {
setTimeout(() => {
setRefresh((prev) => prev + 1);
@@ -95,13 +98,13 @@ export default function AceEditor({
editor.commands.addCommand({
name: "myCommand",
bindKey: { win: "Ctrl-Enter", mac: "Command-Enter" },
- exec: function (editor) {
+ exec: function (editor: any) {
if (ctrlEnterFn) ctrlEnterFn(editor);
},
readOnly: true,
});
- editor.getSession().on("change", function (e) {
+ editor.getSession().on("change", function () {
if (onChange) {
clearTimeout(timeout);
diff --git a/src/functions/backend/setup/generate-wireguard-client-qr.ts b/src/functions/backend/setup/generate-wireguard-client-qr.ts
new file mode 100644
index 0000000..b1df426
--- /dev/null
+++ b/src/functions/backend/setup/generate-wireguard-client-qr.ts
@@ -0,0 +1,35 @@
+import QRCode from "qrcode";
+
+type Params = {
+ config?: string | null;
+ width?: number;
+};
+
+export default async function generateWireguardClientQR({
+ config,
+ width = 320,
+}: Params) {
+ if (!config) {
+ return {
+ qr_data_uri: null,
+ };
+ }
+
+ try {
+ const svg = await QRCode.toString(config, {
+ type: "svg",
+ errorCorrectionLevel: "M",
+ width,
+ });
+
+ const qr_data_uri = `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
+
+ return {
+ qr_data_uri,
+ };
+ } catch (error) {
+ return {
+ qr_data_uri: null,
+ };
+ }
+}
diff --git a/src/functions/backend/setup/grab-client-dir-names.ts b/src/functions/backend/setup/grab-client-dir-names.ts
new file mode 100644
index 0000000..78766ff
--- /dev/null
+++ b/src/functions/backend/setup/grab-client-dir-names.ts
@@ -0,0 +1,77 @@
+import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
+import path from "path";
+import grabDirNames from "@/src/utils/grab-dir-names";
+import { execSync } from "child_process";
+
+type Params = {
+ client: BUN_SQLITE_WGUI_CLIENTS;
+ host_id?: number;
+};
+
+const {
+ WGUI_LIB_HOSTS_CONFIGS_DIR,
+ WGUI_LIB_HOST_CLIENTS_DIR_NAME,
+ WIREGUARD_PRIVATE_KEY_FILE_NAME,
+ WIREGUARD_PUBLIC_KEY_FILE_NAME,
+ WIREGUARD_CLIENT_CONFIG_FILE_NAME,
+} = grabDirNames();
+
+export default function grabClientDirnames({
+ client,
+ host_id: passed_host_id,
+}: Params) {
+ const host_id = passed_host_id || 0;
+
+ const CLIENT_WG_IP = client?.wg_ip_address;
+
+ const HOST_CONFIG_DIR = path.join(
+ WGUI_LIB_HOSTS_CONFIGS_DIR,
+ String(host_id),
+ );
+
+ const HOST_CLIENTS_DIR = path.join(
+ HOST_CONFIG_DIR,
+ WGUI_LIB_HOST_CLIENTS_DIR_NAME,
+ );
+
+ const CLIENT_DIR = path.join(HOST_CLIENTS_DIR, `${client.id}`);
+
+ const CLIENT_PRIVATE_KEY_FILE = path.join(
+ CLIENT_DIR,
+ WIREGUARD_PRIVATE_KEY_FILE_NAME,
+ );
+ const CLIENT_PUBLIC_KEY_FILE = path.join(
+ CLIENT_DIR,
+ WIREGUARD_PUBLIC_KEY_FILE_NAME,
+ );
+ const CLIENT_CONFIG_FILE = path.join(
+ CLIENT_DIR,
+ WIREGUARD_CLIENT_CONFIG_FILE_NAME,
+ );
+
+ const CLIENT_PRIVATE_KEY = execSync(`cat ${CLIENT_PRIVATE_KEY_FILE}`, {
+ encoding: "utf-8",
+ }).trim();
+
+ const CLIENT_PUBLIC_KEY = execSync(`cat ${CLIENT_PUBLIC_KEY_FILE}`, {
+ encoding: "utf-8",
+ }).trim();
+
+ const HOST_PUBLIC_KEY = execSync(
+ `cat ${path.join(HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME)}`,
+ { encoding: "utf-8" },
+ ).trim();
+
+ return {
+ CLIENT_WG_IP,
+ HOST_CLIENTS_DIR,
+ HOST_CONFIG_DIR,
+ CLIENT_DIR,
+ CLIENT_PRIVATE_KEY_FILE,
+ CLIENT_PUBLIC_KEY_FILE,
+ CLIENT_CONFIG_FILE,
+ CLIENT_PRIVATE_KEY,
+ CLIENT_PUBLIC_KEY,
+ HOST_PUBLIC_KEY,
+ };
+}
diff --git a/src/functions/backend/setup/manage-wireguard-host.ts b/src/functions/backend/setup/manage-wireguard-host.ts
index a52d526..4a92dae 100644
--- a/src/functions/backend/setup/manage-wireguard-host.ts
+++ b/src/functions/backend/setup/manage-wireguard-host.ts
@@ -4,6 +4,7 @@ import deriveWireguardInterfaceName from "@/src/utils/derive-wireguard-interface
import type { APIResponseObject } from "@moduletrace/bunext/types";
import { execSync } from "node:child_process";
import path from "node:path";
+import grabHostDirnames from "./grab-host-dir-names";
const { WGUI_LIB_HOSTS_CONFIGS_DIR, WGUI_WG_QUICK_MANAGE_SCRIPT } =
grabDirNames();
@@ -19,11 +20,9 @@ export default function manageWireguardHost({
action,
host_id = AppData["WireguardHostID"],
}: Params): APIResponseObject {
- const INTERFACE_NAME = deriveWireguardInterfaceName({ host_id });
- const HOST_CONFIG_PATH = path.join(
- WGUI_LIB_HOSTS_CONFIGS_DIR,
- `${INTERFACE_NAME}.conf`,
- );
+ const { HOST_CONFIG_FILE, INTERFACE_NAME } = grabHostDirnames({
+ host: { id: host_id },
+ });
const IS_ROOT =
typeof process.getuid === "function" && process.getuid() === 0;
@@ -35,7 +34,7 @@ export default function manageWireguardHost({
};
}
- const MANAGE_COMMAND = `${WGUI_WG_QUICK_MANAGE_SCRIPT} ${action} ${INTERFACE_NAME} ${HOST_CONFIG_PATH}`;
+ const MANAGE_COMMAND = `${WGUI_WG_QUICK_MANAGE_SCRIPT} ${action} ${INTERFACE_NAME} ${HOST_CONFIG_FILE}`;
try {
const output = execSync(MANAGE_COMMAND, { encoding: "utf-8" }).trim();
diff --git a/src/functions/backend/setup/read-wireguard-client-config.ts b/src/functions/backend/setup/read-wireguard-client-config.ts
new file mode 100644
index 0000000..b5aaa3a
--- /dev/null
+++ b/src/functions/backend/setup/read-wireguard-client-config.ts
@@ -0,0 +1,53 @@
+import path from "node:path";
+import grabDirNames from "@/src/utils/grab-dir-names";
+import grabHostDirnames from "./grab-host-dir-names";
+
+const { WIREGUARD_CLIENT_CONFIG_FILE_NAME } = grabDirNames();
+
+type Params = {
+ client_id?: string | number;
+ host_id?: string | number;
+};
+
+export default async function readWireguardClientConfig({
+ client_id,
+ host_id,
+}: Params) {
+ if (!client_id) {
+ return {
+ config: null,
+ config_path: null,
+ };
+ }
+
+ const { HOST_CLIENTS_DIR } = grabHostDirnames({
+ host: host_id ? { id: Number(host_id) } : undefined,
+ });
+
+ const config_path = path.join(
+ HOST_CLIENTS_DIR,
+ String(client_id),
+ WIREGUARD_CLIENT_CONFIG_FILE_NAME,
+ );
+
+ try {
+ const config_file = Bun.file(config_path);
+
+ if (!(await config_file.exists())) {
+ return {
+ config: null,
+ config_path,
+ };
+ }
+
+ return {
+ config: await config_file.text(),
+ config_path,
+ };
+ } catch (error) {
+ return {
+ config: null,
+ config_path,
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/functions/backend/setup/setup-wireguard-client.ts b/src/functions/backend/setup/setup-wireguard-client.ts
index 82a5400..a78b9f2 100644
--- a/src/functions/backend/setup/setup-wireguard-client.ts
+++ b/src/functions/backend/setup/setup-wireguard-client.ts
@@ -12,6 +12,7 @@ import setupWireguardHost from "./setup-wireguard-host";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { TableType, User } from "@/src/types";
+import grabClientDirnames from "./grab-client-dir-names";
const {
WGUI_LIB_HOSTS_CONFIGS_DIR,
@@ -25,6 +26,12 @@ type Params = {
client: BUN_SQLITE_WGUI_CLIENTS;
host?: BUN_SQLITE_WGUI_HOSTS;
user: User;
+ /**
+ * Skip the host setup/restart at the end. Use when
+ * regenerating multiple clients and restarting the
+ * host once afterwards instead.
+ */
+ skip_host_setup?: boolean;
};
/**
@@ -38,6 +45,7 @@ export default async function setupWireguardClient({
client,
host,
user,
+ skip_host_setup,
}: Params): Promise {
const host_id = host?.id || AppData["WireguardHostID"];
@@ -50,7 +58,18 @@ export default async function setupWireguardClient({
const variables = variables_res.payload;
- const CLIENT_WG_IP = client?.wg_ip_address;
+ const {
+ CLIENT_WG_IP,
+ CLIENT_CONFIG_FILE,
+ CLIENT_DIR,
+ CLIENT_PRIVATE_KEY_FILE,
+ CLIENT_PUBLIC_KEY_FILE,
+ HOST_CLIENTS_DIR,
+ HOST_CONFIG_DIR,
+ CLIENT_PRIVATE_KEY,
+ CLIENT_PUBLIC_KEY,
+ HOST_PUBLIC_KEY,
+ } = grabClientDirnames({ client, host_id });
if (!CLIENT_WG_IP) {
return {
@@ -59,18 +78,6 @@ export default async function setupWireguardClient({
};
}
- const HOST_CONFIG_DIR = path.join(
- WGUI_LIB_HOSTS_CONFIGS_DIR,
- String(host_id),
- );
-
- const HOST_CLIENTS_DIR = path.join(
- HOST_CONFIG_DIR,
- WGUI_LIB_HOST_CLIENTS_DIR_NAME,
- );
-
- const CLIENT_DIR = path.join(HOST_CLIENTS_DIR, `${client.id}`);
-
const HOST_WG_IP = host?.id
? host?.wg_ip_address
: variables?.find((v) => v.key == "main_host_wg_ip_address")?.value;
@@ -82,19 +89,6 @@ export default async function setupWireguardClient({
};
}
- const CLIENT_PRIVATE_KEY_FILE = path.join(
- CLIENT_DIR,
- WIREGUARD_PRIVATE_KEY_FILE_NAME,
- );
- const CLIENT_PUBLIC_KEY_FILE = path.join(
- CLIENT_DIR,
- WIREGUARD_PUBLIC_KEY_FILE_NAME,
- );
- const CLIENT_CONFIG_FILE = path.join(
- CLIENT_DIR,
- WIREGUARD_CLIENT_CONFIG_FILE_NAME,
- );
-
let pre_sh = ``;
pre_sh += `mkdir -p ${CLIENT_DIR}\n`;
@@ -112,20 +106,10 @@ export default async function setupWireguardClient({
}
try {
- const CLIENT_PRIVATE_KEY = execSync(`cat ${CLIENT_PRIVATE_KEY_FILE}`, {
- encoding: "utf-8",
- }).trim();
-
- const CLIENT_PUBLIC_KEY = execSync(`cat ${CLIENT_PUBLIC_KEY_FILE}`, {
- encoding: "utf-8",
- }).trim();
-
- const HOST_PUBLIC_KEY = execSync(
- `cat ${path.join(HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME)}`,
- { encoding: "utf-8" },
- ).trim();
-
const PUBLIC_IP_ADDRESS =
+ host?.public_ip_address ||
+ variables?.find((v) => v.key == "main_host_public_ip_address")
+ ?.value ||
(await grabHostPublicIPAddress()) ||
client?.public_ip_address ||
HOST_WG_IP;
@@ -138,26 +122,38 @@ export default async function setupWireguardClient({
sh += `[Interface]\n`;
sh += `Address = ${CLIENT_WG_IP}/32\n`;
sh += `PrivateKey = ${CLIENT_PRIVATE_KEY}\n`;
- sh += `DNS = 1.1.1.1\n`;
+ // sh += `DNS = 1.1.1.1\n`;
sh += `\n`;
sh += `[Peer]\n`;
sh += `PublicKey = ${HOST_PUBLIC_KEY}\n`;
sh += `Endpoint = ${PUBLIC_IP_ADDRESS}:51820\n`;
- if (client?.allowed_ips) {
+ if (client?.allow_all_ips == 1) {
+ sh += `AllowedIPs = 0.0.0.0/0, ::/0\n`;
+ } else if (client?.allowed_ips) {
sh += `AllowedIPs = ${client.allowed_ips}\n`;
}
+ sh += `PersistentKeepalive = 25\n`;
+
sh += `EOF\n`;
sh += `\n`;
- const exec = execSync(sh, { encoding: "utf-8" });
+ const exec = execSync(sh, {
+ encoding: "utf-8",
+ });
const CLIENT_CONFIG = execSync(`cat ${CLIENT_CONFIG_FILE}`, {
encoding: "utf-8",
});
- const host_setup_res = await setupWireguardHost({ host, user });
+ const host_setup_res = skip_host_setup
+ ? { success: true }
+ : await setupWireguardHost({
+ host,
+ user,
+ is_update_after_client_setup: true,
+ });
if (!host_setup_res.success) {
return {
@@ -172,6 +168,11 @@ export default async function setupWireguardClient({
stringRes: CLIENT_CONFIG,
};
} catch (error: any) {
+ console.log(
+ `Error setting up wireguard client ${client.id} for host ${host_id} =>`,
+ error.message,
+ );
+
return {
success: false,
msg: error.message,
diff --git a/src/functions/backend/setup/setup-wireguard-host.ts b/src/functions/backend/setup/setup-wireguard-host.ts
index fc8d1e4..7696d2e 100644
--- a/src/functions/backend/setup/setup-wireguard-host.ts
+++ b/src/functions/backend/setup/setup-wireguard-host.ts
@@ -12,6 +12,8 @@ import type { TableType, User } from "@/src/types";
import checkPrivateIPAvailability from "./check-private-ip-availability";
import manageWireguardHost from "./manage-wireguard-host";
import grabHostDirnames from "./grab-host-dir-names";
+import path from "node:path";
+import grabClientDirnames from "./grab-client-dir-names";
const { WIREGUARD_PRIVATE_KEY_FILE_NAME, WIREGUARD_PUBLIC_KEY_FILE_NAME } =
grabDirNames();
@@ -20,12 +22,14 @@ type Params = {
host?: BUN_SQLITE_WGUI_HOSTS;
wg_subnet_ip?: string;
user: User;
+ is_update_after_client_setup?: boolean;
};
export default async function setupWireguardHost({
host,
wg_subnet_ip,
user,
+ is_update_after_client_setup,
}: Params): Promise {
const {
HOST_CLIENTS_DIR,
@@ -57,7 +61,7 @@ export default async function setupWireguardHost({
query: {
query: {
host_id: {
- value: HOST_ID,
+ value: String(HOST_ID),
},
},
},
@@ -78,35 +82,39 @@ export default async function setupWireguardHost({
};
}
- const is_ip_available = await checkPrivateIPAvailability({
- ip_address: HOST_WG_IP,
- });
+ const is_ip_available = is_update_after_client_setup
+ ? { success: true }
+ : await checkPrivateIPAvailability({
+ ip_address: HOST_WG_IP,
+ });
- if (!is_ip_available.success) {
+ if (!is_ip_available.success && !is_update_after_client_setup) {
return {
success: false,
msg: `IP not available`,
};
}
- let pre_sh = ``;
+ if (!is_update_after_client_setup) {
+ let pre_sh = ``;
- pre_sh += `set -e\n`;
- pre_sh += `mkdir -p ${HOST_CONFIG_DIR}\n`;
- pre_sh += `mkdir -p ${HOST_CLIENTS_DIR}\n`;
- pre_sh += `mkdir -p ${HOST_IPTABLES_DIR}\n`;
- pre_sh += `cd ${HOST_CONFIG_DIR}\n`;
- pre_sh += `if [ ! -f ${WIREGUARD_PRIVATE_KEY_FILE_NAME} ]; then\n`;
- pre_sh += ` wg genkey | tee ${WIREGUARD_PRIVATE_KEY_FILE_NAME} | wg pubkey > ${WIREGUARD_PUBLIC_KEY_FILE_NAME}\n`;
- pre_sh += `fi\n`;
+ pre_sh += `set -e\n`;
+ pre_sh += `mkdir -p ${HOST_CONFIG_DIR}\n`;
+ pre_sh += `mkdir -p ${HOST_CLIENTS_DIR}\n`;
+ pre_sh += `mkdir -p ${HOST_IPTABLES_DIR}\n`;
+ pre_sh += `cd ${HOST_CONFIG_DIR}\n`;
+ pre_sh += `if [ ! -f ${WIREGUARD_PRIVATE_KEY_FILE_NAME} ]; then\n`;
+ pre_sh += ` wg genkey | tee ${WIREGUARD_PRIVATE_KEY_FILE_NAME} | wg pubkey > ${WIREGUARD_PUBLIC_KEY_FILE_NAME}\n`;
+ pre_sh += `fi\n`;
- try {
- const exec_pre_setup = execSync(pre_sh, { encoding: "utf-8" });
- } catch (error: any) {
- return {
- success: false,
- msg: error.message,
- };
+ try {
+ const exec_pre_setup = execSync(pre_sh, { encoding: "utf-8" });
+ } catch (error: any) {
+ return {
+ success: false,
+ msg: error.message,
+ };
+ }
}
try {
@@ -118,7 +126,7 @@ export default async function setupWireguardHost({
encoding: "utf-8",
}).trim();
- if (HOST_ID == 0) {
+ if (HOST_ID == 0 && !is_update_after_client_setup) {
const update_variables = await BunSQLite.insert<
BUN_SQLITE_WGUI_VARIABLES,
TableType
@@ -140,7 +148,7 @@ export default async function setupWireguardHost({
if (!update_variables.success) {
throw new Error(`Couldn't update host variables`);
}
- } else if (HOST_ID) {
+ } else if (HOST_ID && !is_update_after_client_setup) {
const update_host = await BunSQLite.insert<
BUN_SQLITE_WGUI_HOSTS,
TableType
@@ -208,10 +216,15 @@ export default async function setupWireguardHost({
if (clients[0]) {
for (let i = 0; i < clients.length; i++) {
const client = clients[i];
- if (!client?.id || !client.public_key) continue;
+ if (!client?.id) continue;
+
+ const { CLIENT_PUBLIC_KEY } = grabClientDirnames({
+ client,
+ host_id: HOST_ID,
+ });
sh += `[Peer]\n`;
- sh += `PublicKey = ${client.public_key}\n`;
+ sh += `PublicKey = ${CLIENT_PUBLIC_KEY}\n`;
sh += `AllowedIPs = ${client.allowed_ips}\n`;
sh += `\n`;
}
@@ -219,6 +232,8 @@ export default async function setupWireguardHost({
sh += `EOF\n`;
+ sh += `chmod 600 ${INTERFACE_NAME}.conf\n`;
+
sh += `\n`;
const exec = execSync(sh, { encoding: "utf-8" });
@@ -240,6 +255,11 @@ export default async function setupWireguardHost({
msg: [exec.trim(), manage_res.msg].join("\n\n"),
};
} catch (error: any) {
+ console.log(
+ `Error setting up wireguard host ${HOST_ID} =>`,
+ error.message,
+ );
+
return {
success: false,
msg: error.message,
diff --git a/src/functions/backend/setup/sync-wireguard-hosts.ts b/src/functions/backend/setup/sync-wireguard-hosts.ts
index 317e7db..4ddb89d 100644
--- a/src/functions/backend/setup/sync-wireguard-hosts.ts
+++ b/src/functions/backend/setup/sync-wireguard-hosts.ts
@@ -8,6 +8,7 @@ import type {
} from "@/db/types/db";
import type { TableType } from "@/src/types";
import { AppData } from "@/src/data/app-data";
+import bunext from "@moduletrace/bunext";
const HOST_CONFIG_FILE_NAME_PATTERN = /^wgui(\d+)\.conf$/;
@@ -50,7 +51,7 @@ export default async function syncWireguardHosts() {
const res = manageWireguardHost({ action: "up", host_id });
- console.log(
+ bunext.bunextLog.info(
`[wgui] wireguard host ${host_config_file_name}: ${
res.success ? `up` : `failed — ${res.msg}`
}`,
diff --git a/src/functions/backend/setup/update-wireguard-host-public-ip.ts b/src/functions/backend/setup/update-wireguard-host-public-ip.ts
new file mode 100644
index 0000000..b948c8f
--- /dev/null
+++ b/src/functions/backend/setup/update-wireguard-host-public-ip.ts
@@ -0,0 +1,174 @@
+import type {
+ BUN_SQLITE_WGUI_CLIENTS,
+ BUN_SQLITE_WGUI_HOSTS,
+ BUN_SQLITE_WGUI_VARIABLES,
+} from "@/db/types/db";
+import { AppData } from "@/src/data/app-data";
+import BunSQLite from "/home/archben/Projects/Git/moduletrace/bun-sqlite";
+// import BunSQLite from "@moduletrace/bun-sqlite";
+import type { APIResponseObject } from "@moduletrace/bunext/types";
+import type { TableType, User } from "@/src/types";
+import setupWireguardClient from "./setup-wireguard-client";
+import setupWireguardHost from "./setup-wireguard-host";
+
+const IP_V4_REGEX = /^\d{1,3}(\.\d{1,3}){3}$/;
+
+type Params = {
+ host_id?: string | number | null;
+ public_ip_address?: string | null;
+ user: User;
+};
+
+/**
+ * Update the public IP address of a host and regenerate
+ * every client config of that host with the new Endpoint.
+ * @param param0
+ * @returns
+ */
+export default async function updateWireguardHostPublicIP({
+ host_id,
+ public_ip_address,
+ user,
+}: Params): Promise {
+ const final_host_id = Number(host_id || AppData["WireguardHostID"]);
+ const ip = (public_ip_address || "").trim();
+
+ if (!ip || !IP_V4_REGEX.test(ip)) {
+ return {
+ success: false,
+ msg: `Invalid public IP address provided`,
+ };
+ }
+
+ let host: BUN_SQLITE_WGUI_HOSTS | undefined;
+
+ try {
+ if (final_host_id == AppData["WireguardHostID"]) {
+ const update_variables = await BunSQLite.insert<
+ BUN_SQLITE_WGUI_VARIABLES,
+ TableType
+ >({
+ table: "variables",
+ data: [
+ {
+ key: "main_host_public_ip_address",
+ value: ip,
+ },
+ ],
+ update_on_duplicate: true,
+ });
+
+ // console.log("update_variables", update_variables);
+
+ // if (!update_variables.success) {
+ // throw new Error(`Couldn't update host public IP variable`);
+ // }
+ } else {
+ const host_res = await BunSQLite.select<
+ BUN_SQLITE_WGUI_HOSTS,
+ TableType
+ >({
+ table: "hosts",
+ targetId: final_host_id,
+ });
+
+ host = host_res.singleRes || undefined;
+
+ if (!host) {
+ return {
+ success: false,
+ msg: `Host record not found`,
+ };
+ }
+
+ const update_host = await BunSQLite.insert<
+ BUN_SQLITE_WGUI_HOSTS,
+ TableType
+ >({
+ table: "hosts",
+ data: [
+ {
+ id: host.id,
+ user_id: user.id,
+ public_ip_address: ip,
+ },
+ ],
+ update_on_duplicate: true,
+ });
+
+ if (!update_host.success) {
+ throw new Error(`Couldn't update host record`);
+ }
+
+ host.public_ip_address = ip;
+ }
+ } catch (error: any) {
+ return {
+ success: false,
+ msg: error.message,
+ };
+ }
+
+ const host_clients_res = await BunSQLite.select<
+ BUN_SQLITE_WGUI_CLIENTS,
+ TableType
+ >({
+ table: "clients",
+ query: {
+ query: {
+ host_id: {
+ value: String(final_host_id),
+ },
+ },
+ },
+ });
+
+ const clients = host_clients_res.payload || [];
+
+ let updated_count = 0;
+ const client_errors: string[] = [];
+
+ for (let i = 0; i < clients.length; i++) {
+ const client = clients[i];
+
+ if (!client?.id) continue;
+
+ const client_setup_res = await setupWireguardClient({
+ client,
+ host,
+ user,
+ skip_host_setup: true,
+ });
+
+ console.log("client_setup_res", client_setup_res);
+
+ if (client_setup_res.success) {
+ updated_count++;
+ } else {
+ client_errors.push(
+ `Client ${client.name || `#${client.id}`}: ${client_setup_res.msg}`,
+ );
+ }
+ }
+
+ const host_setup_res = await setupWireguardHost({
+ host,
+ user,
+ is_update_after_client_setup: true,
+ });
+
+ if (!host_setup_res.success) {
+ return {
+ success: false,
+ msg: `${updated_count} client config(s) updated, but the host could not be restarted: ${host_setup_res.msg}`,
+ numberRes: updated_count,
+ };
+ }
+
+ return {
+ success: true,
+ msg: `Host IP updated to ${ip}. ${updated_count} client config(s) regenerated.`,
+ numberRes: updated_count,
+ stringRes: client_errors[0] ? client_errors.join("; ") : null,
+ };
+}
diff --git a/src/pages/__root.tsx b/src/pages/__root.tsx
index f0d5504..01028cb 100755
--- a/src/pages/__root.tsx
+++ b/src/pages/__root.tsx
@@ -84,6 +84,8 @@ export function Head({ serverRes, ctx }: BunextPageHeadFCProps) {
href="https://fonts.gstatic.com"
crossOrigin=""
/>
+
+
>
);
}
diff --git a/src/pages/admin/clients/(partials)/client-form-modal.tsx b/src/pages/admin/clients/(partials)/client-form-modal.tsx
index 3defb7b..3a2cde0 100644
--- a/src/pages/admin/clients/(partials)/client-form-modal.tsx
+++ b/src/pages/admin/clients/(partials)/client-form-modal.tsx
@@ -1,4 +1,9 @@
-import { useState, type Dispatch, type ReactNode, type SetStateAction } from "react";
+import {
+ useState,
+ type Dispatch,
+ type ReactNode,
+ type SetStateAction,
+} from "react";
import { X } from "lucide-react";
import Modal from "@/src/components/twui/elements/Modal";
import Input from "@/src/components/twui/form/Input";
@@ -97,7 +102,7 @@ export default function ClientFormModal({ open, setOpen, onCreated }: Props) {
@@ -108,7 +113,10 @@ export default function ClientFormModal({ open, setOpen, onCreated }: Props) {
) : null}
- setOpen(false)} disabled={busy}>
+ setOpen(false)}
+ disabled={busy}
+ >
Cancel
);
-}
\ No newline at end of file
+}
diff --git a/src/pages/admin/clients/(sections)/clients-table-section.tsx b/src/pages/admin/clients/(sections)/clients-table-section.tsx
index 526ea23..b9417f5 100644
--- a/src/pages/admin/clients/(sections)/clients-table-section.tsx
+++ b/src/pages/admin/clients/(sections)/clients-table-section.tsx
@@ -82,6 +82,7 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
| Allowed IPs |
Public key |
Created |
+ Actions |
diff --git a/src/pages/admin/hosts/(sections)/host-public-ip-section.tsx b/src/pages/admin/hosts/(sections)/host-public-ip-section.tsx
new file mode 100644
index 0000000..035e4b5
--- /dev/null
+++ b/src/pages/admin/hosts/(sections)/host-public-ip-section.tsx
@@ -0,0 +1,127 @@
+import { useEffect, useState } from "react";
+import AdminCard from "@/src/components/general/admin-card";
+import AdminButton from "@/src/components/general/admin-button";
+import Input from "@/src/components/twui/form/Input";
+import H3 from "@/src/components/twui/layout/H3";
+import P from "@/src/components/twui/layout/P";
+import Row from "@/src/components/twui/layout/Row";
+import Span from "@/src/components/twui/layout/Span";
+import Stack from "@/src/components/twui/layout/Stack";
+import Tag from "@/src/components/twui/elements/Tag";
+import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
+import type { APIResponseObject } from "@moduletrace/bunext/types";
+import Button from "@/src/components/twui/layout/Button";
+
+type Props = {
+ host_id: number | string;
+ current_ip?: string;
+};
+
+export default function HostPublicIpSection({ host_id, current_ip }: Props) {
+ const [ip, setIp] = useState(current_ip || "");
+ const [displayedIp, setDisplayedIp] = useState(current_ip || "");
+ const [typed, setTyped] = useState(false);
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState();
+ const [success, setSuccess] = useState();
+
+ useEffect(() => {
+ if (!current_ip || typed) return;
+ setIp(current_ip);
+ setDisplayedIp(current_ip);
+ }, [current_ip, typed]);
+
+ async function handleUpdate() {
+ setError(undefined);
+ setSuccess(undefined);
+
+ const value = ip.trim();
+
+ if (!value) {
+ setError("Please enter the new public IP address");
+ return;
+ }
+
+ setBusy(true);
+
+ try {
+ const res = await fetchApi<{ [k: string]: any }, APIResponseObject>(
+ "/api/admin/update-host-public-ip",
+ {
+ method: "POST",
+ body: {
+ host_id,
+ public_ip_address: value,
+ },
+ },
+ );
+
+ if (!res.success) {
+ setError(res.msg || "Could not update the host IP");
+ return;
+ }
+
+ setDisplayedIp(value);
+ setSuccess(res.msg || "Host IP updated");
+ } catch (error: any) {
+ setError(error.message || "Could not update the host IP");
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ return (
+
+
+
+ Host public IP
+
+
+ The public IP address clients use to reach this host.
+ Updating it regenerates every client config with the new
+ Endpoint.
+
+
+
+
+ Current:
+
+
+ {displayedIp || "Auto-detected"}
+
+ {
+ setTyped(true);
+ setIp(e.target.value);
+ }}
+ />
+
+
+ {error ? {error} : null}
+ {success ? {success} : null}
+
+ );
+}
diff --git a/src/pages/admin/hosts/[host_id]/clients/(partials)/clients-list.tsx b/src/pages/admin/hosts/[host_id]/clients/(partials)/clients-list.tsx
index ad4cf99..ed4e8fb 100644
--- a/src/pages/admin/hosts/[host_id]/clients/(partials)/clients-list.tsx
+++ b/src/pages/admin/hosts/[host_id]/clients/(partials)/clients-list.tsx
@@ -3,6 +3,7 @@ import Paper from "@/src/components/twui/elements/Paper";
import Center from "@/src/components/twui/layout/Center";
import Span from "@/src/components/twui/layout/Span";
import Stack from "@/src/components/twui/layout/Stack";
+import ClientRow from "@/src/components/general/client-row";
import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
import { AppContext } from "@/src/pages/__root";
import { useContext } from "react";
@@ -11,6 +12,12 @@ type Props = {
host_id: string | number;
};
+const thClass =
+ "px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-[0.08em] " +
+ "text-foreground-light/40 dark:text-foreground-dark/40 whitespace-nowrap";
+
+const thRightClass = `${thClass} text-right`;
+
export default function ClientsList({ host_id }: Props) {
const { pageProps } = useContext(AppContext);
@@ -20,6 +27,7 @@ export default function ClientsList({ host_id }: Props) {
query: {
host_id: { value: host_id },
},
+ order: { field: "created_at", strategy: "DESC" },
},
initial_res: pageProps?.clients || undefined,
});
@@ -44,5 +52,25 @@ export default function ClientsList({ host_id }: Props) {
);
}
- return {};
-}
+ return (
+
+
+
+
+ | Client |
+ Tunnel IP |
+ Allowed IPs |
+ Public key |
+ Created |
+ Actions |
+
+
+
+ {clients.map((client) => (
+
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/admin/hosts/[host_id]/clients/[client_id]/(partials)/client-detail-actions.tsx b/src/pages/admin/hosts/[host_id]/clients/[client_id]/(partials)/client-detail-actions.tsx
new file mode 100644
index 0000000..2b3696a
--- /dev/null
+++ b/src/pages/admin/hosts/[host_id]/clients/[client_id]/(partials)/client-detail-actions.tsx
@@ -0,0 +1,65 @@
+import { useState } from "react";
+import { Pencil, Trash2 } from "lucide-react";
+import Button from "@/src/components/twui/layout/Button";
+import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
+import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
+
+type Props = {
+ client: BUN_SQLITE_WGUI_CLIENTS;
+};
+
+export default function ClientDetailActions({ client }: Props) {
+ const [busy, setBusy] = useState(false);
+
+ const host_id = Number(client?.host_id || 0);
+
+ async function handleDelete() {
+ if (
+ !window.confirm(
+ `Delete client "${client?.name || `#${client?.id}`}"? The client will be removed from the host and its config file deleted.`,
+ )
+ ) {
+ return;
+ }
+
+ setBusy(true);
+
+ const res = await adminCrudHandler({
+ action: "delete",
+ table: "clients",
+ id: client?.id,
+ });
+
+ if (!res.success) {
+ setBusy(false);
+ window.alert(res.msg || "Could not delete client");
+ return;
+ }
+
+ window.location.pathname = `/admin/hosts/${host_id}/clients`;
+ }
+
+ return (
+ <>
+ }
+ href={`/admin/hosts/${host_id}/clients/${client?.id}/edit`}
+ >
+ Edit client
+
+ }
+ disabled={busy}
+ onClick={handleDelete}
+ className="hover:bg-error/10"
+ >
+ {busy ? "Deleting…" : "Delete client"}
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/pages/admin/hosts/[host_id]/clients/[client_id]/(partials)/client-info-row.tsx b/src/pages/admin/hosts/[host_id]/clients/[client_id]/(partials)/client-info-row.tsx
new file mode 100644
index 0000000..7f0f430
--- /dev/null
+++ b/src/pages/admin/hosts/[host_id]/clients/[client_id]/(partials)/client-info-row.tsx
@@ -0,0 +1,54 @@
+import { useState } from "react";
+import { Check, Copy } from "lucide-react";
+import Row from "@/src/components/twui/layout/Row";
+import Span from "@/src/components/twui/layout/Span";
+
+type Props = {
+ label: string;
+ value: string;
+ mono?: boolean;
+ copyable?: boolean;
+};
+
+export default function ClientInfoRow({ label, value, mono, copyable }: Props) {
+ const [copied, setCopied] = useState(false);
+
+ return (
+
+
+ {label}
+
+
+
+ {value}
+
+ {copyable && value && value != "—" ? (
+
+ ) : null}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/admin/hosts/[host_id]/clients/[client_id]/(sections)/client-config-section.tsx b/src/pages/admin/hosts/[host_id]/clients/[client_id]/(sections)/client-config-section.tsx
new file mode 100644
index 0000000..f9a4e54
--- /dev/null
+++ b/src/pages/admin/hosts/[host_id]/clients/[client_id]/(sections)/client-config-section.tsx
@@ -0,0 +1,61 @@
+import { useContext } from "react";
+import AdminCard from "@/src/components/general/admin-card";
+import CodeBlock from "@/src/components/twui/elements/CodeBlock";
+import EmptyContent from "@/src/components/twui/elements/EmptyContent";
+import H3 from "@/src/components/twui/layout/H3";
+import P from "@/src/components/twui/layout/P";
+import Row from "@/src/components/twui/layout/Row";
+import Span from "@/src/components/twui/layout/Span";
+import Stack from "@/src/components/twui/layout/Stack";
+import { AppContext } from "@/src/pages/__root";
+import AceEditor from "@/src/components/twui/editors/AceEditor";
+
+export default function ClientConfigSection() {
+ const { pageProps } = useContext(AppContext);
+
+ const config = pageProps?.client_config;
+
+ if (!config) {
+ return (
+
+
+
+ Client configuration
+
+
+
+
+ Run the client setup for this client to generate its
+ wg.conf on the server.
+ {pageProps?.client_config_path
+ ? ` Expected at ${pageProps.client_config_path}.`
+ : ""}
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Client configuration
+
+ {pageProps?.client_config_path}
+
+
+
+ Use this file with the WireGuard app
+
+
+
+
+
+ );
+}
diff --git a/src/pages/admin/hosts/[host_id]/clients/[client_id]/(sections)/client-info-section.tsx b/src/pages/admin/hosts/[host_id]/clients/[client_id]/(sections)/client-info-section.tsx
new file mode 100644
index 0000000..b1516a4
--- /dev/null
+++ b/src/pages/admin/hosts/[host_id]/clients/[client_id]/(sections)/client-info-section.tsx
@@ -0,0 +1,58 @@
+import { useContext } from "react";
+import AdminCard from "@/src/components/general/admin-card";
+import H3 from "@/src/components/twui/layout/H3";
+import Row from "@/src/components/twui/layout/Row";
+import Stack from "@/src/components/twui/layout/Stack";
+import { AppContext } from "@/src/pages/__root";
+import ClientInfoRow from "../(partials)/client-info-row";
+
+export default function ClientInfoSection() {
+ const { pageProps } = useContext(AppContext);
+
+ const client = pageProps?.client;
+ const host_id = Number(client?.host_id || 0);
+
+ if (!client) {
+ return null;
+ }
+
+ const rows = [
+ {
+ label: "Host",
+ value: host_id ? `Host #${host_id}` : "Main host",
+ },
+ {
+ label: "Tunnel IP",
+ value: client.wg_ip_address || "—",
+ mono: true,
+ },
+ {
+ label: "Allowed IPs",
+ value: client.allowed_ips || "—",
+ mono: true,
+ },
+ {
+ label: "Public key",
+ value: client.public_key || "—",
+ mono: true,
+ copyable: true,
+ },
+ {
+ label: "Notes",
+ value: client.notes || "—",
+ },
+ ];
+
+ return (
+
+
+
+ Client information
+
+ {rows.map((row) => (
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/admin/hosts/[host_id]/clients/[client_id]/(sections)/client-qr-section.tsx b/src/pages/admin/hosts/[host_id]/clients/[client_id]/(sections)/client-qr-section.tsx
new file mode 100644
index 0000000..bdc4130
--- /dev/null
+++ b/src/pages/admin/hosts/[host_id]/clients/[client_id]/(sections)/client-qr-section.tsx
@@ -0,0 +1,61 @@
+import { useContext } from "react";
+import AdminCard from "@/src/components/general/admin-card";
+import H3 from "@/src/components/twui/layout/H3";
+import Img from "@/src/components/twui/layout/Img";
+import P from "@/src/components/twui/layout/P";
+import Row from "@/src/components/twui/layout/Row";
+import Stack from "@/src/components/twui/layout/Stack";
+import { AppContext } from "@/src/pages/__root";
+
+export default function ClientQRSection() {
+ const { pageProps } = useContext(AppContext);
+
+ const qr_data_uri = pageProps?.client_qr_data_uri;
+
+ return (
+
+
+
+ Add via QR code
+
+ {qr_data_uri ? (
+
+
+
+
+ Scan with the official WireGuard app, or import the
+ config file manually via the config tab
+
+
+
+ ) : (
+
+
+ No QR code available
+
+
+ Generate the client config first — the QR code is
+ created from the stored wg.conf
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/admin/hosts/[host_id]/clients/[client_id]/edit/(sections)/edit-client-form-section.tsx b/src/pages/admin/hosts/[host_id]/clients/[client_id]/edit/(sections)/edit-client-form-section.tsx
new file mode 100644
index 0000000..a50431c
--- /dev/null
+++ b/src/pages/admin/hosts/[host_id]/clients/[client_id]/edit/(sections)/edit-client-form-section.tsx
@@ -0,0 +1,20 @@
+import Section from "@/src/components/twui/layout/Section";
+import AddClientForm from "../../../add-client/(partials)/client-form/add-client-form";
+import { useContext } from "react";
+import { AppContext } from "@/src/pages/__root";
+
+export default function EditClientFormSection() {
+ const { pageProps, query } = useContext(AppContext);
+
+ const host_id = Number(query?.host_id || 0);
+
+ return (
+
+ );
+}
diff --git a/src/pages/admin/hosts/[host_id]/clients/[client_id]/edit/index.server.ts b/src/pages/admin/hosts/[host_id]/clients/[client_id]/edit/index.server.ts
new file mode 100644
index 0000000..0454c59
--- /dev/null
+++ b/src/pages/admin/hosts/[host_id]/clients/[client_id]/edit/index.server.ts
@@ -0,0 +1,28 @@
+import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
+import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
+import BunSQLite from "@moduletrace/bun-sqlite";
+import type { BunextPageServerFn } from "@moduletrace/bunext/types";
+
+type ClientEditPageQuery = PageQueryObject & {
+ client_id?: string;
+};
+
+const server: BunextPageServerFn = async ({ query }) => {
+ const page_query = query as ClientEditPageQuery;
+
+ const client_res = await BunSQLite.select<
+ BUN_SQLITE_WGUI_CLIENTS,
+ TableType
+ >({
+ table: "clients",
+ targetId: page_query.client_id,
+ });
+
+ return {
+ props: {
+ client: client_res.singleRes || null,
+ },
+ };
+};
+
+export default server;
\ No newline at end of file
diff --git a/src/pages/admin/hosts/[host_id]/clients/[client_id]/edit/index.tsx b/src/pages/admin/hosts/[host_id]/clients/[client_id]/edit/index.tsx
new file mode 100644
index 0000000..4c31e07
--- /dev/null
+++ b/src/pages/admin/hosts/[host_id]/clients/[client_id]/edit/index.tsx
@@ -0,0 +1,55 @@
+import { ArrowLeft } from "lucide-react";
+import { useContext } from "react";
+import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
+import AdminHero from "@/src/components/general/admin-hero";
+import { SiteData } from "@/src/data/site-data";
+import { AppContext } from "@/src/pages/__root";
+import Button from "@/src/components/twui/layout/Button";
+import Divider from "@/src/components/twui/layout/Divider";
+import Stack from "@/src/components/twui/layout/Stack";
+import EmptyContent from "@/src/components/twui/elements/EmptyContent";
+import EditClientFormSection from "./(sections)/edit-client-form-section";
+
+export default function AdminEditClientPage() {
+ const { pageProps, query } = useContext(AppContext);
+
+ const host_id = Number(query?.host_id || pageProps?.client?.host_id || 0);
+
+ return (
+ <>
+ }
+ href={`/admin/hosts/${host_id}/clients/${pageProps?.client?.id}`}
+ >
+ View client
+
+ }
+ />
+
+
+
+
+ {pageProps?.client ? (
+
+ ) : (
+
+ )}
+
+ >
+ );
+}
+
+export const meta: BunextPageModuleMeta = {
+ title: `Edit Client | ${SiteData["SiteName"]}`,
+ description: `WireGuard edit client page`,
+};
\ No newline at end of file
diff --git a/src/pages/admin/hosts/[host_id]/clients/[client_id]/index.server.ts b/src/pages/admin/hosts/[host_id]/clients/[client_id]/index.server.ts
new file mode 100644
index 0000000..512a8ee
--- /dev/null
+++ b/src/pages/admin/hosts/[host_id]/clients/[client_id]/index.server.ts
@@ -0,0 +1,69 @@
+import type {
+ BUN_SQLITE_WGUI_CLIENTS,
+ BUN_SQLITE_WGUI_HOSTS,
+} from "@/db/types/db";
+import { AppData } from "@/src/data/app-data";
+import generateWireguardClientQR from "@/src/functions/backend/setup/generate-wireguard-client-qr";
+import readWireguardClientConfig from "@/src/functions/backend/setup/read-wireguard-client-config";
+import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
+import BunSQLite from "@moduletrace/bun-sqlite";
+import type { BunextPageServerFn } from "@moduletrace/bunext/types";
+
+type ClientPageQuery = PageQueryObject & {
+ client_id?: string;
+};
+
+const server: BunextPageServerFn = async ({ query }) => {
+ const page_query = query as ClientPageQuery;
+
+ const client_res = await BunSQLite.select<
+ BUN_SQLITE_WGUI_CLIENTS,
+ TableType
+ >({
+ table: "clients",
+ targetId: page_query.client_id,
+ });
+
+ const client = client_res.singleRes || null;
+
+ const host_id = Number(
+ client?.host_id ||
+ page_query.host_id ||
+ AppData["WireguardHostID"],
+ );
+
+ let host: BUN_SQLITE_WGUI_HOSTS | null = null;
+
+ if (host_id && host_id != AppData["WireguardHostID"]) {
+ const host_res = await BunSQLite.select<
+ BUN_SQLITE_WGUI_HOSTS,
+ TableType
+ >({
+ table: "hosts",
+ targetId: host_id,
+ });
+
+ host = host_res.singleRes || null;
+ }
+
+ const config_res = await readWireguardClientConfig({
+ client_id: client?.id,
+ host_id,
+ });
+
+ const qr_res = await generateWireguardClientQR({
+ config: config_res.config,
+ });
+
+ return {
+ props: {
+ client,
+ host,
+ client_config: config_res.config,
+ client_config_path: config_res.config_path,
+ client_qr_data_uri: qr_res.qr_data_uri,
+ },
+ };
+};
+
+export default server;
\ No newline at end of file
diff --git a/src/pages/admin/hosts/[host_id]/clients/[client_id]/index.tsx b/src/pages/admin/hosts/[host_id]/clients/[client_id]/index.tsx
new file mode 100644
index 0000000..3797277
--- /dev/null
+++ b/src/pages/admin/hosts/[host_id]/clients/[client_id]/index.tsx
@@ -0,0 +1,69 @@
+import { ArrowLeft } from "lucide-react";
+import { useContext } from "react";
+import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
+import AdminHero from "@/src/components/general/admin-hero";
+import { SiteData } from "@/src/data/site-data";
+import { AppContext } from "@/src/pages/__root";
+import Button from "@/src/components/twui/layout/Button";
+import Divider from "@/src/components/twui/layout/Divider";
+import Stack from "@/src/components/twui/layout/Stack";
+import EmptyContent from "@/src/components/twui/elements/EmptyContent";
+import ClientInfoSection from "./(sections)/client-info-section";
+import ClientConfigSection from "./(sections)/client-config-section";
+import ClientQRSection from "./(sections)/client-qr-section";
+import ClientDetailActions from "./(partials)/client-detail-actions";
+
+export default function AdminClientDetailPage() {
+ const { pageProps } = useContext(AppContext);
+
+ const host_id = Number(
+ pageProps?.host?.id || pageProps?.client?.host_id || 0,
+ );
+
+ const desc =
+ pageProps?.client?.notes ||
+ "Client details and WireGuard configuration";
+
+ return (
+ <>
+
+ }
+ href={`/admin/hosts/${host_id}/clients`}
+ >
+ Back to clients
+
+ {pageProps?.client ? (
+
+ ) : null}
+ >
+ }
+ />
+
+
+
+
+ {pageProps?.client ? (
+ <>
+
+
+
+ >
+ ) : (
+
+ )}
+
+ >
+ );
+}
+
+export const meta: BunextPageModuleMeta = {
+ title: `Client | ${SiteData["SiteName"]}`,
+ description: `WireGuard client details page`,
+};
diff --git a/src/pages/admin/hosts/[host_id]/clients/add-client/(functions)/grab-next-available-client-ip.ts b/src/pages/admin/hosts/[host_id]/clients/add-client/(functions)/grab-next-available-client-ip.ts
index c45473a..09048a4 100644
--- a/src/pages/admin/hosts/[host_id]/clients/add-client/(functions)/grab-next-available-client-ip.ts
+++ b/src/pages/admin/hosts/[host_id]/clients/add-client/(functions)/grab-next-available-client-ip.ts
@@ -31,7 +31,7 @@ export default async function grabNextAvailableClientIp({
setForm((prev) => ({
...prev,
wg_ip_address: res.msg,
- allowed_ips: `${res.msg}/32`,
+ allowed_ips: `${res.msg}/24`,
}));
return {
diff --git a/src/pages/admin/hosts/[host_id]/clients/add-client/(functions)/submit-add-client-form.ts b/src/pages/admin/hosts/[host_id]/clients/add-client/(functions)/submit-add-client-form.ts
index 68017e5..b55404f 100644
--- a/src/pages/admin/hosts/[host_id]/clients/add-client/(functions)/submit-add-client-form.ts
+++ b/src/pages/admin/hosts/[host_id]/clients/add-client/(functions)/submit-add-client-form.ts
@@ -18,23 +18,29 @@ export default async function submitAddClientForm(
{ host_id }: Params,
) {
try {
- if (!window.confirm(`Create new client?`)) {
+ const confirm_msg = existing_full?.id
+ ? `Update this Client?`
+ : `Create new client?`;
+
+ if (!window.confirm(confirm_msg)) {
return;
}
const final_host_id = Number(app_context.query?.host_id || 0);
- const new_client_data: BUN_SQLITE_WGUI_CLIENTS = _.omitBy(
- {
- name: form.name || "",
- wg_ip_address: form.wg_ip_address || "",
- allowed_ips: form.allowed_ips || "",
- notes: form.notes || "",
- host_id: final_host_id,
- user_id: app_context?.user?.id,
- },
- (value) => value === undefined,
- );
+ const new_client_data: BUN_SQLITE_WGUI_CLIENTS =
+ _.omitBy(
+ {
+ name: form.name || "",
+ wg_ip_address: form.wg_ip_address || "",
+ allowed_ips: form.allowed_ips || "",
+ notes: form.notes || "",
+ host_id: final_host_id,
+ user_id: app_context?.user?.id,
+ allow_all_ips: form.allow_all_ips,
+ },
+ (value) => value === undefined,
+ );
setLoading(true);
@@ -53,7 +59,7 @@ export default async function submitAddClientForm(
if (res.success) {
if (existing_full?.id) {
- window.location.reload();
+ window.location.pathname = `/admin/hosts/${final_host_id}/clients/${existing_full.id}`;
} else {
window.location.pathname = `/admin/hosts/${final_host_id}/clients`;
}
diff --git a/src/pages/admin/hosts/[host_id]/clients/add-client/(partials)/client-form/add-client-form-allowed-ips.tsx b/src/pages/admin/hosts/[host_id]/clients/add-client/(partials)/client-form/add-client-form-allowed-ips.tsx
index a13637f..2c3dd10 100644
--- a/src/pages/admin/hosts/[host_id]/clients/add-client/(partials)/client-form/add-client-form-allowed-ips.tsx
+++ b/src/pages/admin/hosts/[host_id]/clients/add-client/(partials)/client-form/add-client-form-allowed-ips.tsx
@@ -1,5 +1,7 @@
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
+import Checkbox from "@/src/components/twui/form/Checkbox";
import Input from "@/src/components/twui/form/Input";
+import Stack from "@/src/components/twui/layout/Stack";
import useFormInit from "@/src/hooks/use-form-init";
export default function AddClientFormAllowedIps({
@@ -7,17 +9,31 @@ export default function AddClientFormAllowedIps({
setForm,
}: ReturnType>) {
return (
- {
- setForm((prev) => ({
- ...prev,
- allowed_ips: e.target.value,
- }));
- }}
- value={form.allowed_ips}
- showLabel
- />
+
+ {form.allow_all_ips ? null : (
+ {
+ setForm((prev) => ({
+ ...prev,
+ allowed_ips: e.target.value,
+ }));
+ }}
+ value={form.allowed_ips}
+ showLabel
+ />
+ )}
+ {
+ setForm((prev) => ({
+ ...prev,
+ allow_all_ips: value ? 1 : 0,
+ }));
+ }}
+ />
+
);
}
diff --git a/src/pages/admin/hosts/[host_id]/clients/add-client/(partials)/client-form/add-client-form-wg-ip-address.tsx b/src/pages/admin/hosts/[host_id]/clients/add-client/(partials)/client-form/add-client-form-wg-ip-address.tsx
index 573a69f..e01aabf 100644
--- a/src/pages/admin/hosts/[host_id]/clients/add-client/(partials)/client-form/add-client-form-wg-ip-address.tsx
+++ b/src/pages/admin/hosts/[host_id]/clients/add-client/(partials)/client-form/add-client-form-wg-ip-address.tsx
@@ -48,7 +48,7 @@ export default function AddClientFormWgIpAddress({
setForm((prev) => ({
...prev,
wg_ip_address: e.target.value,
- allowed_ips: `${e.target.value}/32`,
+ allowed_ips: `${e.target.value.replace(/\.\d+$/, "")}.0/24`,
}));
}}
value={form.wg_ip_address}
diff --git a/src/pages/admin/hosts/[host_id]/index.tsx b/src/pages/admin/hosts/[host_id]/index.tsx
index 88effb1..555eae6 100644
--- a/src/pages/admin/hosts/[host_id]/index.tsx
+++ b/src/pages/admin/hosts/[host_id]/index.tsx
@@ -3,8 +3,16 @@ import AdminHero from "@/src/components/general/admin-hero";
import { SiteData } from "@/src/data/site-data";
import Button from "@/src/components/twui/layout/Button";
import Divider from "@/src/components/twui/layout/Divider";
+import Stack from "@/src/components/twui/layout/Stack";
+import HostPublicIpSection from "../(sections)/host-public-ip-section";
+import { useContext } from "react";
+import { AppContext } from "@/src/pages/__root";
export default function AdminSingleHostPage() {
+ const { pageProps } = useContext(AppContext);
+
+ const host_id = Number(pageProps?.host?.id || 0);
+
return (
<>
+
+
+ {pageProps?.host ? (
+
+ ) : null}
+
>
);
}
@@ -27,4 +44,4 @@ export default function AdminSingleHostPage() {
export const meta: BunextPageModuleMeta = {
title: `Host | ${SiteData["SiteName"]}`,
description: `WireGuard host page`,
-};
+};
\ No newline at end of file
diff --git a/src/pages/admin/hosts/index.tsx b/src/pages/admin/hosts/index.tsx
index f14cfbd..55d4bcb 100644
--- a/src/pages/admin/hosts/index.tsx
+++ b/src/pages/admin/hosts/index.tsx
@@ -14,10 +14,15 @@ import checkIfMainHostIsSet from "@/src/functions/check-if-main-host-is-set";
import SetupMainHostButton from "@/src/components/general/setup-main-host-button";
import Row from "@/src/components/twui/layout/Row";
import { Plus } from "lucide-react";
+import HostPublicIpSection from "./(sections)/host-public-ip-section";
export default function AdminHostPage() {
const { hosts, variables, clients } = useHostData();
+ const main_host_public_ip = variables?.find(
+ (v) => v.key == "main_host_public_ip_address",
+ )?.value;
+
const is_main_host_set = checkIfMainHostIsSet({ variables });
if (!is_main_host_set) {
@@ -57,11 +62,20 @@ export default function AdminHostPage() {
Main Host
+
}
size="small"
- href={`/admin/hosts/0/clients/add`}
+ href={`/admin/hosts/0/clients/add-client`}
>
Add Client
@@ -71,6 +85,10 @@ export default function AdminHostPage() {
variables={variables}
clients={clients}
/>
+
(
- {
- table: "hosts",
- targetId: client.host_id,
- },
- );
+ const host_res = await BunSQLite.select<
+ BUN_SQLITE_WGUI_HOSTS,
+ TableType
+ >({
+ table: "hosts",
+ targetId: client.host_id,
+ });
host = host_res.singleRes || undefined;
}
return await setupWireguardClient({ client, host, user });
-}
\ No newline at end of file
+}
diff --git a/src/pages/api/admin/crud/(tables)/clients/del.ts b/src/pages/api/admin/crud/(tables)/clients/del.ts
index b51ea15..0a32166 100644
--- a/src/pages/api/admin/crud/(tables)/clients/del.ts
+++ b/src/pages/api/admin/crud/(tables)/clients/del.ts
@@ -1,13 +1,23 @@
import _ from "lodash";
+import { execSync } from "node:child_process";
+import path from "node:path";
import type { AdminCrudAPIParams, TableType } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
+import grabDirNames from "@/src/utils/grab-dir-names";
+import grabHostDirnames from "@/src/functions/backend/setup/grab-host-dir-names";
+import setupWireguardHost from "@/src/functions/backend/setup/setup-wireguard-host";
import type {
BUN_SQLITE_WGUI_ALL_TYPEDEFS,
BUN_SQLITE_WGUI_CLIENTS,
+ BUN_SQLITE_WGUI_HOSTS,
} from "@/db/types/db";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { ServerQueryParam } from "@moduletrace/bun-sqlite/dist/types";
+import grabClientDirnames from "@/src/functions/backend/setup/grab-client-dir-names";
+
+const { WGUI_LIB_HOSTS_CONFIGS_DIR, WGUI_LIB_HOST_CLIENTS_DIR_NAME } =
+ grabDirNames();
export default async function (
params: AdminCrudAPIParams,
@@ -43,21 +53,78 @@ export default async function (
if (!clients_to_delete.payload) {
return {
success: false,
- msg: `No Media to Delete.`,
+ msg: `No Client to Delete.`,
};
}
+ const affected_host_ids = new Set();
+
for (let i = 0; i < clients_to_delete.payload.length; i++) {
const client = clients_to_delete.payload[i];
if (!client?.id) continue;
+ const host_id = Number(client.host_id || 0);
+
+ const { CLIENT_DIR } = grabClientDirnames({
+ client,
+ host_id,
+ });
+
+ try {
+ execSync(`rm -rf ${CLIENT_DIR}`, { encoding: "utf-8" });
+ } catch (error) {}
+
+ affected_host_ids.add(host_id);
+
await BunSQLite.delete({
table: "clients",
targetId: client.id,
});
}
+ const host_setup_errors: string[] = [];
+
+ for (const host_id of affected_host_ids) {
+ try {
+ let host: BUN_SQLITE_WGUI_HOSTS | undefined;
+
+ if (host_id) {
+ const host_res = await BunSQLite.select<
+ BUN_SQLITE_WGUI_HOSTS,
+ TableType
+ >({
+ table: "hosts",
+ targetId: host_id,
+ });
+
+ host = host_res.singleRes || undefined;
+ }
+
+ const host_setup_res = await setupWireguardHost({
+ host,
+ user,
+ is_update_after_client_setup: true,
+ });
+
+ if (!host_setup_res.success) {
+ host_setup_errors.push(
+ `Host ${host_id}: ${host_setup_res.msg}`,
+ );
+ }
+ } catch (error: any) {
+ host_setup_errors.push(`Host ${host_id}: ${error.message}`);
+ }
+ }
+
+ if (host_setup_errors[0]) {
+ return {
+ success: false,
+ msg: `Client deleted but host config could not be regenerated: ${host_setup_errors.join("; ")}`,
+ };
+ }
+
return {
success: true,
+ msg: `${clients_to_delete.payload.length} client(s) deleted`,
};
}
diff --git a/src/pages/api/admin/crud/(tables)/clients/put.ts b/src/pages/api/admin/crud/(tables)/clients/put.ts
index 9d7bd3a..e5d85c6 100644
--- a/src/pages/api/admin/crud/(tables)/clients/put.ts
+++ b/src/pages/api/admin/crud/(tables)/clients/put.ts
@@ -32,7 +32,7 @@ export default async function (
const PUT = await BunSQLite.update({
table,
- data: body?.update_data,
+ data: body.update_data,
targetId,
query: final_sql_query,
});
@@ -44,13 +44,9 @@ export default async function (
});
if (!client_setup_res.success) {
- PUT.debug = {
- ...(PUT.debug || {}),
- client_setup: client_setup_res,
- };
- PUT.error = client_setup_res.msg || client_setup_res.error;
+ return client_setup_res;
}
}
return PUT;
-}
\ No newline at end of file
+}
diff --git a/src/pages/api/admin/update-host-public-ip.ts b/src/pages/api/admin/update-host-public-ip.ts
new file mode 100644
index 0000000..4a82ad9
--- /dev/null
+++ b/src/pages/api/admin/update-host-public-ip.ts
@@ -0,0 +1,53 @@
+import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
+import userAuth from "@/src/functions/backend/auth/user-auth";
+import updateWireguardHostPublicIP from "@/src/functions/backend/setup/update-wireguard-host-public-ip";
+import type { ApiReqParams } from "@/src/types";
+import type {
+ APIResponseObject,
+ BunextAPIRouteHandler,
+} from "@moduletrace/bunext/types";
+
+export const handler: BunextAPIRouteHandler = async (
+ params,
+) => {
+ const req = params.req;
+ const body = params.body as ApiReqParams;
+
+ if (req.method !== "POST") {
+ return {
+ success: false,
+ };
+ }
+
+ const { user, user_types } = await userAuth({ req });
+
+ if (!user?.logged_in_status) {
+ return {
+ success: false,
+ msg: `Unauthorized`,
+ logoutUser: true,
+ };
+ }
+
+ try {
+ const is_super_admin = checkUserAccess({ user_types });
+
+ if (!is_super_admin.success) {
+ return {
+ success: false,
+ msg: `Unauthorized`,
+ };
+ }
+
+ return await updateWireguardHostPublicIP({
+ host_id: body?.host_id,
+ public_ip_address: body?.public_ip_address,
+ user,
+ });
+ } catch (error: any) {
+ return {
+ success: false,
+ msg: error.message,
+ };
+ }
+};
\ No newline at end of file
diff --git a/src/scripts/wg-quick-manage.sh b/src/scripts/wg-quick-manage.sh
index 83035f8..cc43c97 100755
--- a/src/scripts/wg-quick-manage.sh
+++ b/src/scripts/wg-quick-manage.sh
@@ -41,11 +41,11 @@ case "$ACTION" in
;;
esac
-EXPECTED_CONFIG_PATH="/var/lib/wgui/hosts/${INTERFACE}.conf"
-if [ "$CONFIG_PATH" != "$EXPECTED_CONFIG_PATH" ]; then
- echo "error: config path must be $EXPECTED_CONFIG_PATH" >&2
- exit 1
-fi
+# EXPECTED_CONFIG_PATH="/var/lib/wgui/hosts/${INTERFACE}.conf"
+# if [ "$CONFIG_PATH" != "$EXPECTED_CONFIG_PATH" ]; then
+# echo "error: config path must be $EXPECTED_CONFIG_PATH" >&2
+# exit 1
+# fi
WG_QUICK_BIN="$(command -v wg-quick)"
if [ -z "$WG_QUICK_BIN" ]; then
diff --git a/src/server.ts b/src/server.ts
index 11a7b67..2fbefe2 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -2,6 +2,8 @@ import bunext from "@moduletrace/bunext";
import syncWireguardHosts from "./functions/backend/setup/sync-wireguard-hosts";
import handleMediaServer from "./functions/server/media/handle-media-server";
import { SiteData } from "./data/site-data";
+import { execFile, execSync } from "node:child_process";
+import path from "node:path";
declare global {}
@@ -28,6 +30,15 @@ const server = Bun.serve({
port,
});
-bunext.bunextLog.info(`Server running on http://localhost:${server.port} ...`);
+bunext.bunextLog.build(`Setting up wireguard ...`);
+execSync(path.join(__dirname, "scripts", "install-wg-ui.sh"), {
+ stdio: "ignore",
+ env: {
+ ...process.env,
+ },
+});
+bunext.bunextLog.success(`Wireguard setup complete!`);
-syncWireguardHosts();
+await syncWireguardHosts();
+
+bunext.bunextLog.info(`Server running on http://localhost:${server.port} ...`);
diff --git a/src/types/index.ts b/src/types/index.ts
index a97eb4e..ee103ae 100755
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -40,6 +40,9 @@ export type PagePropsType = {
hosts?: BUN_SQLITE_WGUI_HOSTS[] | null;
client?: BUN_SQLITE_WGUI_CLIENTS | null;
clients?: BUN_SQLITE_WGUI_CLIENTS[] | null;
+ client_config?: string | null;
+ client_config_path?: string | null;
+ client_qr_data_uri?: string | null;
environment?: string;
envs?: {
R2_PUBLIC_DOMAIN?: string;
@@ -199,6 +202,7 @@ export type ApiReqParams<
user_id?: string | number | null;
dependent_id?: string | number | null;
host_id?: string | number | null;
+ public_ip_address?: string | null;
media_base_64?: string;
media_base_64_data_url?: string;