SSH key management via Web UI
Diff
Cargo.lock | 1 +
chartered-db/Cargo.toml | 1 +
chartered-frontend/package-lock.json | 359 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
chartered-frontend/package.json | 1 +
chartered-db/src/lib.rs | 2 ++
chartered-db/src/users.rs | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
chartered-frontend/src/index.tsx | 4 ++++
chartered-web/src/main.rs | 20 +++++++++++++-------
chartered-frontend/src/sections/Nav.tsx | 18 +-----------------
chartered-frontend/src/pages/ssh-keys/AddSshKeys.tsx | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
chartered-web/src/endpoints/web_api/mod.rs | 4 ++++
chartered-web/src/endpoints/web_api/ssh_key.rs | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
13 files changed, 770 insertions(+), 30 deletions(-)
@@ -198,6 +198,7 @@
"serde",
"serde_json",
"thiserror",
"thrussh-keys",
"tokio",
]
@@ -20,3 +20,4 @@
thiserror = "1"
tokio = "1"
dotenv = "0.15"
thrussh-keys = "0.21"
@@ -11,6 +11,7 @@
"dependencies": {
"bootstrap": "^5.1.1",
"react": "^17.0.2",
"react-bootstrap": "^2.0.0-beta.6",
"react-bootstrap-icons": "^1.5.0",
"react-dom": "^17.0.2",
"react-markdown": "^7.0.1",
@@ -1778,12 +1779,73 @@
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.1.tgz",
"integrity": "sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.1.0.tgz",
"integrity": "sha512-RxqQKmE8sO7TGdrcSlHTcVzMP450hqowtBSd2bBS9oPlcokVkaGq28c3Rwa8ty5ctw4EBCjXqjP7xdcKMGDzug==",
"dependencies": {
"@babel/runtime": "^7.6.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1"
}
},
"node_modules/@restart/context": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz",
"integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==",
"peerDependencies": {
"react": ">=16.3.2"
}
},
"node_modules/@restart/hooks": {
"version": "0.3.27",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz",
"integrity": "sha512-s984xV/EapUIfkjlf8wz9weP2O9TNKR96C68FfMEy2bE69+H4cNv3RD4Mf97lW7Htt7PjZrYTjSC8f3SB9VCXw==",
"dependencies": {
"dequal": "^2.0.2"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@restart/ui": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@restart/ui/-/ui-0.2.2.tgz",
"integrity": "sha512-PgNkiyOaWwx8ttQ45KNABXU3780fB/UxNFxcsCpC4RRAlaByZHHbNLOKfhuFs+ZUU0uLxEH9wYQEhDAZc6ajkA==",
"dependencies": {
"@babel/runtime": "^7.13.16",
"@popperjs/core": "^2.9.2",
"@react-aria/ssr": "^3.0.1",
"@restart/hooks": "^0.4.0",
"@types/warning": "^3.0.0",
"dequal": "^2.0.2",
"dom-helpers": "^5.2.0",
"prop-types": "^15.7.2",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
},
"peerDependencies": {
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
}
},
"node_modules/@restart/ui/node_modules/@restart/hooks": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.0.tgz",
"integrity": "sha512-+RenTVobiCHPjUTbhQDV8m0PU1xEWqgloMIIOlf86oKnfghKR/l4tKto7TH543shEQZZa7ARSMTvT0cXN9u8+g==",
"dependencies": {
"dequal": "^2.0.2"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@types/debug": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
@@ -1800,6 +1862,11 @@
"@types/unist": "*"
}
},
"node_modules/@types/invariant": {
"version": "2.2.35",
"resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz",
"integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg=="
},
"node_modules/@types/mdast": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
@@ -1844,6 +1911,14 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz",
"integrity": "sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz",
"integrity": "sha512-KibDWL6nshuOJ0fu8ll7QnV/LVTo3PzQ9aCPnRUYPfX7eZohHwLIdNHj7pftanREzHNP4/nJa8oeM73uSiavMQ==",
"dependencies": {
"@types/react": "*"
}
@@ -1857,6 +1932,11 @@
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
},
"node_modules/@types/warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
"integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
},
"node_modules/@zeit/schemas": {
"version": "2.6.0",
@@ -2946,6 +3026,11 @@
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"node_modules/cli-boxes": {
"version": "1.0.0",
@@ -3839,6 +3924,14 @@
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/dequal": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
"integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==",
"engines": {
"node": ">=6"
}
},
"node_modules/des.js": {
@@ -3873,6 +3966,15 @@
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"dev": true
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dom-serializer": {
"version": "0.2.2",
@@ -5410,6 +5512,14 @@
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/is-absolute-url": {
@@ -8490,6 +8600,18 @@
"react-is": "^16.8.1"
}
},
"node_modules/prop-types-extra": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
"integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
"dependencies": {
"react-is": "^16.3.2",
"warning": "^4.0.0"
},
"peerDependencies": {
"react": ">=0.14.0"
}
},
"node_modules/property-information": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.0.1.tgz",
@@ -8706,6 +8828,34 @@
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-bootstrap": {
"version": "2.0.0-beta.6",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.0.0-beta.6.tgz",
"integrity": "sha512-eHEkmESWYYgNLjqbz31G696Eytu+6GeF8CPHQ8t9Se12dUEej8OjBakyQP0OGms9yy1ZZeLG/Fvuo7VxiwMcuQ==",
"dependencies": {
"@babel/runtime": "^7.14.0",
"@restart/context": "^2.1.4",
"@restart/hooks": "^0.3.26",
"@restart/ui": "^0.2.1",
"@types/invariant": "^2.2.33",
"@types/prop-types": "^15.7.3",
"@types/react": ">=16.14.8",
"@types/react-transition-group": "^4.4.1",
"@types/warning": "^3.0.0",
"classnames": "^2.3.1",
"dom-helpers": "^5.2.1",
"invariant": "^2.2.4",
"prop-types": "^15.7.2",
"prop-types-extra": "^1.1.0",
"react-transition-group": "^4.4.1",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
},
"peerDependencies": {
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
}
},
"node_modules/react-bootstrap-icons": {
@@ -8736,6 +8886,11 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"node_modules/react-markdown": {
"version": "7.0.1",
@@ -8820,6 +8975,21 @@
},
"peerDependencies": {
"react": ">= 0.14.0"
}
},
"node_modules/react-transition-group": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
"integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/readable-stream": {
@@ -10742,6 +10912,20 @@
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/uncontrollable": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
"integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
"dependencies": {
"@babel/runtime": "^7.6.3",
"@types/react": ">=16.9.11",
"invariant": "^2.2.4",
"react-lifecycles-compat": "^3.0.4"
},
"peerDependencies": {
"react": ">=15.0.0"
}
},
"node_modules/uncss": {
@@ -11304,6 +11488,14 @@
"domexception": "^1.0.1",
"webidl-conversions": "^4.0.2",
"xml-name-validator": "^3.0.0"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/wcwidth": {
@@ -12675,9 +12867,57 @@
"@popperjs/core": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.1.tgz",
"integrity": "sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==",
"peer": true
"integrity": "sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw=="
},
"@react-aria/ssr": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.1.0.tgz",
"integrity": "sha512-RxqQKmE8sO7TGdrcSlHTcVzMP450hqowtBSd2bBS9oPlcokVkaGq28c3Rwa8ty5ctw4EBCjXqjP7xdcKMGDzug==",
"requires": {
"@babel/runtime": "^7.6.2"
}
},
"@restart/context": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz",
"integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==",
"requires": {}
},
"@restart/hooks": {
"version": "0.3.27",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz",
"integrity": "sha512-s984xV/EapUIfkjlf8wz9weP2O9TNKR96C68FfMEy2bE69+H4cNv3RD4Mf97lW7Htt7PjZrYTjSC8f3SB9VCXw==",
"requires": {
"dequal": "^2.0.2"
}
},
"@restart/ui": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@restart/ui/-/ui-0.2.2.tgz",
"integrity": "sha512-PgNkiyOaWwx8ttQ45KNABXU3780fB/UxNFxcsCpC4RRAlaByZHHbNLOKfhuFs+ZUU0uLxEH9wYQEhDAZc6ajkA==",
"requires": {
"@babel/runtime": "^7.13.16",
"@popperjs/core": "^2.9.2",
"@react-aria/ssr": "^3.0.1",
"@restart/hooks": "^0.4.0",
"@types/warning": "^3.0.0",
"dequal": "^2.0.2",
"dom-helpers": "^5.2.0",
"prop-types": "^15.7.2",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
},
"dependencies": {
"@restart/hooks": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.0.tgz",
"integrity": "sha512-+RenTVobiCHPjUTbhQDV8m0PU1xEWqgloMIIOlf86oKnfghKR/l4tKto7TH543shEQZZa7ARSMTvT0cXN9u8+g==",
"requires": {
"dequal": "^2.0.2"
}
}
}
},
"@types/debug": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
@@ -12694,6 +12934,11 @@
"@types/unist": "*"
}
},
"@types/invariant": {
"version": "2.2.35",
"resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz",
"integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg=="
},
"@types/mdast": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
@@ -12738,6 +12983,14 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz",
"integrity": "sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-transition-group": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz",
"integrity": "sha512-KibDWL6nshuOJ0fu8ll7QnV/LVTo3PzQ9aCPnRUYPfX7eZohHwLIdNHj7pftanREzHNP4/nJa8oeM73uSiavMQ==",
"requires": {
"@types/react": "*"
}
@@ -12751,6 +13004,11 @@
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
},
"@types/warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
"integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
},
"@zeit/schemas": {
"version": "2.6.0",
@@ -13632,6 +13890,11 @@
}
}
}
},
"classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"cli-boxes": {
"version": "1.0.0",
@@ -14371,6 +14634,11 @@
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
"dev": true
},
"dequal": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
"integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug=="
},
"des.js": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
@@ -14404,6 +14672,15 @@
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"dev": true
}
}
},
"dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"requires": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"dom-serializer": {
@@ -15618,6 +15895,14 @@
"get-intrinsic": "^1.1.0",
"has": "^1.0.3",
"side-channel": "^1.0.4"
}
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"is-absolute-url": {
@@ -17940,6 +18225,15 @@
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
}
},
"prop-types-extra": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
"integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
"requires": {
"react-is": "^16.3.2",
"warning": "^4.0.0"
}
},
"property-information": {
@@ -18116,6 +18410,30 @@
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"react-bootstrap": {
"version": "2.0.0-beta.6",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.0.0-beta.6.tgz",
"integrity": "sha512-eHEkmESWYYgNLjqbz31G696Eytu+6GeF8CPHQ8t9Se12dUEej8OjBakyQP0OGms9yy1ZZeLG/Fvuo7VxiwMcuQ==",
"requires": {
"@babel/runtime": "^7.14.0",
"@restart/context": "^2.1.4",
"@restart/hooks": "^0.3.26",
"@restart/ui": "^0.2.1",
"@types/invariant": "^2.2.33",
"@types/prop-types": "^15.7.3",
"@types/react": ">=16.14.8",
"@types/react-transition-group": "^4.4.1",
"@types/warning": "^3.0.0",
"classnames": "^2.3.1",
"dom-helpers": "^5.2.1",
"invariant": "^2.2.4",
"prop-types": "^15.7.2",
"prop-types-extra": "^1.1.0",
"react-transition-group": "^4.4.1",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
}
},
"react-bootstrap-icons": {
@@ -18141,6 +18459,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-markdown": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-7.0.1.tgz",
@@ -18209,6 +18532,17 @@
"lowlight": "^1.17.0",
"prismjs": "^1.22.0",
"refractor": "^3.2.0"
}
},
"react-transition-group": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
"integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==",
"requires": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
}
},
"readable-stream": {
@@ -19761,6 +20095,17 @@
"has-bigints": "^1.0.1",
"has-symbols": "^1.0.2",
"which-boxed-primitive": "^1.0.2"
}
},
"uncontrollable": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
"integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
"requires": {
"@babel/runtime": "^7.6.3",
"@types/react": ">=16.9.11",
"invariant": "^2.2.4",
"react-lifecycles-compat": "^3.0.4"
}
},
"uncss": {
@@ -20200,6 +20545,14 @@
"domexception": "^1.0.1",
"webidl-conversions": "^4.0.2",
"xml-name-validator": "^3.0.0"
}
},
"warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"wcwidth": {
@@ -22,6 +22,7 @@
"dependencies": {
"bootstrap": "^5.1.1",
"react": "^17.0.2",
"react-bootstrap": "^2.0.0-beta.6",
"react-bootstrap-icons": "^1.5.0",
"react-dom": "^17.0.2",
"react-markdown": "^7.0.1",
@@ -59,6 +59,8 @@
Query(#[from] diesel::result::Error),
TaskJoin(#[from] tokio::task::JoinError),
KeyParse(#[from] thrussh_keys::Error),
}
diesel_infix_operator!(BitwiseAnd, " & ", diesel::sql_types::Integer);
@@ -1,10 +1,11 @@
use super::{
schema::{user_api_keys, user_crate_permissions, user_ssh_keys, users},
ConnectionPool, Result,
};
use diesel::{insert_into, prelude::*, Associations, Identifiable, Queryable};
use rand::{thread_rng, Rng};
use std::sync::Arc;
use std::{collections::HashMap, sync::Arc};
use thrussh_keys::PublicKeyBase64;
#[derive(Identifiable, Queryable, Associations, PartialEq, Eq, Hash, Debug)]
pub struct User {
@@ -60,8 +61,6 @@
) -> Result<Option<(UserSshKey, User)>> {
use crate::schema::user_ssh_keys::dsl::ssh_key;
eprintln!("ssh key: {:x?}", given_ssh_key);
tokio::task::spawn_blocking(move || {
let conn = conn.get()?;
@@ -71,6 +70,93 @@
.select((user_ssh_keys::all_columns, users::all_columns))
.get_result(&conn)
.optional()?)
})
.await?
}
pub async fn insert_ssh_key(
self: Arc<Self>,
conn: ConnectionPool,
ssh_key: &str,
) -> Result<()> {
let mut split = ssh_key.split_whitespace();
let key = match (split.next(), split.next()) {
(Some(_), Some(key)) => key,
(Some(key), None) => key,
_ => return Err(thrussh_keys::Error::CouldNotReadKey.into()),
};
let parsed_key = thrussh_keys::parse_public_key_base64(key)?;
tokio::task::spawn_blocking(move || {
use crate::schema::user_ssh_keys::dsl::{ssh_key, user_id};
let conn = conn.get()?;
insert_into(crate::schema::user_ssh_keys::dsl::user_ssh_keys)
.values((
ssh_key.eq(parsed_key.public_key_bytes()),
user_id.eq(self.id),
))
.execute(&conn)?;
Ok(())
})
.await?
}
pub async fn delete_user_ssh_key_by_id(
self: Arc<Self>,
conn: ConnectionPool,
ssh_key_id: i32,
) -> Result<bool> {
use crate::schema::user_ssh_keys::dsl::{id, user_id, user_ssh_keys};
tokio::task::spawn_blocking(move || {
let conn = conn.get()?;
let rows = diesel::delete(
user_ssh_keys
.filter(user_id.eq(self.id))
.filter(id.eq(ssh_key_id)),
)
.execute(&conn)?;
Ok(rows > 0)
})
.await?
}
pub async fn list_ssh_keys(
self: Arc<Self>,
conn: ConnectionPool,
) -> Result<HashMap<i32, String>> {
tokio::task::spawn_blocking(move || {
use crate::schema::user_ssh_keys::dsl::user_id;
let conn = conn.get()?;
let selected: Vec<(i32, Vec<u8>)> = crate::schema::user_ssh_keys::table
.filter(user_id.eq(self.id))
.inner_join(users::table)
.select((user_ssh_keys::id, user_ssh_keys::ssh_key))
.load(&conn)?;
Ok(selected
.into_iter()
.map(|(id, key)| {
(
id,
thrussh_keys::key::parse_public_key(&key)
.map(|v| v.fingerprint())
.unwrap_or_else(|e| format!("INVALID: {}", e)),
)
})
.collect())
})
.await?
}
@@ -17,6 +17,8 @@
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import SingleCrate from "./pages/SingleCrate";
import ListSshKeys from "./pages/ssh-keys/ListSshKeys";
import AddSshKeys from "./pages/ssh-keys/AddSshKeys";
function App() {
return (
@@ -28,6 +30,8 @@
<PrivateRoute exact path="/" component={() => <Redirect to="/dashboard" />} />
<PrivateRoute exact path="/dashboard" component={() => <Dashboard />} />
<PrivateRoute exact path="/crates/:crate" component={() => <SingleCrate />} />
<PrivateRoute exact path="/ssh-keys/list" component={() => <ListSshKeys />} />
<PrivateRoute exact path="/ssh-keys/add" component={() => <AddSshKeys />} />
</Switch>
</Router>
</ProvideAuth>
@@ -5,16 +5,14 @@
mod middleware;
use axum::{
body::Body,
handler::{delete, get, post, put},
http::header::ACCESS_CONTROL_ALLOW_ORIGIN,
http::Method,
AddExtensionLayer, Router,
};
use tower::ServiceBuilder;
use tower_http::cors::{Any, CorsLayer};
use tower_http::set_header::SetResponseHeaderLayer;
#[allow(clippy::unused_async)]
async fn hello_world() -> &'static str {
"hello, world!"
@@ -71,9 +69,11 @@
let web_unauthenticated =
axum_box_after_every_route!(Router::new().route("/login", post(endpoints::web_api::login)));
let web_authenticated = axum_box_after_every_route!(
Router::new().route("/crates/:crate", get(endpoints::web_api::crate_info))
)
let web_authenticated = axum_box_after_every_route!(Router::new()
.route("/crates/:crate", get(endpoints::web_api::crate_info))
.route("/ssh-key", get(endpoints::web_api::get_ssh_keys))
.route("/ssh-key", put(endpoints::web_api::add_ssh_key))
.route("/ssh-key/:id", delete(endpoints::web_api::delete_ssh_key)))
.layer(
ServiceBuilder::new()
.layer_fn(middleware::auth::AuthMiddleware)
@@ -93,7 +93,13 @@
.layer(
CorsLayer::new()
.allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS])
.allow_methods(vec![
Method::GET,
Method::POST,
Method::DELETE,
Method::PUT,
Method::OPTIONS,
])
.allow_origin(Any)
.allow_credentials(false),
)
@@ -26,23 +26,7 @@
<NavLink to="/dashboard" className="nav-link">Home</NavLink>
</li>
<li className="nav-item">
<a className="nav-link" href="#">Link</a>
</li>
<li className="nav-item dropdown">
<a className="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Dropdown
</a>
<ul className="dropdown-menu" aria-labelledby="navbarDropdown">
<li><a className="dropdown-item" href="#">Action</a></li>
<li><a className="dropdown-item" href="#">Another action</a></li>
<li><hr className="dropdown-divider" /></li>
<li><a className="dropdown-item" href="#">Something else here</a></li>
</ul>
</li>
<li className="nav-item">
<a className="nav-link disabled" href="#" tabIndex={-1} aria-disabled="true">Disabled</a>
<NavLink to="/ssh-keys/list" className="nav-link">SSH Keys</NavLink>
</li>
</ul>
@@ -1,0 +1,85 @@
import React = require("react");
import { useState, useEffect } from 'react';
import { Link, useHistory } from 'react-router-dom';
import Nav from "../../sections/Nav";
import { useAuth } from "../../useAuth";
import { authenticatedEndpoint } from "../../util";
import { Plus } from "react-bootstrap-icons";
export default function ListSshKeys() {
const auth = useAuth();
const router = useHistory();
const [sshKey, setSshKey] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const submitSshKey = async (evt) => {
evt.preventDefault();
setError("");
setLoading(true);
try {
let res = await fetch(authenticatedEndpoint(auth, 'ssh-key'), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ key: sshKey }),
});
let json = await res.json();
if (json.error) {
throw new Error(json.error);
}
setSshKey("");
router.push("/ssh-keys/list");
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
};
return (
<div className="text-white">
<Nav />
<div className="container mt-4 pb-4">
<h1>New SSH Key</h1>
<div className="alert alert-danger alert-dismissible" role="alert" style={{ display: error ? 'block' : 'none' }}>
{error}
<button type="button" className="btn-close" aria-label="Close" onClick={() => setError("")}>
</button>
</div>
<div className="card border-0 shadow-sm text-black">
<div className="card-body">
<form onSubmit={submitSshKey}>
<textarea className="form-control" rows={3}
placeholder="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILYAIoV2OKRSh/DcM3TicD/NK/4TdqwwBPbKgFQKmGZ3 john@home"
onChange={e => setSshKey(e.target.value)}
value={sshKey}
/>
<div className="clearfix"></div>
<button type="submit" className="btn btn-success mt-2 float-end" style={{ display: !loading ? 'block' : 'none' }}>Submit</button>
<div className="spinner-border text-primary mt-4 float-end" role="status" style={{ display: loading ? 'block' : 'none' }}>
<span className="visually-hidden">Submitting...</span>
</div>
<Link to="/ssh-keys/list" className="btn btn-danger mt-2 float-end me-1">Cancel</Link>
</form>
</div>
</div>
</div>
</div>
);
}
@@ -1,0 +1,118 @@
import React = require("react");
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import Nav from "../../sections/Nav";
import { useAuth } from "../../useAuth";
import { authenticatedEndpoint } from "../../util";
import { Plus, Trash } from "react-bootstrap-icons";
import { Button, Modal } from "react-bootstrap";
export default function ListSshKeys() {
const auth = useAuth();
const [error, setError] = useState("");
const [deleting, setDeleting] = useState(null);
const [sshKeys, setSshKeys] = useState(null);
const [reloadSshKeys, setReloadSshKeys] = useState(0);
useEffect(async () => {
let res = await fetch(authenticatedEndpoint(auth, 'ssh-key'));
let json = await res.json();
setSshKeys(json);
}, [reloadSshKeys]);
if (!sshKeys) {
return (<div>loading...</div>);
}
const deleteKey = async () => {
setError("");
try {
let res = await fetch(authenticatedEndpoint(auth, `ssh-key/${deleting.id}`), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
let json = await res.json();
if (json.error) {
throw new Error(json.error);
}
setReloadSshKeys(reloadSshKeys + 1);
} catch (e) {
setError(e.message);
} finally {
setDeleting(null);
}
};
return (
<div className="text-white">
<Nav />
<div className="container mt-4 pb-4">
<h1>Manage your SSH Keys</h1>
<div className="alert alert-danger alert-dismissible" role="alert" style={{ display: error ? 'block' : 'none' }}>
{error}
<button type="button" className="btn-close" aria-label="Close" onClick={() => setError("")}>
</button>
</div>
<div className="card border-0 shadow-sm text-black">
<ul className="list-group list-group-flush">
{sshKeys.keys.map(key => (
<li key={key.id} className="list-group-item">
<div className="d-flex">
<div className="flex-grow-1"><strong>{key.fingerprint}</strong></div>
<div>
<button type="button" className="btn text-danger" onClick={() => setDeleting(key)}><Trash /></button>
</div>
</div>
</li>
))}
</ul>
</div>
<Link to="/ssh-keys/add" className="btn btn-outline-light mt-2 float-end"><Plus /> Add New</Link>
</div>
<DeleteModal show={deleting != null}
onCancel={() => setDeleting(null)}
onConfirm={() => deleteKey()}
fingerprint={deleting?.fingerprint} />
</div>
);
}
function DeleteModal(props: { show: boolean, onCancel: () => void, onConfirm: () => void, fingerprint: string }) {
return (
<Modal
show={props.show}
onHide={props.onCancel}
size="lg"
aria-labelledby="delete-modal-title"
centered
>
<Modal.Header closeButton>
<Modal.Title id="delete-modal-title">
Are you sure you wish to delete this SSH key?
</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>
Are you sure you wish to delete the SSH key with the fingerprint: <strong>{props.fingerprint}</strong>?
</p>
</Modal.Body>
<Modal.Footer>
<Button onClick={props.onCancel} variant="primary">Close</Button>
<Button onClick={props.onConfirm} variant="danger">Delete</Button>
</Modal.Footer>
</Modal>
);
}
@@ -1,5 +1,9 @@
mod crate_info;
mod login;
mod ssh_key;
pub use crate_info::handle as crate_info;
pub use login::handle as login;
pub use ssh_key::{
handle_delete as delete_ssh_key, handle_get as get_ssh_keys, handle_put as add_ssh_key,
};
@@ -1,0 +1,95 @@
use chartered_db::{users::User, ConnectionPool};
use axum::{extract, Json};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use thiserror::Error;
#[derive(Serialize)]
pub struct GetResponse {
keys: Vec<GetResponseKey>,
}
#[derive(Serialize)]
pub struct GetResponseKey {
id: i32,
fingerprint: String,
}
pub async fn handle_get(
extract::Extension(db): extract::Extension<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
) -> Result<Json<GetResponse>, Error> {
let keys = user
.list_ssh_keys(db)
.await?
.into_iter()
.map(|(id, fingerprint)| GetResponseKey { id, fingerprint })
.collect();
Ok(Json(GetResponse { keys }))
}
#[derive(Deserialize)]
pub struct PutRequest {
key: String,
}
#[derive(Serialize)]
pub struct PutResponse {
error: bool,
}
pub async fn handle_put(
extract::Extension(db): extract::Extension<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
extract::Json(req): extract::Json<PutRequest>,
) -> Result<Json<PutResponse>, Error> {
match user.insert_ssh_key(db, &req.key).await {
Ok(()) => Ok(Json(PutResponse { error: false })),
Err(e @ chartered_db::Error::KeyParse(_)) => Err(Error::KeyParse(e)),
Err(e) => Err(Error::Database(e)),
}
}
#[derive(Serialize)]
pub struct DeleteResponse {
error: bool,
}
pub async fn handle_delete(
extract::Extension(db): extract::Extension<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
extract::Path((_api_key, ssh_key_id)): extract::Path<(String, i32)>,
) -> Result<Json<DeleteResponse>, Error> {
let deleted = user.delete_user_ssh_key_by_id(db, ssh_key_id).await?;
if deleted {
Ok(Json(DeleteResponse { error: false }))
} else {
Err(Error::NonExistentKey)
}
}
#[derive(Error, Debug)]
pub enum Error {
#[error("Failed to query database")]
Database(#[from] chartered_db::Error),
#[error("Failed to parse SSH key: {0}")]
KeyParse(chartered_db::Error),
#[error("The key given does not exist")]
NonExistentKey,
}
impl Error {
pub fn status_code(&self) -> axum::http::StatusCode {
use axum::http::StatusCode;
match self {
Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::KeyParse(_) | Self::NonExistentKey => StatusCode::BAD_REQUEST,
}
}
}
define_error_response!(Error);