Finalized ssh tunnels, updatetd database schemas, started on config editor.

This commit is contained in:
LukeGus
2025-07-21 01:25:38 -05:00
parent cfaa04e42c
commit 547701378f
18 changed files with 4791 additions and 25 deletions

898
package-lock.json generated
View File

@@ -26,6 +26,10 @@
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.11",
"@types/bcryptjs": "^2.4.6",
"@uiw/codemirror-extensions-hyper-link": "^4.24.1",
"@uiw/codemirror-extensions-langs": "^4.24.1",
"@uiw/codemirror-themes": "^4.24.1",
"@uiw/react-codemirror": "^4.24.1",
"@xterm/addon-attach": "^0.11.0",
"@xterm/addon-clipboard": "^0.1.0",
"@xterm/addon-fit": "^0.10.0",
@@ -37,7 +41,9 @@
"chalk": "^4.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^17.2.0",
"drizzle-orm": "^0.44.3",
"express": "^5.1.0",
"jsonwebtoken": "^9.0.2",
@@ -91,6 +97,423 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.18.6",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
"integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
"integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-angular": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@codemirror/lang-angular/-/lang-angular-0.1.4.tgz",
"integrity": "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==",
"license": "MIT",
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-javascript": "^6.1.2",
"@codemirror/language": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.3"
}
},
"node_modules/@codemirror/lang-cpp": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz",
"integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/cpp": "^1.0.0"
}
},
"node_modules/@codemirror/lang-css": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.2",
"@lezer/css": "^1.1.7"
}
},
"node_modules/@codemirror/lang-go": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz",
"integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/go": "^1.0.0"
}
},
"node_modules/@codemirror/lang-html": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
"integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/css": "^1.1.0",
"@lezer/html": "^1.3.0"
}
},
"node_modules/@codemirror/lang-java": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz",
"integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/java": "^1.0.0"
}
},
"node_modules/@codemirror/lang-javascript": {
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/javascript": "^1.0.0"
}
},
"node_modules/@codemirror/lang-json": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
"integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/lang-less": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-less/-/lang-less-6.0.2.tgz",
"integrity": "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==",
"license": "MIT",
"dependencies": {
"@codemirror/lang-css": "^6.2.0",
"@codemirror/language": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@codemirror/lang-lezer": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-lezer/-/lang-lezer-6.0.2.tgz",
"integrity": "sha512-mcVAf8lw+sCfSlr2ivMqV8JtNmOQjSXdA1vHKRtoW0OZsz1k6qhF+DX0K2TbWlAThqiGgRkRSZyYzIoEtKB2uQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/lezer": "^1.0.0"
}
},
"node_modules/@codemirror/lang-liquid": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.2.3.tgz",
"integrity": "sha512-yeN+nMSrf/lNii3FJxVVEGQwFG0/2eDyH6gNOj+TGCa0hlNO4bhQnoO5ISnd7JOG+7zTEcI/GOoyraisFVY7jQ==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.1"
}
},
"node_modules/@codemirror/lang-markdown": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.3.tgz",
"integrity": "sha512-1fn1hQAPWlSSMCvnF810AkhWpNLkJpl66CRfIy3vVl20Sl4NwChkorCHqpMtNbXr1EuMJsrDnhEpjZxKZ2UX3A==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.7.1",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.3.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.2.1",
"@lezer/markdown": "^1.0.0"
}
},
"node_modules/@codemirror/lang-php": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz",
"integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==",
"license": "MIT",
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/php": "^1.0.0"
}
},
"node_modules/@codemirror/lang-python": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz",
"integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.3.2",
"@codemirror/language": "^6.8.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.1",
"@lezer/python": "^1.1.4"
}
},
"node_modules/@codemirror/lang-rust": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz",
"integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/rust": "^1.0.0"
}
},
"node_modules/@codemirror/lang-sass": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz",
"integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==",
"license": "MIT",
"dependencies": {
"@codemirror/lang-css": "^6.2.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.2",
"@lezer/sass": "^1.0.0"
}
},
"node_modules/@codemirror/lang-sql": {
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.9.0.tgz",
"integrity": "sha512-xmtpWqKSgum1B1J3Ro6rf7nuPqf2+kJQg5SjrofCAcyCThOe0ihSktSoXfXuhQBnwx1QbmreBbLJM5Jru6zitg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@codemirror/lang-vue": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz",
"integrity": "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==",
"license": "MIT",
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-javascript": "^6.1.2",
"@codemirror/language": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.1"
}
},
"node_modules/@codemirror/lang-wast": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.2.tgz",
"integrity": "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@codemirror/lang-xml": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
"integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/xml": "^1.0.0"
}
},
"node_modules/@codemirror/lang-yaml": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz",
"integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.0.0",
"@lezer/yaml": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz",
"integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/language-data": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.5.1.tgz",
"integrity": "sha512-0sWxeUSNlBr6OmkqybUTImADFUP0M3P0IiSde4nc24bz/6jIYzqYSgkOSLS+CBIoW1vU8Q9KUWXscBXeoMVC9w==",
"license": "MIT",
"dependencies": {
"@codemirror/lang-angular": "^0.1.0",
"@codemirror/lang-cpp": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
"@codemirror/lang-go": "^6.0.0",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-java": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.0",
"@codemirror/lang-json": "^6.0.0",
"@codemirror/lang-less": "^6.0.0",
"@codemirror/lang-liquid": "^6.0.0",
"@codemirror/lang-markdown": "^6.0.0",
"@codemirror/lang-php": "^6.0.0",
"@codemirror/lang-python": "^6.0.0",
"@codemirror/lang-rust": "^6.0.0",
"@codemirror/lang-sass": "^6.0.0",
"@codemirror/lang-sql": "^6.0.0",
"@codemirror/lang-vue": "^0.1.1",
"@codemirror/lang-wast": "^6.0.0",
"@codemirror/lang-xml": "^6.0.0",
"@codemirror/lang-yaml": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/legacy-modes": "^6.4.0"
}
},
"node_modules/@codemirror/legacy-modes": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.1.tgz",
"integrity": "sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.8.5",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
"integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.5.11",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
"integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/theme-one-dark": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.38.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
"integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -848,6 +1271,218 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@lezer/common": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"license": "MIT"
},
"node_modules/@lezer/cpp": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.3.tgz",
"integrity": "sha512-ykYvuFQKGsRi6IcE+/hCSGUhb/I4WPjd3ELhEblm2wS2cOznDFzO+ubK2c+ioysOnlZ3EduV+MVQFCPzAIoY3w==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/css": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz",
"integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@lezer/go": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz",
"integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@lezer/highlight": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/html": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
"integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/java": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz",
"integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/javascript": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz",
"integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@lezer/json": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/lezer": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@lezer/lezer/-/lezer-1.1.2.tgz",
"integrity": "sha512-O8yw3CxPhzYHB1hvwbdozjnAslhhR8A5BH7vfEMof0xk3p+/DFDfZkA9Tde6J+88WgtwaHy4Sy6ThZSkaI0Evw==",
"license": "MIT",
"dependencies": {
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/markdown": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.4.3.tgz",
"integrity": "sha512-kfw+2uMrQ/wy/+ONfrH83OkdFNM0ye5Xq96cLlaCy7h5UT9FO54DU4oRoIc0CSBh5NWmWuiIJA7NGLMJbQ+Oxg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@lezer/php": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.3.tgz",
"integrity": "sha512-NDwgktd5qh/EfEn7Dogf2N6eNnC5WPJ5NslB8nKhgXSuH2uSJamCEom1g4VGo+ibfoADK8D69NMCMhuuYbVskg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.1.0"
}
},
"node_modules/@lezer/python": {
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz",
"integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/rust": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz",
"integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/sass": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lezer/sass/-/sass-1.1.0.tgz",
"integrity": "sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/xml": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz",
"integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/yaml": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.3.tgz",
"integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.4.0"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@nextjournal/lang-clojure": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@nextjournal/lang-clojure/-/lang-clojure-1.0.0.tgz",
"integrity": "sha512-gOCV71XrYD0DhwGoPMWZmZ0r92/lIHsqQu9QWdpZYYBwiChNwMO4sbVMP7eTuAqffFB2BTtCSC+1skSH9d3bNg==",
"license": "ISC",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@nextjournal/lezer-clojure": "1.0.0"
}
},
"node_modules/@nextjournal/lezer-clojure": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@nextjournal/lezer-clojure/-/lezer-clojure-1.0.0.tgz",
"integrity": "sha512-VZyuGu4zw5mkTOwQBTaGVNWmsOZAPw5ZRxu1/Knk/Xfs7EDBIogwIs5UXTYkuECX5ZQB8eOB+wKA2pc7VyqaZQ==",
"license": "ISC",
"dependencies": {
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1918,6 +2553,67 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
"node_modules/@replit/codemirror-lang-csharp": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@replit/codemirror-lang-csharp/-/codemirror-lang-csharp-6.2.0.tgz",
"integrity": "sha512-6utbaWkoymhoAXj051mkRp+VIJlpwUgCX9Toevz3YatiZsz512fw3OVCedXQx+WcR0wb6zVHjChnuxqfCLtFVQ==",
"license": "MIT",
"peerDependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@replit/codemirror-lang-nix": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@replit/codemirror-lang-nix/-/codemirror-lang-nix-6.0.1.tgz",
"integrity": "sha512-lvzjoYn9nfJzBD5qdm3Ut6G3+Or2wEacYIDJ49h9+19WSChVnxv4ojf+rNmQ78ncuxIt/bfbMvDLMeMP0xze6g==",
"license": "MIT",
"peerDependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@replit/codemirror-lang-solidity": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@replit/codemirror-lang-solidity/-/codemirror-lang-solidity-6.0.2.tgz",
"integrity": "sha512-/dpTVH338KFV6SaDYYSadkB4bI/0B0QRF/bkt1XS3t3QtyR49mn6+2k0OUQhvt2ZSO7kt10J+OPilRAtgbmX0w==",
"license": "MIT",
"dependencies": {
"@lezer/highlight": "^1.2.0"
},
"peerDependencies": {
"@codemirror/language": "^6.0.0"
}
},
"node_modules/@replit/codemirror-lang-svelte": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@replit/codemirror-lang-svelte/-/codemirror-lang-svelte-6.0.0.tgz",
"integrity": "sha512-U2OqqgMM6jKelL0GNWbAmqlu1S078zZNoBqlJBW+retTc5M4Mha6/Y2cf4SVg6ddgloJvmcSpt4hHrVoM4ePRA==",
"license": "MIT",
"peerDependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.1",
"@codemirror/lang-html": "^6.2.0",
"@codemirror/lang-javascript": "^6.1.1",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0",
"@lezer/javascript": "^1.2.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.11",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz",
@@ -3186,6 +3882,133 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@uiw/codemirror-extensions-basic-setup": {
"version": "4.24.1",
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.24.1.tgz",
"integrity": "sha512-o1m1a8eUS3fWERMbDFvN8t8sZUFPgDKNemmlQ5Ot2vKm+Ax84lKP1dhEFgkiOaZ1bDHk4T5h6SjHuTghrJHKww==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@codemirror/autocomplete": ">=6.0.0",
"@codemirror/commands": ">=6.0.0",
"@codemirror/language": ">=6.0.0",
"@codemirror/lint": ">=6.0.0",
"@codemirror/search": ">=6.0.0",
"@codemirror/state": ">=6.0.0",
"@codemirror/view": ">=6.0.0"
}
},
"node_modules/@uiw/codemirror-extensions-hyper-link": {
"version": "4.24.1",
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-hyper-link/-/codemirror-extensions-hyper-link-4.24.1.tgz",
"integrity": "sha512-qf3docpmsHHM0OKLO5m2Fc8t4G+pr1+k9QwrhlM2iolku/INbz+B1JzbRcSU0ow1EcxKtHRtCFE4Lnu6DwP7CQ==",
"license": "MIT",
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@codemirror/state": ">=6.0.0",
"@codemirror/view": ">=6.0.0"
}
},
"node_modules/@uiw/codemirror-extensions-langs": {
"version": "4.24.1",
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-langs/-/codemirror-extensions-langs-4.24.1.tgz",
"integrity": "sha512-8Q33k/UhNni2u5VvAHD+2mxe4hNIqZTNySSUcnJ7urV2lXXau+0fimsQlI+GQLF7gy5F1BUzIi+yvOMrEPK9Ig==",
"license": "MIT",
"dependencies": {
"@codemirror/lang-angular": "^0.1.0",
"@codemirror/lang-cpp": "^6.0.0",
"@codemirror/lang-css": "^6.2.0",
"@codemirror/lang-html": "^6.4.0",
"@codemirror/lang-java": "^6.0.0",
"@codemirror/lang-javascript": "^6.1.0",
"@codemirror/lang-json": "^6.0.0",
"@codemirror/lang-less": "^6.0.1",
"@codemirror/lang-lezer": "^6.0.0",
"@codemirror/lang-liquid": "^6.0.1",
"@codemirror/lang-markdown": "^6.1.0",
"@codemirror/lang-php": "^6.0.0",
"@codemirror/lang-python": "^6.1.0",
"@codemirror/lang-rust": "^6.0.0",
"@codemirror/lang-sass": "^6.0.1",
"@codemirror/lang-sql": "^6.4.0",
"@codemirror/lang-vue": "^0.1.1",
"@codemirror/lang-wast": "^6.0.0",
"@codemirror/lang-xml": "^6.0.0",
"@codemirror/language-data": ">=6.0.0",
"@codemirror/legacy-modes": ">=6.0.0",
"@nextjournal/lang-clojure": "^1.0.0",
"@replit/codemirror-lang-csharp": "^6.1.0",
"@replit/codemirror-lang-nix": "^6.0.1",
"@replit/codemirror-lang-solidity": "^6.0.1",
"@replit/codemirror-lang-svelte": "^6.0.0",
"codemirror-lang-mermaid": "^0.5.0"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@codemirror/language-data": ">=6.0.0",
"@codemirror/legacy-modes": ">=6.0.0"
}
},
"node_modules/@uiw/codemirror-themes": {
"version": "4.24.1",
"resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.24.1.tgz",
"integrity": "sha512-hduBbFNiWNW6nYa2/giKQ9YpzhWNw87BGpCjC+cXYMZ7bCD6q5DC6Hw+7z7ZwSzEaOQvV91lmirOjJ8hn9+pkg==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@codemirror/language": ">=6.0.0",
"@codemirror/state": ">=6.0.0",
"@codemirror/view": ">=6.0.0"
}
},
"node_modules/@uiw/react-codemirror": {
"version": "4.24.1",
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.24.1.tgz",
"integrity": "sha512-BivF4NLqbuBQK5gPVhSkOARi9nPXw8X5r25EnInPeY+I9l1dfEX8O9V6+0xHTlGHyUo0cNfGEF9t1KHEicUfJw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.6",
"@codemirror/commands": "^6.1.0",
"@codemirror/state": "^6.1.1",
"@codemirror/theme-one-dark": "^6.0.0",
"@uiw/codemirror-extensions-basic-setup": "4.24.1",
"codemirror": "^6.0.0"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@babel/runtime": ">=7.11.0",
"@codemirror/state": ">=6.0.0",
"@codemirror/theme-one-dark": ">=6.0.0",
"@codemirror/view": ">=6.0.0",
"codemirror": ">=6.0.0",
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@vitejs/plugin-react-swc": {
"version": "3.10.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.2.tgz",
@@ -3726,6 +4549,32 @@
"node": ">=6"
}
},
"node_modules/codemirror": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/codemirror-lang-mermaid": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/codemirror-lang-mermaid/-/codemirror-lang-mermaid-0.5.0.tgz",
"integrity": "sha512-Taw/2gPCyNArQJCxIP/HSUif+3zrvD+6Ugt7KJZ2dUKou/8r3ZhcfG8krNTZfV2iu8AuGnymKuo7bLPFyqsh/A==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.9.0",
"@lezer/highlight": "^1.1.6",
"@lezer/lr": "^1.3.10"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3793,6 +4642,25 @@
"node": ">= 0.6"
}
},
"node_modules/cookie-parser": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cookie-signature": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
@@ -3836,6 +4704,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3949,6 +4823,18 @@
"node": ">=0.3.1"
}
},
"node_modules/dotenv": {
"version": "17.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz",
"integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/drizzle-orm": {
"version": "0.44.3",
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.3.tgz",
@@ -6636,6 +7522,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"license": "MIT"
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -7175,6 +8067,12 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -30,6 +30,10 @@
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.11",
"@types/bcryptjs": "^2.4.6",
"@uiw/codemirror-extensions-hyper-link": "^4.24.1",
"@uiw/codemirror-extensions-langs": "^4.24.1",
"@uiw/codemirror-themes": "^4.24.1",
"@uiw/react-codemirror": "^4.24.1",
"@xterm/addon-attach": "^0.11.0",
"@xterm/addon-clipboard": "^0.1.0",
"@xterm/addon-fit": "^0.10.0",
@@ -41,7 +45,9 @@
"chalk": "^4.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^17.2.0",
"drizzle-orm": "^0.44.3",
"express": "^5.1.0",
"jsonwebtoken": "^9.0.2",

View File

@@ -0,0 +1,128 @@
import React, { useState, useEffect } from "react";
import CodeMirror from "@uiw/react-codemirror";
import type { LanguageName } from '@uiw/codemirror-extensions-langs';
import { loadLanguage } from '@uiw/codemirror-extensions-langs';
import { hyperLink } from '@uiw/codemirror-extensions-hyper-link';
import { EditorView } from '@codemirror/view';
import { createTheme } from '@uiw/codemirror-themes';
import { tags as t } from '@lezer/highlight';
interface ConfigCodeEditorProps {
content: string;
fileName: string;
onContentChange: (value: string) => void;
}
export function ConfigCodeEditor({content, fileName, onContentChange,}: ConfigCodeEditorProps) {
const langName = getLanguageName(fileName);
const langExt = langName ? loadLanguage(langName) : null;
const extensions = [hyperLink];
if (langExt) extensions.unshift(langExt);
// Custom theme based on built-in 'dark', with overrides for background, gutter, and font size
const customDarkTheme = [
createTheme({
theme: 'dark',
settings: {
background: '#09090b',
gutterBackground: '#18181b',
gutterForeground: 'oklch(0.985 0 0)',
foreground: '#e0e0e0',
caret: '#ffcc00',
selection: '#22223b99',
selectionMatch: '#22223b66',
lineHighlight: '#18181b',
gutterBorder: '1px solid #22223b',
},
styles: [
{ tag: t.keyword, color: '#ff5370' }, // red
{ tag: t.string, color: '#c3e88d' }, // green
{ tag: t.number, color: '#82aaff' }, // blue
{ tag: t.comment, color: '#5c6370' }, // gray
{ tag: t.variableName, color: '#f78c6c' }, // orange
{ tag: t.function(t.variableName), color: '#82aaff' }, // blue
{ tag: t.typeName, color: '#ffcb6b' }, // yellow
{ tag: t.className, color: '#ffcb6b' }, // yellow
{ tag: t.definition(t.typeName), color: '#ffcb6b' }, // yellow
{ tag: t.operator, color: '#89ddff' }, // cyan
{ tag: t.bool, color: '#f78c6c' }, // orange
{ tag: t.null, color: '#f78c6c' }, // orange
{ tag: t.tagName, color: '#ff5370' }, // red
{ tag: t.attributeName, color: '#c792ea' }, // purple
{ tag: t.angleBracket, color: '#89ddff' }, // cyan
],
}),
EditorView.theme({
'&': {
fontSize: '13px',
},
}),
];
function getLanguageName(filename: string): LanguageName | undefined {
const ext = filename.slice(filename.lastIndexOf('.') + 1).toLowerCase();
switch (ext) {
case 'js':
case 'mjs':
case 'cjs': return 'javascript';
case 'ts': return 'typescript';
case 'tsx': return 'tsx';
case 'json': return 'json';
case 'css': return 'css';
case 'html':
case 'htm': return 'html';
case 'md': return 'markdown';
case 'py': return 'python';
case 'sh': return 'shell';
case 'yaml':
case 'yml': return 'yaml';
case 'go': return 'go';
case 'java': return 'java';
case 'c': return 'c';
case 'cpp':
case 'cc':
case 'cxx': return 'cpp';
case 'rs': return 'rust';
case 'php': return 'php';
case 'rb': return 'ruby';
case 'swift': return 'swift';
case 'lua': return 'lua';
case 'xml': return 'xml';
case 'sql': return 'sql';
default: return undefined;
}
}
useEffect(() => {
document.body.style.overflowX = 'hidden';
return () => {
document.body.style.overflowX = '';
};
}, []);
return (
<div style={{ width: '100%', height: '100%', position: 'relative', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<div
style={{
width: '100%',
height: '100%',
overflow: 'auto',
flex: 1,
display: 'flex',
flexDirection: 'column',
}}
className="config-codemirror-scroll-wrapper"
>
<CodeMirror
value={content}
extensions={extensions.concat(customDarkTheme)}
onChange={(value: any) => onContentChange(value)}
theme={customDarkTheme}
height="100%"
basicSetup={{ lineNumbers: true }}
style={{ minHeight: '100%', minWidth: '100%', flex: 1 }}
/>
</div>
</div>
);
}

View File

@@ -1,18 +1,33 @@
import React, { useState } from "react";
import {ConfigEditorSidebar} from "@/apps/Config Editor/ConfigEditorSidebar.tsx";
import React from "react";
import {ConfigCodeEditor} from "@/apps/Config Editor/ConfigCodeEditor.tsx";
import {ConfigTopbar} from "@/apps/Config Editor/ConfigTopbar.tsx";
interface ConfigEditorProps {
onSelectView: (view: string) => void;
}
export function ConfigEditor({ onSelectView }: ConfigEditorProps): React.ReactElement {
export function ConfigEditor({onSelectView}: ConfigEditorProps): React.ReactElement {
const [content, setContent] = useState<string>("");
const [fileName, setFileName] = useState<string>("config.yaml");
return (
<div>
<ConfigEditorSidebar
onSelectView={onSelectView}
/>
Config Editor
<div style={{ position: 'relative', width: '100vw', height: '100vh', overflow: 'hidden' }}>
{/* Sidebar - fixed width, full height */}
<div style={{ position: 'absolute', top: 0, left: 0, width: 256, height: '100vh', zIndex: 20 }}>
<ConfigEditorSidebar onSelectView={onSelectView} />
</div>
{/* Topbar - fixed height, full width minus sidebar */}
<div style={{ position: 'absolute', top: 0, left: 256, right: 0, height: 46, zIndex: 30 }}>
<ConfigTopbar />
</div>
{/* Editor area - fills remaining space, with padding for sidebar and topbar */}
<div style={{ position: 'absolute', top: 46, left: 256, right: 0, bottom: 0, overflow: 'hidden', zIndex: 10 }}>
<ConfigCodeEditor
content={content}
fileName={fileName}
onContentChange={setContent}
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,16 @@
import React from "react";
export function ConfigTopbar(): React.ReactElement {
return (
<div className="flex h-11.5 z-100" style={{
position: 'relative',
width: '100%',
height: 46,
backgroundColor: '#18181b',
borderBottom: '1px solid #222224',
zIndex: 100,
}}>
test
</div>
)
}

View File

@@ -211,6 +211,14 @@ export function HomepageAuth({ className, setLoggedIn, setIsAdmin, setUsername,
>
Discord
</Button>
<div className="w-px h-4 bg-border"></div>
<Button
variant="link"
className="text-sm"
onClick={() => window.open('https://www.paypal.com/paypalme/LukeGustafson803', '_blank')}
>
Fund
</Button>
</div>
</div>
</div>

View File

@@ -1,18 +1,225 @@
import React from "react";
import {SSHTunnelSidebar} from "@/apps/SSH Tunnel/SSHTunnelSidebar.tsx";
import React, { useState, useEffect, useCallback } from "react";
import { SSHTunnelSidebar } from "@/apps/SSH Tunnel/SSHTunnelSidebar.tsx";
import { SSHTunnelViewer } from "@/apps/SSH Tunnel/SSHTunnelViewer.tsx";
import axios from "axios";
interface ConfigEditorProps {
onSelectView: (view: string) => void;
}
export function SSHTunnel({ onSelectView }: ConfigEditorProps): React.ReactElement {
return (
<div>
<SSHTunnelSidebar
onSelectView={onSelectView}
/>
interface SSHTunnel {
id: number;
name: string;
folder: string;
sourcePort: number;
endpointPort: number;
sourceIP: string;
sourceSSHPort: number;
sourceUsername: string;
sourcePassword: string;
sourceAuthMethod: string;
sourceSSHKey: string;
sourceKeyPassword: string;
sourceKeyType: string;
endpointIP: string;
endpointSSHPort: number;
endpointUsername: string;
endpointPassword: string;
endpointAuthMethod: string;
endpointSSHKey: string;
endpointKeyPassword: string;
endpointKeyType: string;
maxRetries: number;
retryInterval: number;
connectionState: string;
autoStart: boolean;
isPinned: boolean;
}
SSH Tunnel
export function SSHTunnel({ onSelectView }: ConfigEditorProps): React.ReactElement {
const [tunnels, setTunnels] = useState<SSHTunnel[]>([]);
const [tunnelsLoading, setTunnelsLoading] = useState(false);
const [tunnelsError, setTunnelsError] = useState<string | null>(null);
const [tunnelStatusMap, setTunnelStatusMap] = useState<Record<string, any>>({});
const sidebarRef = React.useRef<any>(null);
const fetchTunnels = useCallback(async () => {
setTunnelsLoading(true);
setTunnelsError(null);
try {
const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1];
const res = await axios.get(
(window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + '/ssh_tunnel/tunnel',
{ headers: { Authorization: `Bearer ${jwt}` } }
);
const tunnelData = res.data || [];
setTunnels(tunnelData.map((tunnel: any) => ({
id: tunnel.id,
name: tunnel.name,
folder: tunnel.folder || '',
sourcePort: tunnel.sourcePort,
endpointPort: tunnel.endpointPort,
sourceIP: tunnel.sourceIP,
sourceSSHPort: tunnel.sourceSSHPort,
sourceUsername: tunnel.sourceUsername || '',
sourcePassword: tunnel.sourcePassword || '',
sourceAuthMethod: tunnel.sourceAuthMethod || 'password',
sourceSSHKey: tunnel.sourceSSHKey || '',
sourceKeyPassword: tunnel.sourceKeyPassword || '',
sourceKeyType: tunnel.sourceKeyType || '',
endpointIP: tunnel.endpointIP,
endpointSSHPort: tunnel.endpointSSHPort,
endpointUsername: tunnel.endpointUsername || '',
endpointPassword: tunnel.endpointPassword || '',
endpointAuthMethod: tunnel.endpointAuthMethod || 'password',
endpointSSHKey: tunnel.endpointSSHKey || '',
endpointKeyPassword: tunnel.endpointKeyPassword || '',
endpointKeyType: tunnel.endpointKeyType || '',
maxRetries: tunnel.maxRetries || 3,
retryInterval: tunnel.retryInterval || 5000,
connectionState: tunnel.connectionState || 'DISCONNECTED',
autoStart: tunnel.autoStart || false,
isPinned: tunnel.isPinned || false
})));
} catch (err: any) {
setTunnelsError('Failed to load tunnels');
} finally {
setTunnelsLoading(false);
}
}, []);
// Poll backend for tunnel statuses
const fetchTunnelStatuses = useCallback(async () => {
try {
const res = await axios.get('http://localhost:8083/status');
setTunnelStatusMap(res.data || {});
} catch (err) {
// Optionally handle error
}
}, []);
useEffect(() => {
fetchTunnels();
const interval = setInterval(fetchTunnels, 10000);
return () => clearInterval(interval);
}, [fetchTunnels]);
useEffect(() => {
fetchTunnelStatuses();
const interval = setInterval(fetchTunnelStatuses, 500);
return () => clearInterval(interval);
}, [fetchTunnelStatuses]);
// Merge backend status into tunnels
const tunnelsWithStatus = tunnels.map(tunnel => {
const status = tunnelStatusMap[tunnel.name] || {};
return {
...tunnel,
connectionState: status.status ? status.status.toUpperCase() : tunnel.connectionState,
statusReason: status.reason || '',
statusErrorType: status.errorType || '',
statusManualDisconnect: status.manualDisconnect || false,
statusRetryCount: status.retryCount,
statusMaxRetries: status.maxRetries,
statusNextRetryIn: status.nextRetryIn,
statusRetryExhausted: status.retryExhausted,
};
});
const handleConnect = async (tunnelId: string) => {
// Immediately set to CONNECTING for instant UI feedback
setTunnels(prev => prev.map(t =>
t.id.toString() === tunnelId
? { ...t, connectionState: "CONNECTING" }
: t
));
const tunnel = tunnels.find(t => t.id.toString() === tunnelId);
if (!tunnel) return;
try {
await axios.post('http://localhost:8083/connect', {
...tunnel,
name: tunnel.name
});
// No need to update state here; polling will update real status
} catch (err) {
// Optionally handle error
}
};
const handleDisconnect = async (tunnelId: string) => {
// Immediately set to DISCONNECTING for instant UI feedback
setTunnels(prev => prev.map(t =>
t.id.toString() === tunnelId
? { ...t, connectionState: "DISCONNECTING" }
: t
));
const tunnel = tunnels.find(t => t.id.toString() === tunnelId);
if (!tunnel) return;
try {
await axios.post('http://localhost:8083/disconnect', {
tunnelName: tunnel.name
});
// No need to update state here; polling will update real status
} catch (err) {
// Optionally handle error
}
};
const handleDeleteTunnel = async (tunnelId: string) => {
try {
const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1];
await axios.delete(
(window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + `/ssh_tunnel/tunnel/${tunnelId}`,
{ headers: { Authorization: `Bearer ${jwt}` } }
);
fetchTunnels();
} catch (err: any) {
console.error('Failed to delete tunnel:', err);
}
};
const handleEditTunnel = async (tunnelId: string, data: any) => {
try {
const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1];
await axios.put(
(window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + `/ssh_tunnel/tunnel/${tunnelId}`,
data,
{ headers: { Authorization: `Bearer ${jwt}` } }
);
fetchTunnels();
} catch (err: any) {
console.error('Failed to edit tunnel:', err);
}
};
const handleEditTunnelClick = (tunnelId: string) => {
// Find the tunnel data and pass it to the sidebar
const tunnel = tunnels.find(t => t.id.toString() === tunnelId);
if (tunnel && sidebarRef.current) {
// Call the sidebar's openEditSheet function
sidebarRef.current.openEditSheet(tunnel);
}
};
return (
<div className="flex h-screen w-full">
<div className="w-64 flex-shrink-0">
<SSHTunnelSidebar
ref={sidebarRef}
onSelectView={onSelectView}
onTunnelAdded={fetchTunnels}
onEditTunnel={handleEditTunnelClick}
/>
</div>
<div className="flex-1 overflow-auto">
<SSHTunnelViewer
tunnels={tunnelsWithStatus}
onConnect={handleConnect}
onDisconnect={handleDisconnect}
onDeleteTunnel={handleDeleteTunnel}
onEditTunnel={handleEditTunnelClick}
/>
</div>
</div>
)
);
}

View File

@@ -0,0 +1,186 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Loader2, Edit, Trash2 } from "lucide-react";
const CONNECTION_STATES = {
DISCONNECTED: "disconnected",
CONNECTING: "connecting",
CONNECTED: "connected",
VERIFYING: "verifying",
FAILED: "failed",
UNSTABLE: "unstable",
RETRYING: "retrying",
DISCONNECTING: "disconnecting"
};
interface SSHTunnelObjectProps {
hostConfig: any;
onConnect?: () => void;
onDisconnect?: () => void;
onDelete?: () => void;
onEdit?: () => void;
connectionState?: keyof typeof CONNECTION_STATES;
isPinned?: boolean;
}
export function SSHTunnelObject({
hostConfig = {},
onConnect,
onDisconnect,
onDelete,
onEdit,
connectionState = "DISCONNECTED",
isPinned = false
}: SSHTunnelObjectProps): React.ReactElement {
const getStatusColor = (state: keyof typeof CONNECTION_STATES) => {
switch (state) {
case "CONNECTED":
return "bg-green-500";
case "CONNECTING":
case "VERIFYING":
case "RETRYING":
return "bg-yellow-500";
case "FAILED":
return "bg-red-500";
case "UNSTABLE":
return "bg-orange-500";
default:
return "bg-gray-500";
}
};
const getStatusText = (state: keyof typeof CONNECTION_STATES) => {
switch (state) {
case "CONNECTED":
return "Connected";
case "CONNECTING":
return "Connecting";
case "VERIFYING":
return "Verifying";
case "FAILED":
return "Failed";
case "UNSTABLE":
return "Unstable";
case "RETRYING":
return "Retrying";
default:
return "Disconnected";
}
};
const isConnected = connectionState === "CONNECTED";
const isConnecting = ["CONNECTING", "VERIFYING", "RETRYING"].includes(connectionState);
const isDisconnecting = connectionState === "DISCONNECTING";
return (
<Card className="w-full bg-card border-border shadow-sm hover:shadow-md transition-shadow relative group p-0">
{/* Hover overlay buttons */}
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-10 flex gap-1">
<Button
size="sm"
variant="secondary"
className="h-8 w-8 p-0 bg-black/50 hover:bg-black/70 border-0"
onClick={onEdit}
>
<Edit className="w-4 h-4" />
</Button>
<Button
size="sm"
variant="destructive"
className="h-8 w-8 p-0 bg-red-500/50 hover:bg-red-500/70 border-0"
onClick={onDelete}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
<div className="p-2">
<div className="flex items-center justify-between gap-1 mb-1">
<div className="text-lg font-semibold text-card-foreground flex-1 min-w-0">
<span className="break-words">
{isPinned && <span className="text-yellow-400 mr-1 flex-shrink-0"></span>}
{hostConfig.name || "My SSH Tunnel"}
</span>
</div>
<div className="w-px h-4 bg-border mx-1"></div>
<div className="flex items-center gap-2 flex-shrink-0">
<div className={`w-2 h-2 rounded-full ${getStatusColor(connectionState)}`} />
<span className="text-sm text-muted-foreground whitespace-nowrap">
{getStatusText(connectionState)}
</span>
</div>
</div>
<Separator className="mb-1" />
<div className="space-y-1 mb-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground flex-shrink-0 mr-2">Source:</span>
<span className="text-card-foreground font-mono text-right break-all">
{hostConfig.source || "localhost:22"}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground flex-shrink-0 mr-2">Endpoint:</span>
<span className="text-card-foreground font-mono text-right break-all">
{hostConfig.endpoint || "test:224"}
</span>
</div>
</div>
<Separator className="my-1" />
{/* Error/Status Reason */}
{((connectionState === "FAILED" || connectionState === "UNSTABLE") && hostConfig.statusReason) && (
<div className="mb-2 text-xs text-red-500 bg-red-500/10 rounded px-2 py-1 border border-red-500/20">
{hostConfig.statusReason}
{typeof hostConfig.statusReason === 'string' && hostConfig.statusReason.includes('Max retries exhausted') && (
<>
<br />
<span>
Check your Docker logs for the error reason, join the <a href="https://discord.com/invite/jVQGdvHDrf" target="_blank" rel="noopener noreferrer" className="underline text-blue-400">Discord</a> or create a <a href="https://github.com/LukeGus/Termix/issues/new" target="_blank" rel="noopener noreferrer" className="underline text-blue-400">GitHub issue</a> for help.
</span>
</>
)}
</div>
)}
<div className="flex gap-2 mt-2">
<Button
onClick={onConnect}
disabled={isConnected || isConnecting || isDisconnecting}
className="flex-1"
variant={isConnected ? "secondary" : "default"}
>
{isConnecting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Connecting...
</>
) : isConnected ? (
"Connected"
) : (
"Connect"
)}
</Button>
<Button
onClick={onDisconnect}
disabled={!isConnected || isDisconnecting || isConnecting}
variant="outline"
className="flex-1"
>
{isDisconnecting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Disconnecting...
</>
) : (
"Disconnect"
)}
</Button>
</div>
</div>
</Card>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,137 @@
import React from "react";
import { SSHTunnelObject } from "./SSHTunnelObject";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { Separator } from "@/components/ui/separator";
interface SSHTunnelViewerProps {
tunnels: Array<{
id: number;
name: string;
folder: string;
sourcePort: number;
endpointPort: number;
sourceIP: string;
sourceSSHPort: number;
sourceUsername: string;
sourcePassword: string;
sourceAuthMethod: string;
sourceSSHKey: string;
sourceKeyPassword: string;
sourceKeyType: string;
endpointIP: string;
endpointSSHPort: number;
endpointUsername: string;
endpointPassword: string;
endpointAuthMethod: string;
endpointSSHKey: string;
endpointKeyPassword: string;
endpointKeyType: string;
maxRetries: number;
retryInterval: number;
connectionState?: string;
autoStart: boolean;
isPinned: boolean;
}>;
onConnect?: (tunnelId: string) => void;
onDisconnect?: (tunnelId: string) => void;
onDeleteTunnel?: (tunnelId: string) => void;
onEditTunnel?: (tunnelId: string) => void;
}
export function SSHTunnelViewer({
tunnels = [],
onConnect,
onDisconnect,
onDeleteTunnel,
onEditTunnel
}: SSHTunnelViewerProps): React.ReactElement {
const handleConnect = (tunnelId: string) => {
onConnect?.(tunnelId);
};
const handleDisconnect = (tunnelId: string) => {
onDisconnect?.(tunnelId);
};
// Group tunnels by folder and sort
const tunnelsByFolder = React.useMemo(() => {
const map: Record<string, typeof tunnels> = {};
tunnels.forEach(tunnel => {
const folder = tunnel.folder && tunnel.folder.trim() ? tunnel.folder : 'No Folder';
if (!map[folder]) map[folder] = [];
map[folder].push(tunnel);
});
return map;
}, [tunnels]);
const sortedFolders = React.useMemo(() => {
const folders = Object.keys(tunnelsByFolder);
folders.sort((a, b) => {
if (a === 'No Folder') return -1;
if (b === 'No Folder') return 1;
return a.localeCompare(b);
});
return folders;
}, [tunnelsByFolder]);
const getSortedTunnels = (arr: typeof tunnels) => {
const pinned = arr.filter(t => t.isPinned).sort((a, b) => (a.name || '').localeCompare(b.name || ''));
const rest = arr.filter(t => !t.isPinned).sort((a, b) => (a.name || '').localeCompare(b.name || ''));
return [...pinned, ...rest];
};
return (
<div className="w-full p-6" style={{ width: 'calc(100vw - 256px)', maxWidth: 'none' }}>
<div className="w-full min-w-0" style={{ width: '100%', maxWidth: 'none' }}>
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-foreground mb-2">
SSH Tunnels
</h1>
<p className="text-muted-foreground">
Manage your SSH tunnel connections
</p>
</div>
{/* Accordion Layout */}
{tunnels.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<h3 className="text-lg font-semibold text-foreground mb-2">
No SSH Tunnels
</h3>
<p className="text-muted-foreground max-w-md">
Create your first SSH tunnel to get started. Use the sidebar to add a new tunnel configuration.
</p>
</div>
) : (
<Accordion type="multiple" className="w-full" defaultValue={sortedFolders}>
{sortedFolders.map((folder, idx) => (
<AccordionItem value={folder} key={`folder-${folder}`} className={idx === 0 ? "mt-0" : "mt-2"}>
<AccordionTrigger className="text-base font-semibold rounded-t-none px-3 py-2" style={{marginTop: idx === 0 ? 0 : undefined}}>
{folder}
</AccordionTrigger>
<AccordionContent className="flex flex-col gap-1 px-3 pb-2 pt-1">
<div className="grid grid-cols-4 gap-6 w-full">
{getSortedTunnels(tunnelsByFolder[folder]).map((tunnel, tunnelIndex) => (
<div key={tunnel.id} className="w-full">
<SSHTunnelObject
hostConfig={tunnel}
connectionState={tunnel.connectionState as any}
isPinned={tunnel.isPinned}
onConnect={() => handleConnect(tunnel.id.toString())}
onDisconnect={() => handleDisconnect(tunnel.id.toString())}
onDelete={() => onDeleteTunnel?.(tunnel.id.toString())}
onEdit={() => onEditTunnel?.(tunnel.id.toString())}
/>
</div>
))}
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
)}
</div>
</div>
);
}

View File

@@ -2,6 +2,7 @@ import express from 'express';
import bodyParser from 'body-parser';
import userRoutes from './routes/users.js';
import sshRoutes from './routes/ssh.js';
import sshTunnelRoutes from './routes/ssh_tunnel.js';
import chalk from 'chalk';
import cors from 'cors';
@@ -47,6 +48,7 @@ app.get('/health', (req, res) => {
app.use('/users', userRoutes);
app.use('/ssh', sshRoutes);
app.use('/ssh_tunnel', sshTunnelRoutes);
app.use((err: unknown, req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.error('Unhandled error:', err);

View File

@@ -64,6 +64,36 @@ CREATE TABLE IF NOT EXISTS ssh_data (
is_pinned INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS ssh_tunnel_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT,
folder TEXT,
source_port INTEGER NOT NULL,
endpoint_port INTEGER NOT NULL,
source_ip TEXT NOT NULL,
source_ssh_port INTEGER NOT NULL,
source_username TEXT,
source_password TEXT,
source_auth_method TEXT,
source_ssh_key TEXT,
source_key_password TEXT,
source_key_type TEXT,
endpoint_ip TEXT NOT NULL,
endpoint_ssh_port INTEGER NOT NULL,
endpoint_username TEXT,
endpoint_password TEXT,
endpoint_auth_method TEXT,
endpoint_ssh_key TEXT,
endpoint_key_password TEXT,
endpoint_key_type TEXT,
max_retries INTEGER NOT NULL DEFAULT 3,
retry_interval INTEGER NOT NULL DEFAULT 5000,
connection_state TEXT NOT NULL DEFAULT 'DISCONNECTED',
auto_start INTEGER NOT NULL DEFAULT 0,
is_pinned INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL

View File

@@ -25,6 +25,36 @@ export const sshData = sqliteTable('ssh_data', {
isPinned: integer('is_pinned', { mode: 'boolean' }),
});
export const sshTunnelData = sqliteTable('ssh_tunnel_data', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: text('user_id').notNull().references(() => users.id),
name: text('name'),
folder: text('folder'),
sourcePort: integer('source_port').notNull(),
endpointPort: integer('endpoint_port').notNull(),
sourceIP: text('source_ip').notNull(),
sourceSSHPort: integer('source_ssh_port').notNull(),
sourceUsername: text('source_username'),
sourcePassword: text('source_password'),
sourceAuthMethod: text('source_auth_method'),
sourceSSHKey: text('source_ssh_key', { length: 8192 }),
sourceKeyPassword: text('source_key_password'),
sourceKeyType: text('source_key_type'),
endpointIP: text('endpoint_ip').notNull(),
endpointSSHPort: integer('endpoint_ssh_port').notNull(),
endpointUsername: text('endpoint_username'),
endpointPassword: text('endpoint_password'),
endpointAuthMethod: text('endpoint_auth_method'),
endpointSSHKey: text('endpoint_ssh_key', { length: 8192 }),
endpointKeyPassword: text('endpoint_key_password'),
endpointKeyType: text('endpoint_key_type'),
maxRetries: integer('max_retries').notNull().default(3),
retryInterval: integer('retry_interval').notNull().default(5000),
connectionState: text('connection_state').notNull().default('DISCONNECTED'),
autoStart: integer('auto_start', { mode: 'boolean' }).notNull().default(false),
isPinned: integer('is_pinned', { mode: 'boolean' }).notNull().default(false),
});
export const settings = sqliteTable('settings', {
key: text('key').primaryKey(),
value: text('value').notNull(),

View File

@@ -0,0 +1,306 @@
import express from 'express';
import { db } from '../db/index.js';
import { sshTunnelData } from '../db/schema.js';
import { eq, and } from 'drizzle-orm';
import chalk from 'chalk';
import jwt from 'jsonwebtoken';
import type { Request, Response, NextFunction } from 'express';
const dbIconSymbol = '🗄️';
const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`);
const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => {
return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${dbIconSymbol}]`)} ${message}`;
};
const logger = {
info: (msg: string): void => {
console.log(formatMessage('info', chalk.cyan, msg));
},
warn: (msg: string): void => {
console.warn(formatMessage('warn', chalk.yellow, msg));
},
error: (msg: string, err?: unknown): void => {
console.error(formatMessage('error', chalk.redBright, msg));
if (err) console.error(err);
},
success: (msg: string): void => {
console.log(formatMessage('success', chalk.greenBright, msg));
},
debug: (msg: string): void => {
if (process.env.NODE_ENV !== 'production') {
console.debug(formatMessage('debug', chalk.magenta, msg));
}
}
};
const router = express.Router();
function isNonEmptyString(val: any): val is string {
return typeof val === 'string' && val.trim().length > 0;
}
function isValidPort(val: any): val is number {
return typeof val === 'number' && val > 0 && val < 65536;
}
interface JWTPayload {
userId: string;
iat?: number;
exp?: number;
}
// JWT authentication middleware
function authenticateJWT(req: Request, res: Response, next: NextFunction) {
// Only allow bypass if X-Internal-Request header is set
if (req.headers['x-internal-request'] === '1') {
(req as any).userId = 'internal_service';
return next();
}
const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
logger.warn('Missing or invalid Authorization header');
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
}
const token = authHeader.split(' ')[1];
const jwtSecret = process.env.JWT_SECRET || 'secret';
try {
const payload = jwt.verify(token, jwtSecret) as JWTPayload;
(req as any).userId = payload.userId;
next();
} catch (err) {
logger.warn('Invalid or expired token');
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
// Route: Create SSH tunnel data (requires JWT)
// POST /ssh_tunnel/tunnel
router.post('/tunnel', authenticateJWT, async (req: Request, res: Response) => {
const {
name, folder, sourcePort, endpointPort, sourceIP, sourceSSHPort, sourceUsername,
sourcePassword, sourceAuthMethod, sourceSSHKey, sourceKeyPassword, sourceKeyType,
endpointIP, endpointSSHPort, endpointUsername, endpointPassword, endpointAuthMethod,
endpointSSHKey, endpointKeyPassword, endpointKeyType, maxRetries, retryInterval, autoStart, isPinned
} = req.body;
const userId = (req as any).userId;
if (!isNonEmptyString(userId) || !isNonEmptyString(sourceIP) || !isValidPort(sourcePort) ||
!isValidPort(endpointPort) || !isValidPort(sourceSSHPort) || !isNonEmptyString(endpointIP) ||
!isValidPort(endpointSSHPort)) {
logger.warn('Invalid SSH tunnel data input');
return res.status(400).json({ error: 'Invalid SSH tunnel data' });
}
const sshTunnelDataObj: any = {
userId: userId,
name,
folder,
sourcePort,
endpointPort,
sourceIP,
sourceSSHPort,
sourceUsername,
sourceAuthMethod,
endpointIP,
endpointSSHPort,
endpointUsername,
endpointAuthMethod,
maxRetries: maxRetries || 3,
retryInterval: retryInterval || 5000,
connectionState: 'DISCONNECTED',
autoStart: autoStart || false,
isPinned: isPinned || false
};
// Handle source authentication
if (sourceAuthMethod === 'password') {
sshTunnelDataObj.sourcePassword = sourcePassword;
sshTunnelDataObj.sourceSSHKey = null;
sshTunnelDataObj.sourceKeyPassword = null;
sshTunnelDataObj.sourceKeyType = null;
} else if (sourceAuthMethod === 'key') {
sshTunnelDataObj.sourceSSHKey = sourceSSHKey;
sshTunnelDataObj.sourceKeyPassword = sourceKeyPassword;
sshTunnelDataObj.sourceKeyType = sourceKeyType;
sshTunnelDataObj.sourcePassword = null;
}
// Handle endpoint authentication
if (endpointAuthMethod === 'password') {
sshTunnelDataObj.endpointPassword = endpointPassword;
sshTunnelDataObj.endpointSSHKey = null;
sshTunnelDataObj.endpointKeyPassword = null;
sshTunnelDataObj.endpointKeyType = null;
} else if (endpointAuthMethod === 'key') {
sshTunnelDataObj.endpointSSHKey = endpointSSHKey;
sshTunnelDataObj.endpointKeyPassword = endpointKeyPassword;
sshTunnelDataObj.endpointKeyType = endpointKeyType;
sshTunnelDataObj.endpointPassword = null;
}
try {
await db.insert(sshTunnelData).values(sshTunnelDataObj);
res.json({ message: 'SSH tunnel data created' });
} catch (err) {
logger.error('Failed to save SSH tunnel data', err);
res.status(500).json({ error: 'Failed to save SSH tunnel data' });
}
});
// Route: Update SSH tunnel data (requires JWT)
// PUT /ssh_tunnel/tunnel/:id
router.put('/tunnel/:id', authenticateJWT, async (req: Request, res: Response) => {
const {
name, folder, sourcePort, endpointPort, sourceIP, sourceSSHPort, sourceUsername,
sourcePassword, sourceAuthMethod, sourceSSHKey, sourceKeyPassword, sourceKeyType,
endpointIP, endpointSSHPort, endpointUsername, endpointPassword, endpointAuthMethod,
endpointSSHKey, endpointKeyPassword, endpointKeyType, maxRetries, retryInterval, autoStart, isPinned
} = req.body;
const { id } = req.params;
const userId = (req as any).userId;
if (!isNonEmptyString(userId) || !isNonEmptyString(sourceIP) || !isValidPort(sourcePort) ||
!isValidPort(endpointPort) || !isValidPort(sourceSSHPort) || !isNonEmptyString(endpointIP) ||
!isValidPort(endpointSSHPort) || !id) {
logger.warn('Invalid SSH tunnel data input for update');
return res.status(400).json({ error: 'Invalid SSH tunnel data' });
}
const sshTunnelDataObj: any = {
name,
folder,
sourcePort,
endpointPort,
sourceIP,
sourceSSHPort,
sourceUsername,
sourceAuthMethod,
endpointIP,
endpointSSHPort,
endpointUsername,
endpointAuthMethod,
maxRetries: maxRetries || 3,
retryInterval: retryInterval || 5000,
autoStart: autoStart || false,
isPinned: isPinned || false
};
// Handle source authentication
if (sourceAuthMethod === 'password') {
sshTunnelDataObj.sourcePassword = sourcePassword;
sshTunnelDataObj.sourceSSHKey = null;
sshTunnelDataObj.sourceKeyPassword = null;
sshTunnelDataObj.sourceKeyType = null;
} else if (sourceAuthMethod === 'key') {
sshTunnelDataObj.sourceSSHKey = sourceSSHKey;
sshTunnelDataObj.sourceKeyPassword = sourceKeyPassword;
sshTunnelDataObj.sourceKeyType = sourceKeyType;
sshTunnelDataObj.sourcePassword = null;
}
// Handle endpoint authentication
if (endpointAuthMethod === 'password') {
sshTunnelDataObj.endpointPassword = endpointPassword;
sshTunnelDataObj.endpointSSHKey = null;
sshTunnelDataObj.endpointKeyPassword = null;
sshTunnelDataObj.endpointKeyType = null;
} else if (endpointAuthMethod === 'key') {
sshTunnelDataObj.endpointSSHKey = endpointSSHKey;
sshTunnelDataObj.endpointKeyPassword = endpointKeyPassword;
sshTunnelDataObj.endpointKeyType = endpointKeyType;
sshTunnelDataObj.endpointPassword = null;
}
try {
const result = await db.update(sshTunnelData)
.set(sshTunnelDataObj)
.where(and(eq(sshTunnelData.id, Number(id)), eq(sshTunnelData.userId, userId)));
res.json({ message: 'SSH tunnel data updated' });
} catch (err) {
logger.error('Failed to update SSH tunnel data', err);
res.status(500).json({ error: 'Failed to update SSH tunnel data' });
}
});
// Route: Get SSH tunnel data for the authenticated user (requires JWT)
// GET /ssh_tunnel/tunnel
router.get('/tunnel', authenticateJWT, async (req: Request, res: Response) => {
// If internal request and allAutoStart=1, return all autoStart tunnels
if (req.headers['x-internal-request'] === '1' && req.query.allAutoStart === '1') {
try {
const data = await db
.select()
.from(sshTunnelData)
.where(eq(sshTunnelData.autoStart, true));
return res.json(data);
} catch (err) {
logger.error('Failed to fetch all auto-start SSH tunnel data', err);
return res.status(500).json({ error: 'Failed to fetch auto-start SSH tunnel data' });
}
}
// Default: filter by userId
const userId = (req as any).userId;
if (!isNonEmptyString(userId)) {
logger.warn('Invalid userId for SSH tunnel data fetch');
return res.status(400).json({ error: 'Invalid userId' });
}
try {
const data = await db
.select()
.from(sshTunnelData)
.where(eq(sshTunnelData.userId, userId));
res.json(data);
} catch (err) {
logger.error('Failed to fetch SSH tunnel data', err);
res.status(500).json({ error: 'Failed to fetch SSH tunnel data' });
}
});
// Route: Get all unique folders for the authenticated user (requires JWT)
// GET /ssh_tunnel/folders
router.get('/folders', authenticateJWT, async (req: Request, res: Response) => {
const userId = (req as any).userId;
if (!isNonEmptyString(userId)) {
logger.warn('Invalid userId for SSH tunnel folder fetch');
return res.status(400).json({ error: 'Invalid userId' });
}
try {
const data = await db
.select({ folder: sshTunnelData.folder })
.from(sshTunnelData)
.where(eq(sshTunnelData.userId, userId));
const folderCounts: Record<string, number> = {};
data.forEach(d => {
if (d.folder && d.folder.trim() !== '') {
folderCounts[d.folder] = (folderCounts[d.folder] || 0) + 1;
}
});
const folders = Object.keys(folderCounts).filter(folder => folderCounts[folder] > 0);
res.json(folders);
} catch (err) {
logger.error('Failed to fetch SSH tunnel folders', err);
res.status(500).json({ error: 'Failed to fetch SSH tunnel folders' });
}
});
// Route: Delete SSH tunnel by id (requires JWT)
// DELETE /ssh_tunnel/tunnel/:id
router.delete('/tunnel/:id', authenticateJWT, async (req: Request, res: Response) => {
const userId = (req as any).userId;
const { id } = req.params;
if (!isNonEmptyString(userId) || !id) {
logger.warn('Invalid userId or id for SSH tunnel delete');
return res.status(400).json({ error: 'Invalid userId or id' });
}
try {
const result = await db.delete(sshTunnelData)
.where(and(eq(sshTunnelData.id, Number(id)), eq(sshTunnelData.userId, userId)));
res.json({ message: 'SSH tunnel deleted' });
} catch (err) {
logger.error('Failed to delete SSH tunnel', err);
res.status(500).json({ error: 'Failed to delete SSH tunnel' });
}
});
export default router;

View File

@@ -133,7 +133,6 @@ router.post('/get', async (req, res) => {
}
const jwtSecret = process.env.JWT_SECRET || 'secret';
const token = jwt.sign({ userId: userRecord.id }, jwtSecret, { expiresIn: '50d' });
logger.success(`User authenticated: ${username}`);
res.json({ token });
} catch (err) {
logger.error('Failed to get user', err);

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
import './db/database.js'
import './ssh/ssh.js';
import './ssh_tunnel/ssh_tunnel.js';
import chalk from 'chalk';
const fixedIconSymbol = '🚀';

View File

@@ -39,7 +39,7 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
"col-start-2 font-medium tracking-tight whitespace-normal break-words",
className
)}
{...props}