Compare commits
26 Commits
v0.2.3
...
47a3e1cfef
Author | SHA1 | Date | |
---|---|---|---|
47a3e1cfef | |||
1047818fdc | |||
3d093a3a45 | |||
992d2a4d06 | |||
12f0f187a6 | |||
997e8b419f | |||
1d9132de3b | |||
e1c2618dc8 | |||
a7df7adc8e | |||
03d164c9d3 | |||
f522674a1c | |||
51fcccafa2 | |||
e3913ab4c9 | |||
c16f21bba3 | |||
61d9acc7c6 | |||
8d7b01629d | |||
5685948608 | |||
c98a065587 | |||
e46c3d2b4d | |||
fa228acc3a | |||
e7e0f9d33e | |||
a51b20add7 | |||
890f715388 | |||
89bc74e644 | |||
60c24e3ee4 | |||
486001b584 |
19
doc/todo.md
Normal file
19
doc/todo.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
## Definitely
|
||||||
|
|
||||||
|
* Switch to "process" provider for AWS credentials (much less hacky)
|
||||||
|
* Session timeout (plain duration, or activity-based?)
|
||||||
|
* ~Fix rehide behavior when new request comes in while old one is still being resolved~
|
||||||
|
* Additional hotkey configuration (approve/deny at the very least)
|
||||||
|
* Logging
|
||||||
|
* Icon
|
||||||
|
* Auto-updates
|
||||||
|
* SSH key handling
|
||||||
|
|
||||||
|
## Maybe
|
||||||
|
|
||||||
|
* Flatten error type hierarchy
|
||||||
|
* Rehide after terminal launch from locked
|
||||||
|
* Generalize Request across both credentials and terminal launch?
|
||||||
|
* Make hotkey configuration a little more tolerant of slight mistiming
|
||||||
|
* Distinguish between request that was denied and request that was canceled (e.g. due to error)
|
||||||
|
* Use atomic types for primitive state values instead of RwLock'd types
|
262
package-lock.json
generated
262
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "creddy",
|
"name": "creddy",
|
||||||
"version": "0.2.2",
|
"version": "0.3.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "creddy",
|
"name": "creddy",
|
||||||
"version": "0.2.2",
|
"version": "0.3.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^1.0.2",
|
"@tauri-apps/api": "^1.0.2",
|
||||||
"daisyui": "^2.51.5"
|
"daisyui": "^2.51.5"
|
||||||
@ -78,9 +78,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
||||||
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
|
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
@ -99,19 +99,14 @@
|
|||||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.18",
|
"version": "0.3.19",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
|
||||||
"integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==",
|
"integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
"@jridgewell/sourcemap-codec": "1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": {
|
|
||||||
"version": "1.4.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
|
||||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
|
|
||||||
},
|
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@ -390,9 +385,9 @@
|
|||||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
|
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
|
||||||
},
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.14",
|
"version": "10.4.15",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz",
|
||||||
"integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==",
|
"integrity": "sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@ -401,11 +396,15 @@
|
|||||||
{
|
{
|
||||||
"type": "tidelift",
|
"type": "tidelift",
|
||||||
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
|
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.21.5",
|
"browserslist": "^4.21.10",
|
||||||
"caniuse-lite": "^1.0.30001464",
|
"caniuse-lite": "^1.0.30001520",
|
||||||
"fraction.js": "^4.2.0",
|
"fraction.js": "^4.2.0",
|
||||||
"normalize-range": "^0.1.2",
|
"normalize-range": "^0.1.2",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
@ -455,9 +454,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.21.9",
|
"version": "4.21.10",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz",
|
||||||
"integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
|
"integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@ -473,9 +472,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001503",
|
"caniuse-lite": "^1.0.30001517",
|
||||||
"electron-to-chromium": "^1.4.431",
|
"electron-to-chromium": "^1.4.477",
|
||||||
"node-releases": "^2.0.12",
|
"node-releases": "^2.0.13",
|
||||||
"update-browserslist-db": "^1.0.11"
|
"update-browserslist-db": "^1.0.11"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -494,9 +493,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001515",
|
"version": "1.0.30001534",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001515.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001534.tgz",
|
||||||
"integrity": "sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA==",
|
"integrity": "sha512-vlPVrhsCS7XaSh2VvWluIQEzVhefrUQcEsQWSS5A5V+dM07uv1qHeQzAOTGIMy9i3e9bH15+muvI/UHojVgS/Q==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@ -675,9 +674,9 @@
|
|||||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
|
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.455",
|
"version": "1.4.520",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.455.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.520.tgz",
|
||||||
"integrity": "sha512-8tgdX0Odl24LtmLwxotpJCVjIndN559AvaOtd67u+2mo+IDsgsTF580NB+uuDCqsHw8yFg53l5+imFV9Fw3cbA=="
|
"integrity": "sha512-Frfus2VpYADsrh1lB3v/ft/WVFlVzOIm+Q0p7U7VqHI6qr7NWHYKe+Wif3W50n7JAFoBsWVsoU0+qDks6WQ60g=="
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.15.18",
|
"version": "0.15.18",
|
||||||
@ -1045,9 +1044,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
||||||
"integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==",
|
"integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.stat": "^2.0.2",
|
"@nodelib/fs.stat": "^2.0.2",
|
||||||
"@nodelib/fs.walk": "^1.2.3",
|
"@nodelib/fs.walk": "^1.2.3",
|
||||||
@ -1095,15 +1094,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz",
|
||||||
"integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==",
|
"integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "patreon",
|
"type": "patreon",
|
||||||
"url": "https://www.patreon.com/infusion"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fs.realpath": {
|
"node_modules/fs.realpath": {
|
||||||
@ -1112,9 +1111,9 @@
|
|||||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||||
},
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.2",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -1201,9 +1200,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-core-module": {
|
"node_modules/is-core-module": {
|
||||||
"version": "2.12.1",
|
"version": "2.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
|
||||||
"integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
|
"integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has": "^1.0.3"
|
"has": "^1.0.3"
|
||||||
},
|
},
|
||||||
@ -1239,9 +1238,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "1.19.1",
|
"version": "1.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz",
|
||||||
"integrity": "sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg==",
|
"integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
@ -1435,9 +1434,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.25",
|
"version": "8.4.29",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.25.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz",
|
||||||
"integrity": "sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw==",
|
"integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@ -1597,11 +1596,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.2",
|
"version": "1.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz",
|
||||||
"integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
|
"integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-core-module": "^2.11.0",
|
"is-core-module": "^2.13.0",
|
||||||
"path-parse": "^1.0.7",
|
"path-parse": "^1.0.7",
|
||||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||||
},
|
},
|
||||||
@ -1682,9 +1681,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/sucrase": {
|
"node_modules/sucrase": {
|
||||||
"version": "3.32.0",
|
"version": "3.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
|
||||||
"integrity": "sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==",
|
"integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.2",
|
"@jridgewell/gen-mapping": "^0.3.2",
|
||||||
"commander": "^4.0.0",
|
"commander": "^4.0.0",
|
||||||
@ -1723,21 +1722,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-hmr": {
|
"node_modules/svelte-hmr": {
|
||||||
"version": "0.15.2",
|
"version": "0.15.3",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
|
||||||
"integrity": "sha512-q/bAruCvFLwvNbeE1x3n37TYFb3mTBJ6TrCq6p2CoFbSTNhDE9oAtEfpy+wmc9So8AG0Tja+X0/mJzX9tSfvIg==",
|
"integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.20 || ^14.13.1 || >= 16"
|
"node": "^12.20 || ^14.13.1 || >= 16"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"svelte": "^3.19.0 || ^4.0.0-next.0"
|
"svelte": "^3.19.0 || ^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
|
||||||
"integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==",
|
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@ -1759,7 +1758,6 @@
|
|||||||
"postcss-load-config": "^4.0.1",
|
"postcss-load-config": "^4.0.1",
|
||||||
"postcss-nested": "^6.0.1",
|
"postcss-nested": "^6.0.1",
|
||||||
"postcss-selector-parser": "^6.0.11",
|
"postcss-selector-parser": "^6.0.11",
|
||||||
"postcss-value-parser": "^4.2.0",
|
|
||||||
"resolve": "^1.22.2",
|
"resolve": "^1.22.2",
|
||||||
"sucrase": "^3.32.0"
|
"sucrase": "^3.32.0"
|
||||||
},
|
},
|
||||||
@ -1909,9 +1907,9 @@
|
|||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz",
|
||||||
"integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==",
|
"integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
@ -1948,9 +1946,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@jridgewell/resolve-uri": {
|
"@jridgewell/resolve-uri": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
||||||
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
|
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA=="
|
||||||
},
|
},
|
||||||
"@jridgewell/set-array": {
|
"@jridgewell/set-array": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
@ -1963,19 +1961,12 @@
|
|||||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
||||||
},
|
},
|
||||||
"@jridgewell/trace-mapping": {
|
"@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.18",
|
"version": "0.3.19",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
|
||||||
"integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==",
|
"integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@jridgewell/resolve-uri": "3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
"@jridgewell/sourcemap-codec": "1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/sourcemap-codec": {
|
|
||||||
"version": "1.4.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
|
||||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@nodelib/fs.scandir": {
|
"@nodelib/fs.scandir": {
|
||||||
@ -2128,12 +2119,12 @@
|
|||||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
|
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
|
||||||
},
|
},
|
||||||
"autoprefixer": {
|
"autoprefixer": {
|
||||||
"version": "10.4.14",
|
"version": "10.4.15",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz",
|
||||||
"integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==",
|
"integrity": "sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"browserslist": "^4.21.5",
|
"browserslist": "^4.21.10",
|
||||||
"caniuse-lite": "^1.0.30001464",
|
"caniuse-lite": "^1.0.30001520",
|
||||||
"fraction.js": "^4.2.0",
|
"fraction.js": "^4.2.0",
|
||||||
"normalize-range": "^0.1.2",
|
"normalize-range": "^0.1.2",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
@ -2168,13 +2159,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"version": "4.21.9",
|
"version": "4.21.10",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz",
|
||||||
"integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
|
"integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"caniuse-lite": "^1.0.30001503",
|
"caniuse-lite": "^1.0.30001517",
|
||||||
"electron-to-chromium": "^1.4.431",
|
"electron-to-chromium": "^1.4.477",
|
||||||
"node-releases": "^2.0.12",
|
"node-releases": "^2.0.13",
|
||||||
"update-browserslist-db": "^1.0.11"
|
"update-browserslist-db": "^1.0.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2184,9 +2175,9 @@
|
|||||||
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="
|
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
"caniuse-lite": {
|
||||||
"version": "1.0.30001515",
|
"version": "1.0.30001534",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001515.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001534.tgz",
|
||||||
"integrity": "sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA=="
|
"integrity": "sha512-vlPVrhsCS7XaSh2VvWluIQEzVhefrUQcEsQWSS5A5V+dM07uv1qHeQzAOTGIMy9i3e9bH15+muvI/UHojVgS/Q=="
|
||||||
},
|
},
|
||||||
"chokidar": {
|
"chokidar": {
|
||||||
"version": "3.5.3",
|
"version": "3.5.3",
|
||||||
@ -2305,9 +2296,9 @@
|
|||||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
|
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
|
||||||
},
|
},
|
||||||
"electron-to-chromium": {
|
"electron-to-chromium": {
|
||||||
"version": "1.4.455",
|
"version": "1.4.520",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.455.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.520.tgz",
|
||||||
"integrity": "sha512-8tgdX0Odl24LtmLwxotpJCVjIndN559AvaOtd67u+2mo+IDsgsTF580NB+uuDCqsHw8yFg53l5+imFV9Fw3cbA=="
|
"integrity": "sha512-Frfus2VpYADsrh1lB3v/ft/WVFlVzOIm+Q0p7U7VqHI6qr7NWHYKe+Wif3W50n7JAFoBsWVsoU0+qDks6WQ60g=="
|
||||||
},
|
},
|
||||||
"esbuild": {
|
"esbuild": {
|
||||||
"version": "0.15.18",
|
"version": "0.15.18",
|
||||||
@ -2485,9 +2476,9 @@
|
|||||||
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
|
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
|
||||||
},
|
},
|
||||||
"fast-glob": {
|
"fast-glob": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
||||||
"integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==",
|
"integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@nodelib/fs.stat": "^2.0.2",
|
"@nodelib/fs.stat": "^2.0.2",
|
||||||
"@nodelib/fs.walk": "^1.2.3",
|
"@nodelib/fs.walk": "^1.2.3",
|
||||||
@ -2528,9 +2519,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fraction.js": {
|
"fraction.js": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz",
|
||||||
"integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA=="
|
"integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg=="
|
||||||
},
|
},
|
||||||
"fs.realpath": {
|
"fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@ -2538,9 +2529,9 @@
|
|||||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||||
},
|
},
|
||||||
"fsevents": {
|
"fsevents": {
|
||||||
"version": "2.3.2",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"function-bind": {
|
"function-bind": {
|
||||||
@ -2605,9 +2596,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"is-core-module": {
|
"is-core-module": {
|
||||||
"version": "2.12.1",
|
"version": "2.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
|
||||||
"integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
|
"integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"has": "^1.0.3"
|
"has": "^1.0.3"
|
||||||
}
|
}
|
||||||
@ -2631,9 +2622,9 @@
|
|||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
||||||
},
|
},
|
||||||
"jiti": {
|
"jiti": {
|
||||||
"version": "1.19.1",
|
"version": "1.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz",
|
||||||
"integrity": "sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg=="
|
"integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA=="
|
||||||
},
|
},
|
||||||
"kleur": {
|
"kleur": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
@ -2767,9 +2758,9 @@
|
|||||||
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="
|
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"version": "8.4.25",
|
"version": "8.4.29",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.25.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz",
|
||||||
"integrity": "sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw==",
|
"integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"nanoid": "^3.3.6",
|
"nanoid": "^3.3.6",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
@ -2847,11 +2838,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resolve": {
|
"resolve": {
|
||||||
"version": "1.22.2",
|
"version": "1.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz",
|
||||||
"integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
|
"integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-core-module": "^2.11.0",
|
"is-core-module": "^2.13.0",
|
||||||
"path-parse": "^1.0.7",
|
"path-parse": "^1.0.7",
|
||||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||||
}
|
}
|
||||||
@ -2898,9 +2889,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"sucrase": {
|
"sucrase": {
|
||||||
"version": "3.32.0",
|
"version": "3.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
|
||||||
"integrity": "sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==",
|
"integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.2",
|
"@jridgewell/gen-mapping": "^0.3.2",
|
||||||
"commander": "^4.0.0",
|
"commander": "^4.0.0",
|
||||||
@ -2923,16 +2914,16 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"svelte-hmr": {
|
"svelte-hmr": {
|
||||||
"version": "0.15.2",
|
"version": "0.15.3",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
|
||||||
"integrity": "sha512-q/bAruCvFLwvNbeE1x3n37TYFb3mTBJ6TrCq6p2CoFbSTNhDE9oAtEfpy+wmc9So8AG0Tja+X0/mJzX9tSfvIg==",
|
"integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"tailwindcss": {
|
"tailwindcss": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
|
||||||
"integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==",
|
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@ -2954,7 +2945,6 @@
|
|||||||
"postcss-load-config": "^4.0.1",
|
"postcss-load-config": "^4.0.1",
|
||||||
"postcss-nested": "^6.0.1",
|
"postcss-nested": "^6.0.1",
|
||||||
"postcss-selector-parser": "^6.0.11",
|
"postcss-selector-parser": "^6.0.11",
|
||||||
"postcss-value-parser": "^4.2.0",
|
|
||||||
"resolve": "^1.22.2",
|
"resolve": "^1.22.2",
|
||||||
"sucrase": "^3.32.0"
|
"sucrase": "^3.32.0"
|
||||||
}
|
}
|
||||||
@ -3028,9 +3018,9 @@
|
|||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||||
},
|
},
|
||||||
"yaml": {
|
"yaml": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz",
|
||||||
"integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ=="
|
"integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "creddy",
|
"name": "creddy",
|
||||||
"version": "0.2.3",
|
"version": "0.3.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
669
src-tauri/Cargo.lock
generated
669
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "creddy"
|
name = "creddy"
|
||||||
version = "0.2.3"
|
version = "0.3.3"
|
||||||
description = "A friendly AWS credentials manager"
|
description = "A friendly AWS credentials manager"
|
||||||
authors = ["Joseph Montanaro"]
|
authors = ["Joseph Montanaro"]
|
||||||
license = ""
|
license = ""
|
||||||
@ -25,12 +25,11 @@ tauri-build = { version = "1.0.4", features = [] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tauri = { version = "1.2", features = ["dialog", "os-all", "system-tray"] }
|
tauri = { version = "1.2", features = ["dialog", "dialog-open", "global-shortcut", "os-all", "system-tray"] }
|
||||||
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
|
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
|
||||||
sodiumoxide = "0.2.7"
|
sodiumoxide = "0.2.7"
|
||||||
tokio = { version = ">=1.19", features = ["full"] }
|
tokio = { version = ">=1.19", features = ["full"] }
|
||||||
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls"] }
|
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls"] }
|
||||||
netstat2 = "0.9.1"
|
|
||||||
sysinfo = "0.26.8"
|
sysinfo = "0.26.8"
|
||||||
aws-types = "0.52.0"
|
aws-types = "0.52.0"
|
||||||
aws-sdk-sts = "0.22.0"
|
aws-sdk-sts = "0.22.0"
|
||||||
@ -46,6 +45,8 @@ clap = { version = "3.2.23", features = ["derive"] }
|
|||||||
is-terminal = "0.4.7"
|
is-terminal = "0.4.7"
|
||||||
argon2 = { version = "0.5.0", features = ["std"] }
|
argon2 = { version = "0.5.0", features = ["std"] }
|
||||||
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
||||||
|
which = "4.4.0"
|
||||||
|
windows = { version = "0.51.1", features = ["Win32_Foundation", "Win32_System_Pipes"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# by default Tauri runs in production mode
|
||||||
|
@ -42,6 +42,8 @@ pub fn run() -> tauri::Result<()> {
|
|||||||
ipc::save_credentials,
|
ipc::save_credentials,
|
||||||
ipc::get_config,
|
ipc::get_config,
|
||||||
ipc::save_config,
|
ipc::save_config,
|
||||||
|
ipc::launch_terminal,
|
||||||
|
ipc::get_setup_errors,
|
||||||
])
|
])
|
||||||
.setup(|app| rt::block_on(setup(app)))
|
.setup(|app| rt::block_on(setup(app)))
|
||||||
.build(tauri::generate_context!())?
|
.build(tauri::generate_context!())?
|
||||||
@ -74,19 +76,41 @@ pub async fn connect_db() -> Result<SqlitePool, SetupError> {
|
|||||||
async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
|
async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
|
||||||
APP.set(app.handle()).unwrap();
|
APP.set(app.handle()).unwrap();
|
||||||
|
|
||||||
|
// get_or_create_db_path doesn't create the actual db file, just the directory
|
||||||
|
let is_first_launch = !config::get_or_create_db_path()?.exists();
|
||||||
let pool = connect_db().await?;
|
let pool = connect_db().await?;
|
||||||
let conf = AppConfig::load(&pool).await?;
|
let mut setup_errors: Vec<String> = vec![];
|
||||||
|
|
||||||
|
let conf = match AppConfig::load(&pool).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(SetupError::ConfigParseError(_)) => {
|
||||||
|
setup_errors.push(
|
||||||
|
"Could not load configuration from database. Reverting to defaults.".into()
|
||||||
|
);
|
||||||
|
AppConfig::default()
|
||||||
|
},
|
||||||
|
err => err?,
|
||||||
|
};
|
||||||
|
|
||||||
let session = Session::load(&pool).await?;
|
let session = Session::load(&pool).await?;
|
||||||
let srv = Server::new(conf.listen_addr, conf.listen_port, app.handle()).await?;
|
Server::start(app.handle())?;
|
||||||
|
|
||||||
config::set_auto_launch(conf.start_on_login)?;
|
config::set_auto_launch(conf.start_on_login)?;
|
||||||
if !conf.start_minimized {
|
if let Err(_e) = config::set_auto_launch(conf.start_on_login) {
|
||||||
|
setup_errors.push("Error: Failed to manage autolaunch.".into());
|
||||||
|
}
|
||||||
|
if let Err(e) = config::register_hotkeys(&conf.hotkeys) {
|
||||||
|
setup_errors.push(format!("{e}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// if session is empty, this is probably the first launch, so don't autohide
|
||||||
|
if !conf.start_minimized || is_first_launch {
|
||||||
app.get_window("main")
|
app.get_window("main")
|
||||||
.ok_or(HandlerError::NoMainWindow)?
|
.ok_or(HandlerError::NoMainWindow)?
|
||||||
.show()?;
|
.show()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = AppState::new(conf, session, srv, pool);
|
let state = AppState::new(conf, session, pool, setup_errors);
|
||||||
app.manage(state);
|
app.manage(state);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -19,13 +19,14 @@ fn main() {
|
|||||||
|
|
||||||
let res = match args.subcommand() {
|
let res = match args.subcommand() {
|
||||||
None | Some(("run", _)) => launch_gui(),
|
None | Some(("run", _)) => launch_gui(),
|
||||||
Some(("show", m)) => cli::show(m),
|
Some(("get", m)) => cli::get(m),
|
||||||
Some(("exec", m)) => cli::exec(m),
|
Some(("exec", m)) => cli::exec(m),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
eprintln!("Error: {e}");
|
eprintln!("Error: {e}");
|
||||||
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
use std::ffi::OsString;
|
||||||
use std::process::Command as ChildCommand;
|
use std::process::Command as ChildCommand;
|
||||||
#[cfg(unix)]
|
use std::time::Duration;
|
||||||
use std::os::unix::process::CommandExt;
|
|
||||||
|
|
||||||
use clap::{
|
use clap::{
|
||||||
Command,
|
Command,
|
||||||
@ -8,28 +8,37 @@ use clap::{
|
|||||||
ArgMatches,
|
ArgMatches,
|
||||||
ArgAction
|
ArgAction
|
||||||
};
|
};
|
||||||
use tokio::{
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
net::TcpStream,
|
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
use crate::credentials::Credentials;
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::server::{Request, Response};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use {
|
||||||
|
std::os::unix::process::CommandExt,
|
||||||
|
std::path::Path,
|
||||||
|
tokio::net::UnixStream,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
use crate::app;
|
use {
|
||||||
use crate::config::AppConfig;
|
tokio::net::windows::named_pipe::{NamedPipeClient, ClientOptions},
|
||||||
use crate::credentials::{BaseCredentials, SessionCredentials};
|
windows::Win32::Foundation::ERROR_PIPE_BUSY,
|
||||||
use crate::errors::*;
|
};
|
||||||
|
|
||||||
|
|
||||||
pub fn parser() -> Command<'static> {
|
pub fn parser() -> Command<'static> {
|
||||||
Command::new("creddy")
|
Command::new("creddy")
|
||||||
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
.about("A friendly AWS credentials manager")
|
.about("A friendly AWS credentials manager")
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("run")
|
Command::new("run")
|
||||||
.about("Launch Creddy")
|
.about("Launch Creddy")
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("show")
|
Command::new("get")
|
||||||
.about("Fetch and display AWS credentials")
|
.about("Request AWS credentials from Creddy and output to stdout")
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("base")
|
Arg::new("base")
|
||||||
.short('b')
|
.short('b')
|
||||||
@ -57,10 +66,13 @@ pub fn parser() -> Command<'static> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn show(args: &ArgMatches) -> Result<(), CliError> {
|
pub fn get(args: &ArgMatches) -> Result<(), CliError> {
|
||||||
let base = args.get_one("base").unwrap_or(&false);
|
let base = args.get_one("base").unwrap_or(&false);
|
||||||
let creds = get_credentials(*base)?;
|
let output = match get_credentials(*base)? {
|
||||||
println!("{creds}");
|
Credentials::Base(creds) => serde_json::to_string(&creds).unwrap(),
|
||||||
|
Credentials::Session(creds) => serde_json::to_string(&creds).unwrap(),
|
||||||
|
};
|
||||||
|
println!("{output}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,31 +86,42 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
|
|||||||
let mut cmd = ChildCommand::new(cmd_name);
|
let mut cmd = ChildCommand::new(cmd_name);
|
||||||
cmd.args(cmd_line);
|
cmd.args(cmd_line);
|
||||||
|
|
||||||
if base {
|
match get_credentials(base)? {
|
||||||
let creds: BaseCredentials = serde_json::from_str(&get_credentials(base)?)
|
Credentials::Base(creds) => {
|
||||||
.map_err(|_| RequestError::InvalidJson)?;
|
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
||||||
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
||||||
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
},
|
||||||
}
|
Credentials::Session(creds) => {
|
||||||
else {
|
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
||||||
let creds: SessionCredentials = serde_json::from_str(&get_credentials(base)?)
|
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
||||||
.map_err(|_| RequestError::InvalidJson)?;
|
cmd.env("AWS_SESSION_TOKEN", creds.session_token);
|
||||||
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
}
|
||||||
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
|
||||||
cmd.env("AWS_SESSION_TOKEN", creds.token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
let e = cmd.exec(); // never returns if successful
|
// cmd.exec() never returns if successful
|
||||||
Err(ExecError::ExecutionFailed(e))?;
|
let e = cmd.exec();
|
||||||
Ok(())
|
match e.kind() {
|
||||||
|
std::io::ErrorKind::NotFound => {
|
||||||
|
let name: OsString = cmd_name.into();
|
||||||
|
Err(ExecError::NotFound(name).into())
|
||||||
|
}
|
||||||
|
_ => Err(ExecError::ExecutionFailed(e).into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let mut child = cmd.spawn()
|
let mut child = match cmd.spawn() {
|
||||||
.map_err(|e| ExecError::ExecutionFailed(e))?;
|
Ok(c) => c,
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
let name: OsString = cmd_name.into();
|
||||||
|
return Err(ExecError::NotFound(name).into());
|
||||||
|
}
|
||||||
|
Err(e) => return Err(ExecError::ExecutionFailed(e).into()),
|
||||||
|
};
|
||||||
|
|
||||||
let status = child.wait()
|
let status = child.wait()
|
||||||
.map_err(|e| ExecError::ExecutionFailed(e))?;
|
.map_err(|e| ExecError::ExecutionFailed(e))?;
|
||||||
std::process::exit(status.code().unwrap_or(1));
|
std::process::exit(status.code().unwrap_or(1));
|
||||||
@ -107,40 +130,44 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
|
|||||||
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn get_credentials(base: bool) -> Result<String, RequestError> {
|
async fn get_credentials(base: bool) -> Result<Credentials, RequestError> {
|
||||||
let pool = app::connect_db().await?;
|
let req = Request::GetAwsCredentials { base };
|
||||||
let config = AppConfig::load(&pool).await?;
|
let mut data = serde_json::to_string(&req).unwrap();
|
||||||
let path = if base {"/creddy/base-credentials"} else {"/"};
|
// server expects newline marking end of request
|
||||||
|
data.push('\n');
|
||||||
|
|
||||||
let mut stream = TcpStream::connect((config.listen_addr, config.listen_port)).await?;
|
let mut stream = connect().await?;
|
||||||
let req = format!("GET {path} HTTP/1.0\r\n\r\n");
|
stream.write_all(&data.as_bytes()).await?;
|
||||||
stream.write_all(req.as_bytes()).await?;
|
|
||||||
|
|
||||||
// some day we'll have a proper HTTP parser
|
let mut buf = Vec::with_capacity(1024);
|
||||||
let mut buf = vec![0; 8192];
|
|
||||||
stream.read_to_end(&mut buf).await?;
|
stream.read_to_end(&mut buf).await?;
|
||||||
|
let res: Result<Response, ServerError> = serde_json::from_slice(&buf)?;
|
||||||
let status = buf.split(|&c| &[c] == b" ")
|
match res {
|
||||||
.skip(1)
|
Ok(Response::Aws(creds)) => Ok(creds),
|
||||||
.next()
|
// Eventually we will want this
|
||||||
.ok_or(RequestError::MalformedHttpResponse)?;
|
// Ok(r) => Err(RequestError::Unexpected(r)),
|
||||||
|
Err(e) => Err(RequestError::Server(e)),
|
||||||
if status != b"200" {
|
|
||||||
let s = String::from_utf8_lossy(status).to_string();
|
|
||||||
return Err(RequestError::Failed(s));
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let break_idx = buf.windows(4)
|
|
||||||
.position(|w| w == b"\r\n\r\n")
|
|
||||||
.ok_or(RequestError::MalformedHttpResponse)?;
|
#[cfg(windows)]
|
||||||
let body = &buf[(break_idx + 4)..];
|
async fn connect() -> Result<NamedPipeClient, std::io::Error> {
|
||||||
|
// apparently attempting to connect can fail if there's already a client connected
|
||||||
let creds_str = std::str::from_utf8(body)
|
loop {
|
||||||
.map_err(|_| RequestError::MalformedHttpResponse)?
|
match ClientOptions::new().open(r"\\.\pipe\creddy-requests") {
|
||||||
.to_string();
|
Ok(stream) => return Ok(stream),
|
||||||
|
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => (),
|
||||||
if creds_str == "Denied!" {
|
Err(e) => return Err(e),
|
||||||
return Err(RequestError::Rejected);
|
}
|
||||||
}
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||||
Ok(creds_str)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn connect() -> Result<UnixStream, std::io::Error> {
|
||||||
|
let path = Path::from("/tmp/creddy-requests");
|
||||||
|
std::fs::remove_file(path)?;
|
||||||
|
UnixStream::connect(path)
|
||||||
}
|
}
|
||||||
|
@ -1,76 +1,122 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use netstat2::{AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo};
|
|
||||||
use tauri::Manager;
|
|
||||||
use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt};
|
use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt};
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
use std::os::windows::io::AsRawHandle;
|
||||||
|
|
||||||
use crate::{
|
#[cfg(windows)]
|
||||||
app::APP,
|
use {
|
||||||
errors::*,
|
tokio::net::windows::named_pipe::NamedPipeServer,
|
||||||
config::AppConfig,
|
windows::Win32::{
|
||||||
state::AppState,
|
Foundation::HANDLE,
|
||||||
|
System::Pipes::GetNamedPipeClientProcessId,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use tokio::net::UnixStream;
|
||||||
|
|
||||||
|
use crate::errors::*;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
pub pid: u32,
|
pub pid: u32,
|
||||||
pub exe: PathBuf,
|
pub exe: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::error::Error> {
|
#[cfg(unix)]
|
||||||
let state = APP.get().unwrap().state::<AppState>();
|
pub fn get_client_parent(stream: &UnixStream) -> Result<Client, ClientInfoError> {
|
||||||
let AppConfig {
|
let pid = stream.peer_cred()?;
|
||||||
listen_addr: app_listen_addr,
|
get_process_parent_info(pid)?
|
||||||
listen_port: app_listen_port,
|
|
||||||
..
|
|
||||||
} = *state.config.read().await;
|
|
||||||
|
|
||||||
let sockets_iter = netstat2::iterate_sockets_info(
|
|
||||||
AddressFamilyFlags::IPV4,
|
|
||||||
ProtocolFlags::TCP
|
|
||||||
)?;
|
|
||||||
for item in sockets_iter {
|
|
||||||
let sock_info = item?;
|
|
||||||
let proto_info = match sock_info.protocol_socket_info {
|
|
||||||
ProtocolSocketInfo::Tcp(tcp_info) => tcp_info,
|
|
||||||
ProtocolSocketInfo::Udp(_) => {continue;}
|
|
||||||
};
|
|
||||||
|
|
||||||
if proto_info.local_port == local_port
|
|
||||||
&& proto_info.remote_port == app_listen_port
|
|
||||||
&& proto_info.local_addr == app_listen_addr
|
|
||||||
&& proto_info.remote_addr == app_listen_addr
|
|
||||||
{
|
|
||||||
return Ok(sock_info.associated_pids)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn get_client_parent(stream: &NamedPipeServer) -> Result<Client, ClientInfoError> {
|
||||||
|
let raw_handle = stream.as_raw_handle();
|
||||||
|
let mut pid = 0u32;
|
||||||
|
let handle = HANDLE(raw_handle as _);
|
||||||
|
unsafe { GetNamedPipeClientProcessId(handle, &mut pid as *mut u32)? };
|
||||||
|
|
||||||
|
get_process_parent_info(pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn get_process_parent_info(pid: u32) -> Result<Client, ClientInfoError> {
|
||||||
|
let sys_pid = Pid::from_u32(pid);
|
||||||
|
let mut sys = System::new();
|
||||||
|
sys.refresh_process(sys_pid);
|
||||||
|
let proc = sys.process(sys_pid)
|
||||||
|
.ok_or(ClientInfoError::ProcessNotFound)?;
|
||||||
|
|
||||||
|
let parent_pid_sys = proc.parent()
|
||||||
|
.ok_or(ClientInfoError::ParentPidNotFound)?;
|
||||||
|
sys.refresh_process(parent_pid_sys);
|
||||||
|
let parent = sys.process(parent_pid_sys)
|
||||||
|
.ok_or(ClientInfoError::ParentProcessNotFound)?;
|
||||||
|
|
||||||
|
let exe = match parent.exe() {
|
||||||
|
p if p == Path::new("") => None,
|
||||||
|
p => Some(PathBuf::from(p)),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Client { pid: parent_pid_sys.as_u32(), exe })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// async fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::error::Error> {
|
||||||
|
// let state = APP.get().unwrap().state::<AppState>();
|
||||||
|
// let AppConfig {
|
||||||
|
// listen_addr: app_listen_addr,
|
||||||
|
// listen_port: app_listen_port,
|
||||||
|
// ..
|
||||||
|
// } = *state.config.read().await;
|
||||||
|
|
||||||
|
// let sockets_iter = netstat2::iterate_sockets_info(
|
||||||
|
// AddressFamilyFlags::IPV4,
|
||||||
|
// ProtocolFlags::TCP
|
||||||
|
// )?;
|
||||||
|
// for item in sockets_iter {
|
||||||
|
// let sock_info = item?;
|
||||||
|
// let proto_info = match sock_info.protocol_socket_info {
|
||||||
|
// ProtocolSocketInfo::Tcp(tcp_info) => tcp_info,
|
||||||
|
// ProtocolSocketInfo::Udp(_) => {continue;}
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if proto_info.local_port == local_port
|
||||||
|
// && proto_info.remote_port == app_listen_port
|
||||||
|
// && proto_info.local_addr == app_listen_addr
|
||||||
|
// && proto_info.remote_addr == app_listen_addr
|
||||||
|
// {
|
||||||
|
// return Ok(sock_info.associated_pids)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Ok(vec![])
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
// Theoretically, on some systems, multiple processes can share a socket
|
// Theoretically, on some systems, multiple processes can share a socket
|
||||||
pub async fn get_clients(local_port: u16) -> Result<Vec<Option<Client>>, ClientInfoError> {
|
// pub async fn get_clients(local_port: u16) -> Result<Vec<Option<Client>>, ClientInfoError> {
|
||||||
let mut clients = Vec::new();
|
// let mut clients = Vec::new();
|
||||||
let mut sys = System::new();
|
// let mut sys = System::new();
|
||||||
for p in get_associated_pids(local_port).await? {
|
// for p in get_associated_pids(local_port).await? {
|
||||||
let pid = Pid::from_u32(p);
|
// let pid = Pid::from_u32(p);
|
||||||
sys.refresh_process(pid);
|
// sys.refresh_process(pid);
|
||||||
let proc = sys.process(pid)
|
// let proc = sys.process(pid)
|
||||||
.ok_or(ClientInfoError::ProcessNotFound)?;
|
// .ok_or(ClientInfoError::ProcessNotFound)?;
|
||||||
|
|
||||||
let client = Client {
|
// let client = Client {
|
||||||
pid: p,
|
// pid: p,
|
||||||
exe: proc.exe().to_path_buf(),
|
// exe: proc.exe().to_path_buf(),
|
||||||
};
|
// };
|
||||||
clients.push(Some(client));
|
// clients.push(Some(client));
|
||||||
}
|
// }
|
||||||
|
|
||||||
if clients.is_empty() {
|
// if clients.is_empty() {
|
||||||
clients.push(None);
|
// clients.push(None);
|
||||||
}
|
// }
|
||||||
|
|
||||||
Ok(clients)
|
// Ok(clients)
|
||||||
}
|
// }
|
||||||
|
@ -1,37 +1,67 @@
|
|||||||
use std::net::Ipv4Addr;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use auto_launch::AutoLaunchBuilder;
|
use auto_launch::AutoLaunchBuilder;
|
||||||
use is_terminal::IsTerminal;
|
use is_terminal::IsTerminal;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
use tauri::{
|
||||||
|
Manager,
|
||||||
|
GlobalShortcutManager,
|
||||||
|
async_runtime as rt,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TermConfig {
|
||||||
|
pub name: String,
|
||||||
|
// we call it exec because it isn't always the actual path,
|
||||||
|
// in some cases it's just the name and relies on path-searching
|
||||||
|
// it's a string because it can come from the frontend as json
|
||||||
|
pub exec: String,
|
||||||
|
pub args: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
|
pub struct Hotkey {
|
||||||
|
pub keys: String,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
|
pub struct HotkeysConfig {
|
||||||
|
// tauri uses strings to represent keybinds, so we will as well
|
||||||
|
pub show_window: Hotkey,
|
||||||
|
pub launch_terminal: Hotkey,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
#[serde(default = "default_listen_addr")]
|
|
||||||
pub listen_addr: Ipv4Addr,
|
|
||||||
#[serde(default = "default_listen_port")]
|
|
||||||
pub listen_port: u16,
|
|
||||||
#[serde(default = "default_rehide_ms")]
|
#[serde(default = "default_rehide_ms")]
|
||||||
pub rehide_ms: u64,
|
pub rehide_ms: u64,
|
||||||
#[serde(default = "default_start_minimized")]
|
#[serde(default = "default_start_minimized")]
|
||||||
pub start_minimized: bool,
|
pub start_minimized: bool,
|
||||||
#[serde(default = "default_start_on_login")]
|
#[serde(default = "default_start_on_login")]
|
||||||
pub start_on_login: bool,
|
pub start_on_login: bool,
|
||||||
|
#[serde(default = "default_term_config")]
|
||||||
|
pub terminal: TermConfig,
|
||||||
|
#[serde(default = "default_hotkey_config")]
|
||||||
|
pub hotkeys: HotkeysConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Default for AppConfig {
|
impl Default for AppConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
AppConfig {
|
AppConfig {
|
||||||
listen_addr: default_listen_addr(),
|
|
||||||
listen_port: default_listen_port(),
|
|
||||||
rehide_ms: default_rehide_ms(),
|
rehide_ms: default_rehide_ms(),
|
||||||
start_minimized: default_start_minimized(),
|
start_minimized: default_start_minimized(),
|
||||||
start_on_login: default_start_on_login(),
|
start_on_login: default_start_on_login(),
|
||||||
|
terminal: default_term_config(),
|
||||||
|
hotkeys: default_hotkey_config(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,16 +137,90 @@ pub fn get_or_create_db_path() -> Result<PathBuf, DataDirError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn default_listen_port() -> u16 {
|
fn default_term_config() -> TermConfig {
|
||||||
if cfg!(debug_assertions) {
|
#[cfg(windows)]
|
||||||
12_345
|
{
|
||||||
|
let shell = if which::which("pwsh.exe").is_ok() {
|
||||||
|
"pwsh.exe".to_string()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
"powershell.exe".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (exec, args) = if cfg!(debug_assertions) {
|
||||||
|
("conhost.exe".to_string(), vec![shell.clone()])
|
||||||
|
} else {
|
||||||
|
(shell.clone(), vec![])
|
||||||
|
};
|
||||||
|
|
||||||
|
TermConfig { name: shell, exec, args }
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
19_923
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
for bin in ["gnome-terminal", "konsole"] {
|
||||||
|
if let Ok(_) = which::which(bin) {
|
||||||
|
return TermConfig {
|
||||||
|
name: bin.into(),
|
||||||
|
exec: bin.into(),
|
||||||
|
args: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TermConfig {
|
||||||
|
name: "gnome-terminal".into(),
|
||||||
|
exec: "gnome-terminal".into(),
|
||||||
|
args: vec![],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_listen_addr() -> Ipv4Addr { Ipv4Addr::LOCALHOST }
|
|
||||||
|
fn default_hotkey_config() -> HotkeysConfig {
|
||||||
|
HotkeysConfig {
|
||||||
|
show_window: Hotkey {keys: "alt+shift+C".into(), enabled: true},
|
||||||
|
launch_terminal: Hotkey {keys: "alt+shift+T".into(), enabled: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// note: will panic if called before APP is set
|
||||||
|
pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> tauri::Result<()> {
|
||||||
|
let app = crate::app::APP.get().unwrap();
|
||||||
|
|
||||||
|
let mut manager = app.global_shortcut_manager();
|
||||||
|
manager.unregister_all()?;
|
||||||
|
|
||||||
|
if hotkeys.show_window.enabled {
|
||||||
|
let handle = app.app_handle();
|
||||||
|
manager.register(
|
||||||
|
&hotkeys.show_window.keys,
|
||||||
|
move || {
|
||||||
|
handle.get_window("main")
|
||||||
|
.map(|w| w.show().error_popup("Failed to show"))
|
||||||
|
.ok_or(HandlerError::NoMainWindow)
|
||||||
|
.error_popup("No main window");
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if hotkeys.launch_terminal.enabled {
|
||||||
|
// register() doesn't take an async fn, so we have to use spawn
|
||||||
|
manager.register(
|
||||||
|
&hotkeys.launch_terminal.keys,
|
||||||
|
|| {
|
||||||
|
rt::spawn(async {
|
||||||
|
crate::terminal::launch(false)
|
||||||
|
.await
|
||||||
|
.error_popup("Failed to launch");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fn default_rehide_ms() -> u64 { 1000 }
|
fn default_rehide_ms() -> u64 { 1000 }
|
||||||
// start minimized and on login only in production mode
|
// start minimized and on login only in production mode
|
||||||
fn default_start_minimized() -> bool { !cfg!(debug_assertions) }
|
fn default_start_minimized() -> bool { !cfg!(debug_assertions) }
|
||||||
|
@ -81,6 +81,16 @@ impl Session {
|
|||||||
Session::Empty => Err(GetSessionError::CredentialsEmpty),
|
Session::Empty => Err(GetSessionError::CredentialsEmpty),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn try_get(
|
||||||
|
&self
|
||||||
|
) -> Result<(&BaseCredentials, &SessionCredentials), GetCredentialsError> {
|
||||||
|
match self {
|
||||||
|
Self::Empty => Err(GetCredentialsError::Empty),
|
||||||
|
Self::Locked(_) => Err(GetCredentialsError::Locked),
|
||||||
|
Self::Unlocked{ ref base, ref session } => Ok((base, session))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -152,9 +162,10 @@ impl BaseCredentials {
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
pub struct SessionCredentials {
|
pub struct SessionCredentials {
|
||||||
|
pub version: usize,
|
||||||
pub access_key_id: String,
|
pub access_key_id: String,
|
||||||
pub secret_access_key: String,
|
pub secret_access_key: String,
|
||||||
pub token: String,
|
pub session_token: String,
|
||||||
#[serde(serialize_with = "serialize_expiration")]
|
#[serde(serialize_with = "serialize_expiration")]
|
||||||
#[serde(deserialize_with = "deserialize_expiration")]
|
#[serde(deserialize_with = "deserialize_expiration")]
|
||||||
pub expiration: DateTime,
|
pub expiration: DateTime,
|
||||||
@ -188,7 +199,7 @@ impl SessionCredentials {
|
|||||||
let secret_access_key = aws_session.secret_access_key()
|
let secret_access_key = aws_session.secret_access_key()
|
||||||
.ok_or(GetSessionError::EmptyResponse)?
|
.ok_or(GetSessionError::EmptyResponse)?
|
||||||
.to_string();
|
.to_string();
|
||||||
let token = aws_session.session_token()
|
let session_token = aws_session.session_token()
|
||||||
.ok_or(GetSessionError::EmptyResponse)?
|
.ok_or(GetSessionError::EmptyResponse)?
|
||||||
.to_string();
|
.to_string();
|
||||||
let expiration = aws_session.expiration()
|
let expiration = aws_session.expiration()
|
||||||
@ -196,9 +207,10 @@ impl SessionCredentials {
|
|||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
let session_creds = SessionCredentials {
|
let session_creds = SessionCredentials {
|
||||||
|
version: 1,
|
||||||
access_key_id,
|
access_key_id,
|
||||||
secret_access_key,
|
secret_access_key,
|
||||||
token,
|
session_token,
|
||||||
expiration,
|
expiration,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -220,6 +232,14 @@ impl SessionCredentials {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Credentials {
|
||||||
|
Base(BaseCredentials),
|
||||||
|
Session(SessionCredentials),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fn serialize_expiration<S>(exp: &DateTime, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize_expiration<S>(exp: &DateTime, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where S: Serializer
|
where S: Serializer
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::convert::AsRef;
|
use std::convert::AsRef;
|
||||||
|
use std::ffi::OsString;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
use std::string::FromUtf8Error;
|
||||||
use strum_macros::AsRefStr;
|
use strum_macros::AsRefStr;
|
||||||
|
|
||||||
use thiserror::Error as ThisError;
|
use thiserror::Error as ThisError;
|
||||||
@ -16,14 +18,20 @@ use tauri::api::dialog::{
|
|||||||
MessageDialogBuilder,
|
MessageDialogBuilder,
|
||||||
MessageDialogKind,
|
MessageDialogKind,
|
||||||
};
|
};
|
||||||
use serde::{Serialize, Serializer, ser::SerializeMap};
|
use serde::{
|
||||||
|
Serialize,
|
||||||
|
Serializer,
|
||||||
|
ser::SerializeMap,
|
||||||
|
Deserialize,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
pub trait ErrorPopup {
|
pub trait ErrorPopup {
|
||||||
fn error_popup(self, title: &str);
|
fn error_popup(self, title: &str);
|
||||||
|
fn error_popup_nowait(self, title: &str);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: Error> ErrorPopup for Result<(), E> {
|
impl<E: std::fmt::Display> ErrorPopup for Result<(), E> {
|
||||||
fn error_popup(self, title: &str) {
|
fn error_popup(self, title: &str) {
|
||||||
if let Err(e) = self {
|
if let Err(e) = self {
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
@ -34,6 +42,14 @@ impl<E: Error> ErrorPopup for Result<(), E> {
|
|||||||
rx.recv().unwrap();
|
rx.recv().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn error_popup_nowait(self, title: &str) {
|
||||||
|
if let Err(e) = self {
|
||||||
|
MessageDialogBuilder::new(title, format!("{e}"))
|
||||||
|
.kind(MessageDialogKind::Error)
|
||||||
|
.show(|_| {})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -57,8 +73,12 @@ where
|
|||||||
E: Error,
|
E: Error,
|
||||||
M: serde::ser::SerializeMap,
|
M: serde::ser::SerializeMap,
|
||||||
{
|
{
|
||||||
let src = err.source().map(|s| format!("{s}"));
|
let msg = err.source().map(|s| format!("{s}"));
|
||||||
map.serialize_entry("source", &src)
|
map.serialize_entry("msg", &msg)?;
|
||||||
|
map.serialize_entry("code", &None::<&str>)?;
|
||||||
|
map.serialize_entry("source", &None::<&str>)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -90,6 +110,8 @@ pub enum SetupError {
|
|||||||
ServerSetupError(#[from] std::io::Error),
|
ServerSetupError(#[from] std::io::Error),
|
||||||
#[error("Failed to resolve data directory: {0}")]
|
#[error("Failed to resolve data directory: {0}")]
|
||||||
DataDir(#[from] DataDirError),
|
DataDir(#[from] DataDirError),
|
||||||
|
#[error("Failed to register hotkeys: {0}")]
|
||||||
|
RegisterHotkeys(#[from] tauri::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -109,6 +131,8 @@ pub enum SendResponseError {
|
|||||||
NotFound,
|
NotFound,
|
||||||
#[error("The specified request was already closed by the client")]
|
#[error("The specified request was already closed by the client")]
|
||||||
Abandoned,
|
Abandoned,
|
||||||
|
#[error("A response has already been received for the specified request")]
|
||||||
|
Fulfilled,
|
||||||
#[error("Could not renew AWS sesssion: {0}")]
|
#[error("Could not renew AWS sesssion: {0}")]
|
||||||
SessionRenew(#[from] GetSessionError),
|
SessionRenew(#[from] GetSessionError),
|
||||||
}
|
}
|
||||||
@ -119,12 +143,14 @@ pub enum SendResponseError {
|
|||||||
pub enum HandlerError {
|
pub enum HandlerError {
|
||||||
#[error("Error writing to stream: {0}")]
|
#[error("Error writing to stream: {0}")]
|
||||||
StreamIOError(#[from] std::io::Error),
|
StreamIOError(#[from] std::io::Error),
|
||||||
// #[error("Received invalid UTF-8 in request")]
|
#[error("Received invalid UTF-8 in request")]
|
||||||
// InvalidUtf8,
|
InvalidUtf8(#[from] FromUtf8Error),
|
||||||
#[error("HTTP request malformed")]
|
#[error("HTTP request malformed")]
|
||||||
BadRequest(Vec<u8>),
|
BadRequest(#[from] serde_json::Error),
|
||||||
#[error("HTTP request too large")]
|
#[error("HTTP request too large")]
|
||||||
RequestTooLarge,
|
RequestTooLarge,
|
||||||
|
#[error("Internal server error")]
|
||||||
|
Internal,
|
||||||
#[error("Error accessing credentials: {0}")]
|
#[error("Error accessing credentials: {0}")]
|
||||||
NoCredentials(#[from] GetCredentialsError),
|
NoCredentials(#[from] GetCredentialsError),
|
||||||
#[error("Error getting client details: {0}")]
|
#[error("Error getting client details: {0}")]
|
||||||
@ -133,6 +159,8 @@ pub enum HandlerError {
|
|||||||
Tauri(#[from] tauri::Error),
|
Tauri(#[from] tauri::Error),
|
||||||
#[error("No main application window found")]
|
#[error("No main application window found")]
|
||||||
NoMainWindow,
|
NoMainWindow,
|
||||||
|
#[error("Request was denied")]
|
||||||
|
Denied,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -189,36 +217,49 @@ pub enum CryptoError {
|
|||||||
pub enum ClientInfoError {
|
pub enum ClientInfoError {
|
||||||
#[error("Found PID for client socket, but no corresponding process")]
|
#[error("Found PID for client socket, but no corresponding process")]
|
||||||
ProcessNotFound,
|
ProcessNotFound,
|
||||||
#[error("Couldn't get client socket details: {0}")]
|
#[error("Could not determine parent PID of connected client")]
|
||||||
NetstatError(#[from] netstat2::error::Error),
|
ParentPidNotFound,
|
||||||
|
#[error("Found PID for parent process of client, but no corresponding process")]
|
||||||
|
ParentProcessNotFound,
|
||||||
|
#[error("Could not determine PID of connected client")]
|
||||||
|
WindowsError(#[from] windows::core::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Technically also an error, but formatted as a struct for easy deserialization
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ServerError {
|
||||||
|
code: String,
|
||||||
|
msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ServerError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||||
|
write!(f, "{} ({})", self.msg, self.code)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Errors encountered while requesting credentials via CLI (creddy show, creddy exec)
|
// Errors encountered while requesting credentials via CLI (creddy show, creddy exec)
|
||||||
#[derive(Debug, ThisError, AsRefStr)]
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
pub enum RequestError {
|
pub enum RequestError {
|
||||||
#[error("Credentials request failed: HTTP {0}")]
|
#[error("Error response from server: {0}")]
|
||||||
Failed(String),
|
Server(ServerError),
|
||||||
#[error("Credentials request was rejected")]
|
#[error("Unexpected response from server")]
|
||||||
Rejected,
|
Unexpected(crate::server::Response),
|
||||||
#[error("Couldn't interpret the server's response")]
|
|
||||||
MalformedHttpResponse,
|
|
||||||
#[error("The server did not respond with valid JSON")]
|
#[error("The server did not respond with valid JSON")]
|
||||||
InvalidJson,
|
InvalidJson(#[from] serde_json::Error),
|
||||||
#[error("Error reading/writing stream: {0}")]
|
#[error("Error reading/writing stream: {0}")]
|
||||||
StreamIOError(#[from] std::io::Error),
|
StreamIOError(#[from] std::io::Error),
|
||||||
#[error("Error loading configuration data: {0}")]
|
|
||||||
Setup(#[from] SetupError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<ServerError> for RequestError {
|
||||||
// Errors encountered while running a subprocess (via creddy exec)
|
fn from(s: ServerError) -> Self {
|
||||||
#[derive(Debug, ThisError, AsRefStr)]
|
Self::Server(s)
|
||||||
pub enum ExecError {
|
}
|
||||||
#[error("Please specify a command")]
|
|
||||||
NoCommand,
|
|
||||||
#[error("Failed to execute command: {0}")]
|
|
||||||
ExecutionFailed(#[from] std::io::Error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -233,6 +274,33 @@ pub enum CliError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Errors encountered while trying to launch a child process
|
||||||
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
|
pub enum ExecError {
|
||||||
|
#[error("Please specify a command")]
|
||||||
|
NoCommand,
|
||||||
|
#[error("Executable not found: {0:?}")]
|
||||||
|
NotFound(OsString),
|
||||||
|
#[error("Failed to execute command: {0}")]
|
||||||
|
ExecutionFailed(#[from] std::io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
GetCredentials(#[from] GetCredentialsError),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
|
pub enum LaunchTerminalError {
|
||||||
|
#[error("Could not discover main window")]
|
||||||
|
NoMainWindow,
|
||||||
|
#[error("Failed to communicate with main Creddy window")]
|
||||||
|
IpcFailed(#[from] tauri::Error),
|
||||||
|
#[error("Failed to launch terminal: {0}")]
|
||||||
|
Exec(#[from] ExecError),
|
||||||
|
#[error(transparent)]
|
||||||
|
GetCredentials(#[from] GetCredentialsError),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Serialize implementations
|
// Serialize implementations
|
||||||
// =========================
|
// =========================
|
||||||
@ -263,13 +331,6 @@ impl Serialize for HandlerError {
|
|||||||
let mut map = serializer.serialize_map(None)?;
|
let mut map = serializer.serialize_map(None)?;
|
||||||
map.serialize_entry("code", self.as_ref())?;
|
map.serialize_entry("code", self.as_ref())?;
|
||||||
map.serialize_entry("msg", &format!("{self}"))?;
|
map.serialize_entry("msg", &format!("{self}"))?;
|
||||||
|
|
||||||
match self {
|
|
||||||
HandlerError::NoCredentials(src) => map.serialize_entry("source", &src)?,
|
|
||||||
HandlerError::ClientInfo(src) => map.serialize_entry("source", &src)?,
|
|
||||||
_ => serialize_upstream_err(self, &mut map)?,
|
|
||||||
}
|
|
||||||
|
|
||||||
map.end()
|
map.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -318,6 +379,38 @@ impl Serialize for UnlockError {
|
|||||||
|
|
||||||
match self {
|
match self {
|
||||||
UnlockError::GetSession(src) => map.serialize_entry("source", &src)?,
|
UnlockError::GetSession(src) => map.serialize_entry("source", &src)?,
|
||||||
|
// The string representation of the AEAD error is not very helpful, so skip it
|
||||||
|
UnlockError::Crypto(_src) => map.serialize_entry("source", &None::<&str>)?,
|
||||||
|
_ => serialize_upstream_err(self, &mut map)?,
|
||||||
|
}
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Serialize for ExecError {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
let mut map = serializer.serialize_map(None)?;
|
||||||
|
map.serialize_entry("code", self.as_ref())?;
|
||||||
|
map.serialize_entry("msg", &format!("{self}"))?;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
ExecError::GetCredentials(src) => map.serialize_entry("source", &src)?,
|
||||||
|
_ => serialize_upstream_err(self, &mut map)?,
|
||||||
|
}
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Serialize for LaunchTerminalError {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
let mut map = serializer.serialize_map(None)?;
|
||||||
|
map.serialize_entry("code", self.as_ref())?;
|
||||||
|
map.serialize_entry("msg", &format!("{self}"))?;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
LaunchTerminalError::Exec(src) => map.serialize_entry("source", &src)?,
|
||||||
_ => serialize_upstream_err(self, &mut map)?,
|
_ => serialize_upstream_err(self, &mut map)?,
|
||||||
}
|
}
|
||||||
map.end()
|
map.end()
|
||||||
|
@ -6,12 +6,13 @@ use crate::credentials::{Session,BaseCredentials};
|
|||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::clientinfo::Client;
|
use crate::clientinfo::Client;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
use crate::terminal;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Request {
|
pub struct AwsRequestNotification {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub clients: Vec<Option<Client>>,
|
pub client: Client,
|
||||||
pub base: bool,
|
pub base: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,3 +79,15 @@ pub async fn save_config(config: AppConfig, app_state: State<'_, AppState>) -> R
|
|||||||
.map_err(|e| format!("Error saving config: {e}"))?;
|
.map_err(|e| format!("Error saving config: {e}"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn launch_terminal(base: bool) -> Result<(), LaunchTerminalError> {
|
||||||
|
terminal::launch(base).await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_setup_errors(app_state: State<'_, AppState>) -> Result<Vec<String>, ()> {
|
||||||
|
Ok(app_state.setup_errors.clone())
|
||||||
|
}
|
||||||
|
@ -7,4 +7,5 @@ mod clientinfo;
|
|||||||
mod ipc;
|
mod ipc;
|
||||||
mod state;
|
mod state;
|
||||||
mod server;
|
mod server;
|
||||||
|
mod terminal;
|
||||||
mod tray;
|
mod tray;
|
||||||
|
@ -16,12 +16,13 @@ fn main() {
|
|||||||
app::run().error_popup("Creddy failed to start");
|
app::run().error_popup("Creddy failed to start");
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
Some(("show", m)) => cli::show(m),
|
Some(("get", m)) => cli::get(m),
|
||||||
Some(("exec", m)) => cli::exec(m),
|
Some(("exec", m)) => cli::exec(m),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
eprintln!("Error: {e}");
|
eprintln!("Error: {e}");
|
||||||
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,243 +1,184 @@
|
|||||||
use core::time::Duration;
|
use std::time::Duration;
|
||||||
use std::io;
|
|
||||||
use std::net::{
|
#[cfg(windows)]
|
||||||
Ipv4Addr,
|
use tokio::net::windows::named_pipe::{
|
||||||
SocketAddr,
|
NamedPipeServer,
|
||||||
SocketAddrV4,
|
ServerOptions,
|
||||||
};
|
|
||||||
use tokio::net::{
|
|
||||||
TcpListener,
|
|
||||||
TcpStream,
|
|
||||||
};
|
};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use tokio::time::sleep;
|
|
||||||
|
|
||||||
use tauri::{AppHandle, Manager};
|
use serde::{Serialize, Deserialize};
|
||||||
use tauri::async_runtime as rt;
|
|
||||||
use tauri::async_runtime::JoinHandle;
|
use tauri::{
|
||||||
|
AppHandle,
|
||||||
|
Manager,
|
||||||
|
async_runtime as rt,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{clientinfo, clientinfo::Client};
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::ipc::{Request, Approval};
|
use crate::clientinfo::{self, Client};
|
||||||
|
use crate::credentials::Credentials;
|
||||||
|
use crate::ipc::{Approval, AwsRequestNotification};
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
|
||||||
struct Handler {
|
#[derive(Serialize, Deserialize)]
|
||||||
request_id: u64,
|
pub enum Request {
|
||||||
stream: TcpStream,
|
GetAwsCredentials{
|
||||||
receiver: Option<oneshot::Receiver<Approval>>,
|
base: bool,
|
||||||
app: AppHandle,
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handler {
|
|
||||||
async fn new(stream: TcpStream, app: AppHandle) -> Self {
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
let state = app.state::<AppState>();
|
pub enum Response {
|
||||||
let (chan_send, chan_recv) = oneshot::channel();
|
Aws(Credentials)
|
||||||
let request_id = state.register_request(chan_send).await;
|
|
||||||
Handler {
|
|
||||||
request_id,
|
|
||||||
stream,
|
|
||||||
receiver: Some(chan_recv),
|
|
||||||
app
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle(mut self) {
|
|
||||||
if let Err(e) = self.try_handle().await {
|
|
||||||
eprintln!("{e}");
|
|
||||||
}
|
|
||||||
let state = self.app.state::<AppState>();
|
|
||||||
state.unregister_request(self.request_id).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn try_handle(&mut self) -> Result<(), HandlerError> {
|
|
||||||
let req_path = self.recv_request().await?;
|
|
||||||
let clients = self.get_clients().await?;
|
|
||||||
if self.includes_banned(&clients).await {
|
|
||||||
self.stream.write(b"HTTP/1.0 403 Access Denied\r\n\r\n").await?;
|
|
||||||
return Ok(())
|
|
||||||
}
|
|
||||||
let base = req_path == b"/creddy/base-credentials";
|
|
||||||
|
|
||||||
let req = Request {id: self.request_id, clients, base};
|
|
||||||
self.app.emit_all("credentials-request", &req)?;
|
|
||||||
let starting_visibility = self.show_window()?;
|
|
||||||
|
|
||||||
match self.wait_for_response().await? {
|
|
||||||
Approval::Approved => {
|
|
||||||
let state = self.app.state::<AppState>();
|
|
||||||
let creds = if base {
|
|
||||||
state.serialize_base_creds().await?
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
state.serialize_session_creds().await?
|
|
||||||
};
|
|
||||||
self.send_body(creds.as_bytes()).await?;
|
|
||||||
},
|
|
||||||
Approval::Denied => {
|
|
||||||
let state = self.app.state::<AppState>();
|
|
||||||
for client in req.clients {
|
|
||||||
state.add_ban(client).await;
|
|
||||||
}
|
|
||||||
self.send_body(b"Denied!").await?;
|
|
||||||
self.stream.shutdown().await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// only hide the window if a) it was hidden to start with
|
|
||||||
// and b) there are no other pending requests
|
|
||||||
let state = self.app.state::<AppState>();
|
|
||||||
let delay = {
|
|
||||||
let config = state.config.read().await;
|
|
||||||
Duration::from_millis(config.rehide_ms)
|
|
||||||
};
|
|
||||||
sleep(delay).await;
|
|
||||||
|
|
||||||
if !starting_visibility && state.req_count().await == 0 {
|
|
||||||
let window = self.app.get_window("main").ok_or(HandlerError::NoMainWindow)?;
|
|
||||||
window.hide()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn recv_request(&mut self) -> Result<Vec<u8>, HandlerError> {
|
|
||||||
let mut buf = vec![0; 8192]; // it's what tokio's BufReader uses
|
|
||||||
let mut n = 0;
|
|
||||||
loop {
|
|
||||||
n += self.stream.read(&mut buf[n..]).await?;
|
|
||||||
if n >= 4 && &buf[(n - 4)..n] == b"\r\n\r\n" {break;}
|
|
||||||
if n == buf.len() {return Err(HandlerError::RequestTooLarge);}
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = buf.split(|&c| &[c] == b" ")
|
|
||||||
.skip(1)
|
|
||||||
.next()
|
|
||||||
.ok_or(HandlerError::BadRequest(buf.clone()))?;
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)] {
|
|
||||||
println!("Path: {}", std::str::from_utf8(&path).unwrap());
|
|
||||||
println!("{}", std::str::from_utf8(&buf).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(path.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_clients(&self) -> Result<Vec<Option<Client>>, HandlerError> {
|
|
||||||
let peer_addr = match self.stream.peer_addr()? {
|
|
||||||
SocketAddr::V4(addr) => addr,
|
|
||||||
_ => unreachable!(), // we only listen on IPv4
|
|
||||||
};
|
|
||||||
let clients = clientinfo::get_clients(peer_addr.port()).await?;
|
|
||||||
Ok(clients)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn includes_banned(&self, clients: &Vec<Option<Client>>) -> bool {
|
|
||||||
let state = self.app.state::<AppState>();
|
|
||||||
for client in clients {
|
|
||||||
if state.is_banned(client).await {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_window(&self) -> Result<bool, HandlerError> {
|
|
||||||
let window = self.app.get_window("main").ok_or(HandlerError::NoMainWindow)?;
|
|
||||||
let starting_visibility = window.is_visible()?;
|
|
||||||
if !starting_visibility {
|
|
||||||
window.unminimize()?;
|
|
||||||
window.show()?;
|
|
||||||
}
|
|
||||||
window.set_focus()?;
|
|
||||||
Ok(starting_visibility)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn wait_for_response(&mut self) -> Result<Approval, HandlerError> {
|
|
||||||
self.stream.write(b"HTTP/1.0 200 OK\r\n").await?;
|
|
||||||
self.stream.write(b"Content-Type: application/json\r\n").await?;
|
|
||||||
self.stream.write(b"X-Creddy-delaying-tactic: ").await?;
|
|
||||||
|
|
||||||
#[allow(unreachable_code)] // seems necessary for type inference
|
|
||||||
let stall = async {
|
|
||||||
let delay = std::time::Duration::from_secs(1);
|
|
||||||
loop {
|
|
||||||
tokio::time::sleep(delay).await;
|
|
||||||
self.stream.write(b"x").await?;
|
|
||||||
}
|
|
||||||
Ok(Approval::Denied)
|
|
||||||
};
|
|
||||||
|
|
||||||
// this is the only place we even read this field, so it's safe to unwrap
|
|
||||||
let receiver = self.receiver.take().unwrap();
|
|
||||||
tokio::select!{
|
|
||||||
r = receiver => Ok(r.unwrap()), // only panics if the sender is dropped without sending, which shouldn't be possible
|
|
||||||
e = stall => e,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_body(&mut self, body: &[u8]) -> Result<(), HandlerError> {
|
|
||||||
self.stream.write(b"\r\nContent-Length: ").await?;
|
|
||||||
self.stream.write(body.len().to_string().as_bytes()).await?;
|
|
||||||
self.stream.write(b"\r\n\r\n").await?;
|
|
||||||
self.stream.write(body).await?;
|
|
||||||
self.stream.shutdown().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
addr: Ipv4Addr,
|
listener: tokio::net::windows::named_pipe::NamedPipeServer,
|
||||||
port: u16,
|
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
task: JoinHandle<()>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
pub async fn new(addr: Ipv4Addr, port: u16, app_handle: AppHandle) -> io::Result<Server> {
|
pub fn start(app_handle: AppHandle) -> std::io::Result<()> {
|
||||||
let task = Self::start_server(addr, port, app_handle.app_handle()).await?;
|
let listener = ServerOptions::new()
|
||||||
Ok(Server { addr, port, app_handle, task})
|
.first_pipe_instance(true)
|
||||||
}
|
.create(r"\\.\pipe\creddy-requests")?;
|
||||||
|
|
||||||
pub async fn rebind(&mut self, addr: Ipv4Addr, port: u16) -> io::Result<()> {
|
let srv = Server {listener, app_handle};
|
||||||
if addr == self.addr && port == self.port {
|
rt::spawn(srv.serve());
|
||||||
return Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_task = Self::start_server(addr, port, self.app_handle.app_handle()).await?;
|
|
||||||
self.task.abort();
|
|
||||||
|
|
||||||
self.addr = addr;
|
|
||||||
self.port = port;
|
|
||||||
self.task = new_task;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// construct the listener before spawning the task so that we can return early if it fails
|
async fn serve(mut self) {
|
||||||
async fn start_server(addr: Ipv4Addr, port: u16, app_handle: AppHandle) -> io::Result<JoinHandle<()>> {
|
|
||||||
let sock_addr = SocketAddrV4::new(addr, port);
|
|
||||||
let listener = TcpListener::bind(&sock_addr).await?;
|
|
||||||
let task = rt::spawn(
|
|
||||||
Self::serve(listener, app_handle.app_handle())
|
|
||||||
);
|
|
||||||
Ok(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn serve(listener: TcpListener, app_handle: AppHandle) {
|
|
||||||
loop {
|
loop {
|
||||||
match listener.accept().await {
|
if let Err(e) = self.try_serve().await {
|
||||||
Ok((stream, _)) => {
|
eprintln!("Error accepting connection: {e}");
|
||||||
let handler = Handler::new(stream, app_handle.app_handle()).await;
|
|
||||||
rt::spawn(handler.handle());
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Error accepting connection: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn try_serve(&mut self) -> std::io::Result<()> {
|
||||||
|
// connect() just waits for a client to connect, it doesn't return anything
|
||||||
|
self.listener.connect().await?;
|
||||||
|
|
||||||
|
// create a new pipe instance to listen for the next client, and swap it in
|
||||||
|
let new_listener = ServerOptions::new().create(r"\\.\pipe\creddy-requests")?;
|
||||||
|
let mut stream = std::mem::replace(&mut self.listener, new_listener);
|
||||||
|
let new_handle = self.app_handle.app_handle();
|
||||||
|
rt::spawn(async move {
|
||||||
|
let res = serde_json::to_string(
|
||||||
|
&handle(&mut stream, new_handle).await
|
||||||
|
).unwrap();
|
||||||
|
if let Err(e) = stream.write_all(res.as_bytes()).await {
|
||||||
|
eprintln!("Error responding to request: {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn handle(stream: &mut NamedPipeServer, app_handle: AppHandle) -> Result<Response, HandlerError> {
|
||||||
|
// read from stream until delimiter is reached
|
||||||
|
let mut buf: Vec<u8> = Vec::with_capacity(1024); // requests are small, 1KiB is more than enough
|
||||||
|
let mut n = 0;
|
||||||
|
loop {
|
||||||
|
n += stream.read_buf(&mut buf).await?;
|
||||||
|
if let Some(&b'\n') = buf.last() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if n >= 1024 {
|
||||||
|
return Err(HandlerError::RequestTooLarge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = clientinfo::get_client_parent(&stream)?;
|
||||||
|
|
||||||
|
let req: Request = serde_json::from_slice(&buf)?;
|
||||||
|
match req {
|
||||||
|
Request::GetAwsCredentials{ base } => get_aws_credentials(base, client, app_handle).await,
|
||||||
|
// etc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle) -> Result<Response, HandlerError> {
|
||||||
|
let state = app_handle.state::<AppState>();
|
||||||
|
|
||||||
|
let main_window = app_handle.get_window("main").ok_or(HandlerError::NoMainWindow)?;
|
||||||
|
let is_currently_visible = main_window.is_visible()?;
|
||||||
|
let rehide_after = state.get_or_set_rehide(!is_currently_visible).await;
|
||||||
|
|
||||||
|
let (chan_send, chan_recv) = oneshot::channel();
|
||||||
|
let request_id = state.register_request(chan_send).await;
|
||||||
|
|
||||||
|
// if an error occurs in any of the following, we want to abort the operation
|
||||||
|
// but ? returns immediately, and we want to unregister the request before returning
|
||||||
|
// so we bundle it all up in an async block and return a Result so we can handle errors
|
||||||
|
let proceed = async {
|
||||||
|
let notification = AwsRequestNotification {id: request_id, client, base};
|
||||||
|
app_handle.emit_all("credentials-request", ¬ification)?;
|
||||||
|
|
||||||
|
if !main_window.is_visible()? {
|
||||||
|
main_window.unminimize()?;
|
||||||
|
main_window.show()?;
|
||||||
|
}
|
||||||
|
main_window.set_focus()?;
|
||||||
|
|
||||||
|
match chan_recv.await {
|
||||||
|
Ok(Approval::Approved) => {
|
||||||
|
if base {
|
||||||
|
let creds = state.base_creds_cloned().await?;
|
||||||
|
Ok(Response::Aws(Credentials::Base(creds)))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let creds = state.session_creds_cloned().await?;
|
||||||
|
Ok(Response::Aws(Credentials::Session(creds)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(Approval::Denied) => Err(HandlerError::Denied),
|
||||||
|
Err(_e) => Err(HandlerError::Internal),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = match proceed.await {
|
||||||
|
Ok(r) => Ok(r),
|
||||||
|
Err(e) => {
|
||||||
|
state.unregister_request(request_id).await;
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rt::spawn(
|
||||||
|
handle_rehide(rehide_after, app_handle.app_handle())
|
||||||
|
);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn handle_rehide(rehide_after: bool, app_handle: AppHandle) {
|
||||||
|
let state = app_handle.state::<AppState>();
|
||||||
|
let delay = {
|
||||||
|
let config = state.config.read().await;
|
||||||
|
Duration::from_millis(config.rehide_ms)
|
||||||
|
};
|
||||||
|
tokio::time::sleep(delay).await;
|
||||||
|
|
||||||
|
// if there are no other pending requests, set rehide status back to None
|
||||||
|
if state.req_count().await == 0 {
|
||||||
|
state.clear_rehide().await;
|
||||||
|
// and hide the window if necessary
|
||||||
|
if rehide_after {
|
||||||
|
app_handle.get_window("main").map(|w| {
|
||||||
|
if let Err(e) = w.hide() {
|
||||||
|
eprintln!("{e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
51
src-tauri/src/shortcuts.rs
Normal file
51
src-tauri/src/shortcuts.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
use tauri::{
|
||||||
|
AppHandle,
|
||||||
|
Manager,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::app::APP;
|
||||||
|
use crate::config::HotkeysConfig;
|
||||||
|
use crate::terminal;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum ShortcutAction {
|
||||||
|
ShowWindow,
|
||||||
|
LaunchTerminal,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn exec_shortcut(action: ShortcutAction) {
|
||||||
|
match action {
|
||||||
|
ShowWindow => {
|
||||||
|
let app = APP.get().unwrap();
|
||||||
|
app.get_window("main").map(|w| w.show());
|
||||||
|
},
|
||||||
|
LaunchTerminal => terminal::launch(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> tauri::Result<()> {
|
||||||
|
let app = APP.get().unwrap();
|
||||||
|
let mut manager = app.global_shortcut_manager();
|
||||||
|
manager.unregister_all()?;
|
||||||
|
|
||||||
|
if hotkeys.show_window.enabled {
|
||||||
|
manager.register(
|
||||||
|
hotkeys.show_window.keys,
|
||||||
|
|| exec_shortcut(ShortcutAction::ShowWindow)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if hotkeys.launch_terminal.enabled {
|
||||||
|
manager.register(
|
||||||
|
&hotkeys.launch_terminal.keys,
|
||||||
|
|| exec_shortcut(ShortcutAction::LaunchTerminal)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,16 +1,11 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::oneshot::Sender,
|
|
||||||
sync::RwLock,
|
sync::RwLock,
|
||||||
time::sleep,
|
sync::oneshot::Sender,
|
||||||
};
|
};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use tauri::async_runtime as runtime;
|
|
||||||
use tauri::Manager;
|
|
||||||
|
|
||||||
use crate::app::APP;
|
|
||||||
use crate::credentials::{
|
use crate::credentials::{
|
||||||
Session,
|
Session,
|
||||||
BaseCredentials,
|
BaseCredentials,
|
||||||
@ -18,9 +13,7 @@ use crate::credentials::{
|
|||||||
};
|
};
|
||||||
use crate::{config, config::AppConfig};
|
use crate::{config, config::AppConfig};
|
||||||
use crate::ipc::{self, Approval};
|
use crate::ipc::{self, Approval};
|
||||||
use crate::clientinfo::Client;
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::server::Server;
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -28,21 +21,29 @@ pub struct AppState {
|
|||||||
pub config: RwLock<AppConfig>,
|
pub config: RwLock<AppConfig>,
|
||||||
pub session: RwLock<Session>,
|
pub session: RwLock<Session>,
|
||||||
pub request_count: RwLock<u64>,
|
pub request_count: RwLock<u64>,
|
||||||
pub open_requests: RwLock<HashMap<u64, Sender<ipc::Approval>>>,
|
pub waiting_requests: RwLock<HashMap<u64, Sender<Approval>>>,
|
||||||
pub bans: RwLock<std::collections::HashSet<Option<Client>>>,
|
pub current_rehide_status: RwLock<Option<bool>>,
|
||||||
server: RwLock<Server>,
|
pub pending_terminal_request: RwLock<bool>,
|
||||||
|
// setup_errors is never modified and so doesn't need to be wrapped in RwLock
|
||||||
|
pub setup_errors: Vec<String>,
|
||||||
pool: sqlx::SqlitePool,
|
pool: sqlx::SqlitePool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new(config: AppConfig, session: Session, server: Server, pool: SqlitePool) -> AppState {
|
pub fn new(
|
||||||
|
config: AppConfig,
|
||||||
|
session: Session,
|
||||||
|
pool: SqlitePool,
|
||||||
|
setup_errors: Vec<String>,
|
||||||
|
) -> AppState {
|
||||||
AppState {
|
AppState {
|
||||||
config: RwLock::new(config),
|
config: RwLock::new(config),
|
||||||
session: RwLock::new(session),
|
session: RwLock::new(session),
|
||||||
request_count: RwLock::new(0),
|
request_count: RwLock::new(0),
|
||||||
open_requests: RwLock::new(HashMap::new()),
|
waiting_requests: RwLock::new(HashMap::new()),
|
||||||
bans: RwLock::new(HashSet::new()),
|
current_rehide_status: RwLock::new(None),
|
||||||
server: RwLock::new(server),
|
pending_terminal_request: RwLock::new(false),
|
||||||
|
setup_errors,
|
||||||
pool,
|
pool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,14 +60,16 @@ impl AppState {
|
|||||||
pub async fn update_config(&self, new_config: AppConfig) -> Result<(), SetupError> {
|
pub async fn update_config(&self, new_config: AppConfig) -> Result<(), SetupError> {
|
||||||
let mut live_config = self.config.write().await;
|
let mut live_config = self.config.write().await;
|
||||||
|
|
||||||
|
// update autostart if necessary
|
||||||
if new_config.start_on_login != live_config.start_on_login {
|
if new_config.start_on_login != live_config.start_on_login {
|
||||||
config::set_auto_launch(new_config.start_on_login)?;
|
config::set_auto_launch(new_config.start_on_login)?;
|
||||||
}
|
}
|
||||||
if new_config.listen_addr != live_config.listen_addr
|
|
||||||
|| new_config.listen_port != live_config.listen_port
|
// re-register hotkeys if necessary
|
||||||
|
if new_config.hotkeys.show_window != live_config.hotkeys.show_window
|
||||||
|
|| new_config.hotkeys.launch_terminal != live_config.hotkeys.launch_terminal
|
||||||
{
|
{
|
||||||
let mut sv = self.server.write().await;
|
config::register_hotkeys(&new_config.hotkeys)?;
|
||||||
sv.rebind(new_config.listen_addr, new_config.listen_port).await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
new_config.save(&self.pool).await?;
|
new_config.save(&self.pool).await?;
|
||||||
@ -74,26 +77,42 @@ impl AppState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn register_request(&self, chan: Sender<ipc::Approval>) -> u64 {
|
pub async fn register_request(&self, sender: Sender<Approval>) -> u64 {
|
||||||
let count = {
|
let count = {
|
||||||
let mut c = self.request_count.write().await;
|
let mut c = self.request_count.write().await;
|
||||||
*c += 1;
|
*c += 1;
|
||||||
c
|
c
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut open_requests = self.open_requests.write().await;
|
let mut waiting_requests = self.waiting_requests.write().await;
|
||||||
open_requests.insert(*count, chan); // `count` is the request id
|
waiting_requests.insert(*count, sender); // `count` is the request id
|
||||||
*count
|
*count
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unregister_request(&self, id: u64) {
|
pub async fn unregister_request(&self, id: u64) {
|
||||||
let mut open_requests = self.open_requests.write().await;
|
let mut waiting_requests = self.waiting_requests.write().await;
|
||||||
open_requests.remove(&id);
|
waiting_requests.remove(&id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn req_count(&self) -> usize {
|
pub async fn req_count(&self) -> usize {
|
||||||
let open_requests = self.open_requests.read().await;
|
let waiting_requests = self.waiting_requests.read().await;
|
||||||
open_requests.len()
|
waiting_requests.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_or_set_rehide(&self, new_value: bool) -> bool {
|
||||||
|
let mut rehide = self.current_rehide_status.write().await;
|
||||||
|
match *rehide {
|
||||||
|
Some(original) => original,
|
||||||
|
None => {
|
||||||
|
*rehide = Some(new_value);
|
||||||
|
new_value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear_rehide(&self) {
|
||||||
|
let mut rehide = self.current_rehide_status.write().await;
|
||||||
|
*rehide = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
|
pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
|
||||||
@ -102,31 +121,12 @@ impl AppState {
|
|||||||
session.renew_if_expired().await?;
|
session.renew_if_expired().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut open_requests = self.open_requests.write().await;
|
let mut waiting_requests = self.waiting_requests.write().await;
|
||||||
let chan = open_requests
|
waiting_requests
|
||||||
.remove(&response.id)
|
.remove(&response.id)
|
||||||
.ok_or(SendResponseError::NotFound)
|
.ok_or(SendResponseError::NotFound)?
|
||||||
?;
|
.send(response.approval)
|
||||||
|
.map_err(|_| SendResponseError::Abandoned)
|
||||||
chan.send(response.approval)
|
|
||||||
.map_err(|_e| SendResponseError::Abandoned)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn add_ban(&self, client: Option<Client>) {
|
|
||||||
let mut bans = self.bans.write().await;
|
|
||||||
bans.insert(client.clone());
|
|
||||||
|
|
||||||
runtime::spawn(async move {
|
|
||||||
sleep(Duration::from_secs(5)).await;
|
|
||||||
let app = APP.get().unwrap();
|
|
||||||
let state = app.state::<AppState>();
|
|
||||||
let mut bans = state.bans.write().await;
|
|
||||||
bans.remove(&client);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn is_banned(&self, client: &Option<Client>) -> bool {
|
|
||||||
self.bans.read().await.contains(&client)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> {
|
pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> {
|
||||||
@ -141,22 +141,21 @@ impl AppState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serialize_base_creds(&self) -> Result<String, GetCredentialsError> {
|
pub async fn is_unlocked(&self) -> bool {
|
||||||
let session = self.session.read().await;
|
let session = self.session.read().await;
|
||||||
match *session {
|
matches!(*session, Session::Unlocked{..})
|
||||||
Session::Unlocked{ref base, ..} => Ok(serde_json::to_string(base).unwrap()),
|
|
||||||
Session::Locked(_) => Err(GetCredentialsError::Locked),
|
|
||||||
Session::Empty => Err(GetCredentialsError::Empty),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serialize_session_creds(&self) -> Result<String, GetCredentialsError> {
|
pub async fn base_creds_cloned(&self) -> Result<BaseCredentials, GetCredentialsError> {
|
||||||
let session = self.session.read().await;
|
let app_session = self.session.read().await;
|
||||||
match *session {
|
let (base, _session) = app_session.try_get()?;
|
||||||
Session::Unlocked{ref session, ..} => Ok(serde_json::to_string(session).unwrap()),
|
Ok(base.clone())
|
||||||
Session::Locked(_) => Err(GetCredentialsError::Locked),
|
}
|
||||||
Session::Empty => Err(GetCredentialsError::Empty),
|
|
||||||
}
|
pub async fn session_creds_cloned(&self) -> Result<SessionCredentials, GetCredentialsError> {
|
||||||
|
let app_session = self.session.read().await;
|
||||||
|
let (_bsae, session) = app_session.try_get()?;
|
||||||
|
Ok(session.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn new_session(&self, base: BaseCredentials) -> Result<(), GetSessionError> {
|
async fn new_session(&self, base: BaseCredentials) -> Result<(), GetSessionError> {
|
||||||
@ -165,4 +164,21 @@ impl AppState {
|
|||||||
*app_session = Session::Unlocked {base, session};
|
*app_session = Session::Unlocked {base, session};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn register_terminal_request(&self) -> Result<(), ()> {
|
||||||
|
let mut req = self.pending_terminal_request.write().await;
|
||||||
|
if *req {
|
||||||
|
// if a request is already pending, we can't register a new one
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
*req = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn unregister_terminal_request(&self) {
|
||||||
|
let mut req = self.pending_terminal_request.write().await;
|
||||||
|
*req = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
82
src-tauri/src/terminal.rs
Normal file
82
src-tauri/src/terminal.rs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
use crate::app::APP;
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn launch(use_base: bool) -> Result<(), LaunchTerminalError> {
|
||||||
|
let app = APP.get().unwrap();
|
||||||
|
let state = app.state::<AppState>();
|
||||||
|
|
||||||
|
// register_terminal_request() returns Err if there is another request pending
|
||||||
|
if state.register_terminal_request().await.is_err() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cmd = {
|
||||||
|
let config = state.config.read().await;
|
||||||
|
let mut cmd = Command::new(&config.terminal.exec);
|
||||||
|
cmd.args(&config.terminal.args);
|
||||||
|
cmd
|
||||||
|
};
|
||||||
|
|
||||||
|
// if session is unlocked or empty, wait for credentials from frontend
|
||||||
|
if !state.is_unlocked().await {
|
||||||
|
app.emit_all("launch-terminal-request", ())?;
|
||||||
|
let window = app.get_window("main")
|
||||||
|
.ok_or(LaunchTerminalError::NoMainWindow)?;
|
||||||
|
if !window.is_visible()? {
|
||||||
|
window.unminimize()?;
|
||||||
|
window.show()?;
|
||||||
|
}
|
||||||
|
window.set_focus()?;
|
||||||
|
|
||||||
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
|
app.once_global("credentials-event", move |e| {
|
||||||
|
let success = match e.payload() {
|
||||||
|
Some("\"unlocked\"") | Some("\"entered\"") => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
let _ = tx.send(success);
|
||||||
|
});
|
||||||
|
|
||||||
|
if !rx.await.unwrap_or(false) {
|
||||||
|
state.unregister_terminal_request().await;
|
||||||
|
return Ok(()); // request was canceled by user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// more lock-management
|
||||||
|
{
|
||||||
|
let app_session = state.session.read().await;
|
||||||
|
// session should really be unlocked at this point, but if the frontend misbehaves
|
||||||
|
// (i.e. lies about unlocking) we could end up here with a locked session
|
||||||
|
// this will result in an error popup to the user (see main hotkey handler)
|
||||||
|
let (base_creds, session_creds) = app_session.try_get()?;
|
||||||
|
if use_base {
|
||||||
|
cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id);
|
||||||
|
cmd.env("AWS_SECRET_ACCESS_KEY", &base_creds.secret_access_key);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cmd.env("AWS_ACCESS_KEY_ID", &session_creds.access_key_id);
|
||||||
|
cmd.env("AWS_SECRET_ACCESS_KEY", &session_creds.secret_access_key);
|
||||||
|
cmd.env("AWS_SESSION_TOKEN", &session_creds.session_token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = match cmd.spawn() {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) if std::io::ErrorKind::NotFound == e.kind() => {
|
||||||
|
Err(ExecError::NotFound(cmd.get_program().to_owned()))
|
||||||
|
},
|
||||||
|
Err(e) => Err(ExecError::ExecutionFailed(e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
state.unregister_terminal_request().await;
|
||||||
|
|
||||||
|
res?; // ? auto-conversion is more liberal than .into()
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -8,11 +8,12 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "creddy",
|
"productName": "creddy",
|
||||||
"version": "0.2.3"
|
"version": "0.3.3"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
"os": {"all": true}
|
"os": {"all": true},
|
||||||
|
"dialog": {"open": true}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
|
@ -16,6 +16,25 @@ listen('credentials-request', (tauriEvent) => {
|
|||||||
$appState.pendingRequests.put(tauriEvent.payload);
|
$appState.pendingRequests.put(tauriEvent.payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
listen('launch-terminal-request', async (tauriEvent) => {
|
||||||
|
if ($appState.currentRequest === null) {
|
||||||
|
let status = await invoke('get_session_status');
|
||||||
|
if (status === 'locked') {
|
||||||
|
navigate('Unlock');
|
||||||
|
}
|
||||||
|
else if (status === 'empty') {
|
||||||
|
navigate('EnterCredentials');
|
||||||
|
}
|
||||||
|
// else, session is unlocked, so do nothing
|
||||||
|
// (although we shouldn't even get the event in that case)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
invoke('get_setup_errors')
|
||||||
|
.then(errs => {
|
||||||
|
$appState.setupErrors = errs.map(e => ({msg: e, show: true}));
|
||||||
|
});
|
||||||
|
|
||||||
acceptRequest();
|
acceptRequest();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -9,6 +9,10 @@ export default function() {
|
|||||||
|
|
||||||
resolvers: [],
|
resolvers: [],
|
||||||
|
|
||||||
|
size() {
|
||||||
|
return this.items.length;
|
||||||
|
},
|
||||||
|
|
||||||
put(item) {
|
put(item) {
|
||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
let resolver = this.resolvers.shift();
|
let resolver = this.resolvers.shift();
|
||||||
|
@ -8,6 +8,7 @@ export let appState = writable({
|
|||||||
currentRequest: null,
|
currentRequest: null,
|
||||||
pendingRequests: queue(),
|
pendingRequests: queue(),
|
||||||
credentialStatus: 'locked',
|
credentialStatus: 'locked',
|
||||||
|
setupErrors: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
13
src/ui/KeyCombo.svelte
Normal file
13
src/ui/KeyCombo.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
export let keys;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex gap-x-[0.2em] items-center">
|
||||||
|
{#each keys as key, i}
|
||||||
|
{#if i > 0}
|
||||||
|
<span class="mt-[-0.1em]">+</span>
|
||||||
|
{/if}
|
||||||
|
<kbd class="normal-case px-1 py-0.5 rounded border border-neutral">{key}</kbd>
|
||||||
|
{/each}
|
||||||
|
</div>
|
@ -1,113 +1,42 @@
|
|||||||
<script>
|
<script>
|
||||||
export let color = 'base-content';
|
export let thickness = 8;
|
||||||
export let thickness = '2px';
|
|
||||||
let classes = '';
|
let classes = '';
|
||||||
export { classes as class };
|
export { classes as class };
|
||||||
|
|
||||||
const colorVars = {
|
const radius = (100 - thickness) / 2;
|
||||||
'primary': 'p',
|
// the px are fake, but we need them to satisfy css calc()
|
||||||
'primary-focus': 'pf',
|
const circumference = `${2 * Math.PI * radius}px`;
|
||||||
'primary-content': 'pc',
|
|
||||||
'secondary': 's',
|
|
||||||
'secondary-focus': 'sf',
|
|
||||||
'secondary-content': 'sc',
|
|
||||||
'accent': 'a',
|
|
||||||
'accent-focus': 'af',
|
|
||||||
'accent-content': 'ac',
|
|
||||||
'neutral': 'n',
|
|
||||||
'neutral-focus': 'nf',
|
|
||||||
'neutral-content': 'nc',
|
|
||||||
'base-100': 'b1',
|
|
||||||
'base-200': 'b2',
|
|
||||||
'base-300': 'b3',
|
|
||||||
'base-content': 'bc',
|
|
||||||
'info': 'in',
|
|
||||||
'info-content': 'inc',
|
|
||||||
'success': 'su',
|
|
||||||
'success-content': 'suc',
|
|
||||||
'warning': 'wa',
|
|
||||||
'warning-content': 'wac',
|
|
||||||
'error': 'er',
|
|
||||||
'error-content': 'erc',
|
|
||||||
}
|
|
||||||
|
|
||||||
let arcStyle = `border-width: ${thickness};`;
|
|
||||||
arcStyle += `border-color: hsl(var(--${colorVars[color]})) transparent transparent transparent;`;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
#spinner {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
animation: spin;
|
<svg
|
||||||
animation-duration: 1.5s;
|
style:--circumference={circumference}
|
||||||
animation-iteration-count: infinite;
|
class={classes}
|
||||||
animation-timing-function: linear;
|
viewBox="0 0 100 100"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<circle cx="50" cy="50" r={radius} stroke-width={thickness} />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
circle {
|
||||||
|
fill: transparent;
|
||||||
|
stroke-dasharray: var(--circumference);
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transform-origin: center;
|
||||||
|
animation: chase 3s infinite,
|
||||||
|
spin 1.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes chase {
|
||||||
|
0% { stroke-dashoffset: calc(-1 * var(--circumference)); }
|
||||||
|
50% { stroke-dashoffset: calc(-2 * var(--circumference)); }
|
||||||
|
100% { stroke-dashoffset: calc(-3 * var(--circumference)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
50% { transform: rotate(225deg); }
|
50% { transform: rotate(135deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(270deg); }
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.arc {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
border-radius: 9999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arc-top {
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.arc-right {
|
|
||||||
animation: spin-right;
|
|
||||||
animation-duration: 3s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arc-bottom {
|
|
||||||
animation: spin-bottom;
|
|
||||||
animation-duration: 3s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arc-left {
|
|
||||||
animation: spin-left;
|
|
||||||
animation-duration: 3s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin-top {
|
|
||||||
0% { transform: rotate(-45deg); }
|
|
||||||
50% { transform: rotate(315deg); }
|
|
||||||
100% { transform: rotate(-45deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin-right {
|
|
||||||
0% { transform: rotate(45deg); }
|
|
||||||
50% { transform: rotate(315deg); }
|
|
||||||
100% { transform: rotate(405deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin-bottom {
|
|
||||||
0% { transform: rotate(135deg); }
|
|
||||||
50% { transform: rotate(315deg); }
|
|
||||||
100% { transform: rotate(495deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin-left {
|
|
||||||
0% { transform: rotate(225deg); }
|
|
||||||
50% { transform: rotate(315deg); }
|
|
||||||
100% { transform: rotate(585deg); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
<div id="spinner" class="w-6 h-6 {classes}">
|
|
||||||
<div class="arc arc-top w-full h-full" style={arcStyle}></div>
|
|
||||||
<div class="arc arc-right w-full h-full" style={arcStyle}></div>
|
|
||||||
<div class="arc arc-bottom w-full h-full" style={arcStyle}></div>
|
|
||||||
<div class="arc arc-left w-full h-full" style={arcStyle}></div>
|
|
||||||
</div>
|
|
27
src/ui/settings/FileSetting.svelte
Normal file
27
src/ui/settings/FileSetting.svelte
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { open } from '@tauri-apps/api/dialog';
|
||||||
|
import Setting from './Setting.svelte';
|
||||||
|
|
||||||
|
export let title;
|
||||||
|
export let value;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<Setting {title}>
|
||||||
|
<div slot="input">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-sm input-bordered grow text-right"
|
||||||
|
bind:value
|
||||||
|
on:change={() => dispatch('update', {value})}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
on:click={async () => value = await open()}
|
||||||
|
>Browse</button>
|
||||||
|
</div>
|
||||||
|
<slot name="description" slot="description"></slot>
|
||||||
|
</Setting>
|
72
src/ui/settings/Keybind.svelte
Normal file
72
src/ui/settings/Keybind.svelte
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import KeyCombo from '../KeyCombo.svelte';
|
||||||
|
|
||||||
|
export let description;
|
||||||
|
export let value;
|
||||||
|
|
||||||
|
const id = Math.random().toString().slice(2);
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
const MODIFIERS = new Set(['Alt', 'AltGraph', 'Control', 'Fn', 'FnLock', 'Meta', 'Shift', 'Super', ]);
|
||||||
|
|
||||||
|
|
||||||
|
let listening = false;
|
||||||
|
let keysPressed = [];
|
||||||
|
|
||||||
|
function addModifiers(event) {
|
||||||
|
// add modifier key if it isn't already present
|
||||||
|
if (MODIFIERS.has(event.key) && keysPressed.indexOf(event.key) === -1) {
|
||||||
|
keysPressed.push(event.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMainKey(event) {
|
||||||
|
if (!MODIFIERS.has(event.key)) {
|
||||||
|
keysPressed.push(event.key);
|
||||||
|
|
||||||
|
value.keys = keysPressed.join('+');
|
||||||
|
dispatch('update', {value});
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
unlisten();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listen() {
|
||||||
|
// don't re-listen if we already are
|
||||||
|
if (listening) return;
|
||||||
|
|
||||||
|
listening = true;
|
||||||
|
window.addEventListener('keydown', addModifiers);
|
||||||
|
window.addEventListener('keyup', addMainKey);
|
||||||
|
// setTimeout avoids reacting to the click event that we are currently processing
|
||||||
|
setTimeout(() => window.addEventListener('click', unlisten), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlisten() {
|
||||||
|
listening = false;
|
||||||
|
keysPressed = [];
|
||||||
|
window.removeEventListener('keydown', addModifiers);
|
||||||
|
window.removeEventListener('keyup', addMainKey);
|
||||||
|
window.removeEventListener('click', unlisten);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<input
|
||||||
|
{id}
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-primary"
|
||||||
|
bind:checked={value.enabled}
|
||||||
|
on:change={() => dispatch('update', {value})}
|
||||||
|
>
|
||||||
|
<label for={id} class="cursor-pointer ml-4 text-lg">{description}</label>
|
||||||
|
|
||||||
|
<button class="h-12 p-2 rounded border border-neutral cursor-pointer text-center" on:click={listen}>
|
||||||
|
{#if listening}
|
||||||
|
Click to cancel
|
||||||
|
{:else}
|
||||||
|
<KeyCombo keys={value.keys.split('+')} />
|
||||||
|
{/if}
|
||||||
|
</button>
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
export let title;
|
export let title;
|
||||||
export let value;
|
export let value;
|
||||||
|
|
||||||
export let unit = '';
|
export let unit = '';
|
||||||
export let min = null;
|
export let min = null;
|
||||||
export let max = null;
|
export let max = null;
|
||||||
|
@ -6,14 +6,17 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div>
|
||||||
<div class="flex justify-between">
|
<div class="flex flex-wrap justify-between gap-y-4">
|
||||||
<h3 class="text-lg font-bold">{title}</h3>
|
<h3 class="text-lg font-bold shrink-0">{title}</h3>
|
||||||
<slot name="input"></slot>
|
{#if $$slots.input}
|
||||||
</div>
|
<slot name="input"></slot>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if $$slots.description}
|
{#if $$slots.description}
|
||||||
<p class="mt-3">
|
<p class="mt-3">
|
||||||
<slot name="description"></slot>
|
<slot name="description"></slot>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
14
src/ui/settings/SettingsGroup.svelte
Normal file
14
src/ui/settings/SettingsGroup.svelte
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script>
|
||||||
|
export let name;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="divider mt-0 mb-8">
|
||||||
|
<h2 class="text-xl font-bold">{name}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-12">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
22
src/ui/settings/TextSetting.svelte
Normal file
22
src/ui/settings/TextSetting.svelte
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import Setting from './Setting.svelte';
|
||||||
|
|
||||||
|
export let title;
|
||||||
|
export let value;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<Setting {title}>
|
||||||
|
<div slot="input">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-sm input-bordered grow text-right"
|
||||||
|
bind:value
|
||||||
|
on:change={() => dispatch('update', {value})}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<slot name="description" slot="description"></slot>
|
||||||
|
</Setting>
|
@ -1,3 +1,5 @@
|
|||||||
export { default as Setting } from './Setting.svelte';
|
export { default as Setting } from './Setting.svelte';
|
||||||
export { default as ToggleSetting } from './ToggleSetting.svelte';
|
export { default as ToggleSetting } from './ToggleSetting.svelte';
|
||||||
export { default as NumericSetting } from './NumericSetting.svelte';
|
export { default as NumericSetting } from './NumericSetting.svelte';
|
||||||
|
export { default as FileSetting } from './FileSetting.svelte';
|
||||||
|
export { default as TextSetting } from './TextSetting.svelte';
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import { appState, completeRequest } from '../lib/state.js';
|
import { appState, completeRequest } from '../lib/state.js';
|
||||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
||||||
import Link from '../ui/Link.svelte';
|
import Link from '../ui/Link.svelte';
|
||||||
|
import KeyCombo from '../ui/KeyCombo.svelte';
|
||||||
|
|
||||||
|
|
||||||
// Send response to backend, display error if applicable
|
// Send response to backend, display error if applicable
|
||||||
@ -46,16 +47,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract executable name from full path
|
// Extract executable name from full path
|
||||||
let appName = null;
|
const client = $appState.currentRequest.client;
|
||||||
if ($appState.currentRequest.clients.length === 1) {
|
const m = client.exe?.match(/\/([^/]+?$)|\\([^\\]+?$)/);
|
||||||
let path = $appState.currentRequest.clients[0].exe;
|
const appName = m[1] || m[2];
|
||||||
let m = path.match(/\/([^/]+?$)|\\([^\\]+?$)/);
|
|
||||||
appName = m[1] || m[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executable paths can be long, so ensure they only break on \ or /
|
// Executable paths can be long, so ensure they only break on \ or /
|
||||||
function breakPath(client) {
|
function breakPath(path) {
|
||||||
return client.exe.replace(/(\\|\/)/g, '$1<wbr>');
|
return path.replace(/(\\|\/)/g, '$1<wbr>');
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the request has already been approved/denied, send response immediately
|
// if the request has already been approved/denied, send response immediately
|
||||||
@ -96,29 +94,25 @@
|
|||||||
<h2 class="text-xl font-bold">{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials.</h2>
|
<h2 class="text-xl font-bold">{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials.</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-[auto_1fr] gap-x-3">
|
<div class="grid grid-cols-[auto_1fr] gap-x-3">
|
||||||
{#each $appState.currentRequest.clients as client}
|
<div class="text-right">Path:</div>
|
||||||
<div class="text-right">Path:</div>
|
<code class="">{@html client.exe ? breakPath(client.exe) : 'Unknown'}</code>
|
||||||
<code class="">{@html client ? breakPath(client) : 'Unknown'}</code>
|
<div class="text-right">PID:</div>
|
||||||
<div class="text-right">PID:</div>
|
<code>{client.pid}</code>
|
||||||
<code>{client ? client.pid : 'Unknown'}</code>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between">
|
<div class="w-full flex justify-between">
|
||||||
<Link target={deny} hotkey="Escape">
|
<Link target={deny} hotkey="Escape">
|
||||||
<button class="btn btn-error justify-self-start">
|
<button class="btn btn-error justify-self-start">
|
||||||
Deny
|
<span class="mr-2">Deny</span>
|
||||||
<kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Esc</kbd>
|
<KeyCombo keys={['Esc']} />
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link target={approve} hotkey="Enter" shift="{true}">
|
<Link target={approve} hotkey="Enter" shift="{true}">
|
||||||
<button class="btn btn-success justify-self-end">
|
<button class="btn btn-success justify-self-end">
|
||||||
Approve
|
<span class="mr-2">Approve</span>
|
||||||
<kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Shift</kbd>
|
<KeyCombo keys={['Shift', 'Enter']} />
|
||||||
<span class="mx-0.5">+</span>
|
|
||||||
<kbd class="normal-case px-1 py-0.5 rounded border border-neutral">Enter</kbd>
|
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
try {
|
try {
|
||||||
saving = true;
|
saving = true;
|
||||||
await invoke('save_credentials', {credentials, passphrase});
|
await invoke('save_credentials', {credentials, passphrase});
|
||||||
|
emit('credentials-event', 'entered');
|
||||||
if ($appState.currentRequest) {
|
if ($appState.currentRequest) {
|
||||||
navigate('Approve');
|
navigate('Approve');
|
||||||
}
|
}
|
||||||
@ -39,14 +40,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
if (e.code === "GetSession") {
|
window.error = e;
|
||||||
let root = getRootCause(e);
|
const root = getRootCause(e);
|
||||||
|
if (e.code === 'GetSession' && root.code) {
|
||||||
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
|
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
errorMsg = e.msg;
|
errorMsg = e.msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the alert already existed, shake it
|
||||||
if (alert) {
|
if (alert) {
|
||||||
alert.shake();
|
alert.shake();
|
||||||
}
|
}
|
||||||
@ -54,6 +57,11 @@
|
|||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
emit('credentials-event', 'enter-canceled');
|
||||||
|
navigate('Home');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@ -71,13 +79,13 @@
|
|||||||
<input type="password" placeholder="Re-enter passphrase" bind:value={confirmPassphrase} class="input input-bordered" on:change={confirm} />
|
<input type="password" placeholder="Re-enter passphrase" bind:value={confirmPassphrase} class="input input-bordered" on:change={confirm} />
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
{#if saving}
|
{#if saving }
|
||||||
<Spinner class="w-5 h-5" color="primary-content" thickness="2px"/>
|
<Spinner class="w-5 h-5" thickness="12"/>
|
||||||
{:else}
|
{:else}
|
||||||
Submit
|
Submit
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
<Link target="Home" hotkey="Escape">
|
<Link target={cancel} hotkey="Escape">
|
||||||
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
||||||
</Link>
|
</Link>
|
||||||
</form>
|
</form>
|
||||||
|
@ -10,13 +10,11 @@
|
|||||||
|
|
||||||
import vaultDoorSvg from '../assets/vault_door.svg?raw';
|
import vaultDoorSvg from '../assets/vault_door.svg?raw';
|
||||||
|
|
||||||
|
let launchBase = false;
|
||||||
// onMount(async () => {
|
function launchTerminal() {
|
||||||
// // will block until a request comes in
|
invoke('launch_terminal', {base: launchBase});
|
||||||
// let req = await $appState.pendingRequests.get();
|
launchBase = false;
|
||||||
// $appState.currentRequest = req;
|
}
|
||||||
// navigate('Approve');
|
|
||||||
// });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@ -25,25 +23,45 @@
|
|||||||
</Nav>
|
</Nav>
|
||||||
|
|
||||||
<div class="flex flex-col h-screen items-center justify-center p-4 space-y-4">
|
<div class="flex flex-col h-screen items-center justify-center p-4 space-y-4">
|
||||||
{#await invoke('get_session_status') then status}
|
<div class="flex flex-col items-center space-y-4">
|
||||||
{#if status === 'locked'}
|
{@html vaultDoorSvg}
|
||||||
|
{#await invoke('get_session_status') then status}
|
||||||
|
{#if status === 'locked'}
|
||||||
|
|
||||||
{@html vaultDoorSvg}
|
<h2 class="text-2xl font-bold">Creddy is locked</h2>
|
||||||
<h2 class="text-2xl font-bold">Creddy is locked</h2>
|
<Link target="Unlock" hotkey="Enter" class="w-64">
|
||||||
<Link target="Unlock" hotkey="Enter" class="w-64">
|
<button class="btn btn-primary w-full">Unlock</button>
|
||||||
<button class="btn btn-primary w-full">Unlock</button>
|
</Link>
|
||||||
</Link>
|
|
||||||
|
|
||||||
{:else if status === 'unlocked'}
|
{:else if status === 'unlocked'}
|
||||||
{@html vaultDoorSvg}
|
<h2 class="text-2xl font-bold">Waiting for requests</h2>
|
||||||
<h2 class="text-2xl font-bold">Waiting for requests</h2>
|
<button class="btn btn-primary w-full" on:click={launchTerminal}>
|
||||||
|
Launch Terminal
|
||||||
|
</button>
|
||||||
|
<label class="label cursor-pointer flex items-center space-x-2">
|
||||||
|
<input type="checkbox" class="checkbox checkbox-sm" bind:checked={launchBase}>
|
||||||
|
<span class="label-text">Launch with base credentials</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
{:else if status === 'empty'}
|
{:else if status === 'empty'}
|
||||||
{@html vaultDoorSvg}
|
<h2 class="text-2xl font-bold">No credentials found</h2>
|
||||||
<h2 class="text-2xl font-bold">No credentials found</h2>
|
<Link target="EnterCredentials" hotkey="Enter" class="w-64">
|
||||||
<Link target="EnterCredentials" hotkey="Enter" class="w-64">
|
<button class="btn btn-primary w-full">Enter Credentials</button>
|
||||||
<button class="btn btn-primary w-full">Enter Credentials</button>
|
</Link>
|
||||||
</Link>
|
{/if}
|
||||||
{/if}
|
{/await}
|
||||||
{/await}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if $appState.setupErrors.some(e => e.show)}
|
||||||
|
<div class="toast">
|
||||||
|
{#each $appState.setupErrors as error}
|
||||||
|
{#if error.show}
|
||||||
|
<div class="alert alert-error shadow-lg">
|
||||||
|
{error.msg}
|
||||||
|
<button class="btn btn-sm btn-alert-error" on:click={() => error.show = false}>Ok</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
@ -6,7 +6,9 @@
|
|||||||
import Nav from '../ui/Nav.svelte';
|
import Nav from '../ui/Nav.svelte';
|
||||||
import Link from '../ui/Link.svelte';
|
import Link from '../ui/Link.svelte';
|
||||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
||||||
import { Setting, ToggleSetting, NumericSetting } from '../ui/settings';
|
import SettingsGroup from '../ui/settings/SettingsGroup.svelte';
|
||||||
|
import Keybind from '../ui/settings/Keybind.svelte';
|
||||||
|
import { Setting, ToggleSetting, NumericSetting, FileSetting, TextSetting } from '../ui/settings';
|
||||||
|
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import { backInOut } from 'svelte/easing';
|
import { backInOut } from 'svelte/easing';
|
||||||
@ -14,6 +16,7 @@
|
|||||||
|
|
||||||
let error = null;
|
let error = null;
|
||||||
async function save() {
|
async function save() {
|
||||||
|
console.log('updating config');
|
||||||
try {
|
try {
|
||||||
await invoke('save_config', {config: $appState.config});
|
await invoke('save_config', {config: $appState.config});
|
||||||
}
|
}
|
||||||
@ -23,59 +26,69 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let osType = '';
|
let osType = null;
|
||||||
type().then(t => osType = t);
|
type().then(t => osType = t);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<Nav>
|
<Nav>
|
||||||
<h2 slot="title" class="text-2xl font-bold">Settings</h2>
|
<h1 slot="title" class="text-2xl font-bold">Settings</h1>
|
||||||
</Nav>
|
</Nav>
|
||||||
|
|
||||||
{#await invoke('get_config') then config}
|
{#await invoke('get_config') then config}
|
||||||
<div class="max-w-md mx-auto mt-1.5 p-4">
|
<div class="max-w-lg mx-auto mt-1.5 p-4 space-y-16">
|
||||||
<!-- <h2 class="text-2xl font-bold text-center">Settings</h2> -->
|
<SettingsGroup name="General">
|
||||||
|
<ToggleSetting title="Start on login" bind:value={$appState.config.start_on_login} on:update={save}>
|
||||||
|
<svelte:fragment slot="description">
|
||||||
|
Start Creddy when you log in to your computer.
|
||||||
|
</svelte:fragment>
|
||||||
|
</ToggleSetting>
|
||||||
|
|
||||||
<ToggleSetting title="Start on login" bind:value={$appState.config.start_on_login} on:update={save}>
|
<ToggleSetting title="Start minimized" bind:value={$appState.config.start_minimized} on:update={save}>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Start Creddy when you log in to your computer.
|
Minimize to the system tray at startup.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ToggleSetting>
|
</ToggleSetting>
|
||||||
|
|
||||||
<ToggleSetting title="Start minimized" bind:value={$appState.config.start_minimized} on:update={save}>
|
<NumericSetting title="Re-hide delay" bind:value={$appState.config.rehide_ms} min={0} unit="Milliseconds" on:update={save}>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Minimize to the system tray at startup.
|
How long to wait after a request is approved/denied before minimizing
|
||||||
</svelte:fragment>
|
the window to tray. Only applicable if the window was minimized
|
||||||
</ToggleSetting>
|
to tray before the request was received.
|
||||||
|
</svelte:fragment>
|
||||||
|
</NumericSetting>
|
||||||
|
|
||||||
<NumericSetting title="Re-hide delay" bind:value={$appState.config.rehide_ms} min={0} unit="Milliseconds" on:update={save}>
|
<Setting title="Update credentials">
|
||||||
<svelte:fragment slot="description">
|
<Link slot="input" target="EnterCredentials">
|
||||||
How long to wait after a request is approved/denied before minimizing
|
<button class="btn btn-sm btn-primary">Update</button>
|
||||||
the window to tray. Only applicable if the window was minimized
|
</Link>
|
||||||
to tray before the request was received.
|
<svelte:fragment slot="description">
|
||||||
</svelte:fragment>
|
Update or re-enter your encrypted credentials.
|
||||||
</NumericSetting>
|
</svelte:fragment>
|
||||||
|
</Setting>
|
||||||
|
|
||||||
<NumericSetting
|
<FileSetting
|
||||||
title="Listen port"
|
title="Terminal emulator"
|
||||||
bind:value={$appState.config.listen_port}
|
bind:value={$appState.config.terminal.exec}
|
||||||
min={osType === 'Windows_NT' ? 1 : 0}
|
on:update={save}
|
||||||
on:update={save}
|
>
|
||||||
>
|
<svelte:fragment slot="description">
|
||||||
<svelte:fragment slot="description">
|
Choose your preferred terminal emulator (e.g. <code>gnome-terminal</code> or <code>wt.exe</code>.) May be an absolute path or an executable discoverable on <code>$PATH</code>.
|
||||||
Listen for credentials requests on this port.
|
</svelte:fragment>
|
||||||
(Should be used with <code>$AWS_CONTAINER_CREDENTIALS_FULL_URI</code>)
|
</FileSetting>
|
||||||
</svelte:fragment>
|
</SettingsGroup>
|
||||||
</NumericSetting>
|
|
||||||
|
<SettingsGroup name="Hotkeys">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p>Click on a keybinding to modify it. Use the checkbox to enable or disable a keybinding entirely.</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[auto_1fr_auto] gap-y-3 items-center">
|
||||||
|
<Keybind description="Show Creddy" value={$appState.config.hotkeys.show_window} on:update={save} />
|
||||||
|
<Keybind description="Launch terminal" value={$appState.config.hotkeys.launch_terminal} on:update={save} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsGroup>
|
||||||
|
|
||||||
<Setting title="Update credentials">
|
|
||||||
<Link slot="input" target="EnterCredentials">
|
|
||||||
<button class="btn btn-sm btn-primary">Update</button>
|
|
||||||
</Link>
|
|
||||||
<svelte:fragment slot="description">
|
|
||||||
Update or re-enter your encrypted credentials.
|
|
||||||
</svelte:fragment>
|
|
||||||
</Setting>
|
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
import { emit } from '@tauri-apps/api/event';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import { appState } from '../lib/state.js';
|
import { appState } from '../lib/state.js';
|
||||||
@ -26,6 +27,7 @@
|
|||||||
saving = true;
|
saving = true;
|
||||||
let r = await invoke('unlock', {passphrase});
|
let r = await invoke('unlock', {passphrase});
|
||||||
$appState.credentialStatus = 'unlocked';
|
$appState.credentialStatus = 'unlocked';
|
||||||
|
emit('credentials-event', 'unlocked');
|
||||||
if ($appState.currentRequest) {
|
if ($appState.currentRequest) {
|
||||||
navigate('Approve');
|
navigate('Approve');
|
||||||
}
|
}
|
||||||
@ -34,8 +36,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
if (e.code === 'GetSession') {
|
const root = getRootCause(e);
|
||||||
let root = getRootCause(e);
|
if (e.code === 'GetSession' && root.code) {
|
||||||
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
|
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -51,6 +53,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
emit('credentials-event', 'unlock-canceled');
|
||||||
|
navigate('Home');
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadTime = Date.now();
|
loadTime = Date.now();
|
||||||
})
|
})
|
||||||
@ -69,13 +76,13 @@
|
|||||||
|
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
{#if saving}
|
{#if saving}
|
||||||
<Spinner class="w-5 h-5" color="primary-content" thickness="2px"/>
|
<Spinner class="w-5 h-5" thickness="12"/>
|
||||||
{:else}
|
{:else}
|
||||||
Submit
|
Submit
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Link target="Home" hotkey="Escape">
|
<Link target={cancel} hotkey="Escape">
|
||||||
<button class="btn btn-outline btn-sm w-full">Cancel</button>
|
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
||||||
</Link>
|
</Link>
|
||||||
</form>
|
</form>
|
||||||
|
Reference in New Issue
Block a user