From 38bf212911604a84b0d4a487c3fdbc46aaae24a2 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 18 Sep 2021 02:09:42 +0100 Subject: [PATCH] Web UI support for adding new members to a crate --- chartered-frontend/package-lock.json | 371 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- chartered-frontend/package.json | 3 +++ chartered-db/src/crates.rs | 26 +++++++++++++++++++++++++- chartered-db/src/users.rs | 18 ++++++++++++++++++ chartered-web/src/main.rs | 7 ++++++- chartered-frontend/src/pages/crate/Members.tsx | 146 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- chartered-web/src/endpoints/web_api/mod.rs | 2 ++ chartered-web/src/endpoints/web_api/search_users.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-web/src/endpoints/web_api/crates/members.rs | 23 +++++++++++++++++++++-- chartered-web/src/endpoints/web_api/crates/mod.rs | 3 ++- 10 files changed, 583 insertions(+), 70 deletions(-) diff --git a/chartered-frontend/package-lock.json b/chartered-frontend/package-lock.json index 0c70238..50d1efa 100644 --- a/chartered-frontend/package-lock.json +++ a/chartered-frontend/package-lock.json @@ -10,9 +10,11 @@ "license": "0BSD", "dependencies": { "bootstrap": "^5.1.1", + "lodash": "^4.17.21", "react": "^17.0.2", "react-bootstrap": "^2.0.0-beta.6", "react-bootstrap-icons": "^1.5.0", + "react-bootstrap-typeahead": "^5.2.0", "react-dom": "^17.0.2", "react-human-time": "^1.2.0", "react-markdown": "^7.0.1", @@ -22,6 +24,7 @@ "source-code-pro": "^2.38.0" }, "devDependencies": { + "@types/lodash": "^4.14.173", "@types/react": "^17.0.20", "@types/react-dom": "^17.0.9", "parcel-bundler": "^1.12.5", @@ -1686,6 +1689,19 @@ "node": ">=6.9.0" } }, + "node_modules/@hypnosphi/create-react-context": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz", + "integrity": "sha512-V1klUed202XahrWJLLOT3EXNeCpFHCcJntdFGI15ntCwau+jfT386w7OFTMaCqOgXUH1fa0w/I1oZs+i/Rfr0A==", + "dependencies": { + "gud": "^1.0.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "prop-types": "^15.0.0", + "react": ">=0.14.0" + } + }, "node_modules/@iarna/toml": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", @@ -1868,6 +1884,12 @@ "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz", "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==" }, + "node_modules/@types/lodash": { + "version": "4.14.173", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.173.tgz", + "integrity": "sha512-vv0CAYoaEjCw/mLy96GBTnRoZrSxkGE0BKzKimdR8P3OzrNYNvBgtW7p055A+E8C31vXNUhWKoFCbhq7gbyhFg==", + "dev": true + }, "node_modules/@types/mdast": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", @@ -2786,7 +2808,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -3250,6 +3271,11 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "node_modules/compute-scroll-into-view": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz", + "integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3849,6 +3875,22 @@ "dev": true, "engines": { "node": ">=0.10" + } + }, + "node_modules/deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dependencies": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/deep-extend": { @@ -3888,7 +3930,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "dependencies": { "object-keys": "^1.0.12" }, @@ -4537,8 +4578,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "2.2.7", @@ -4721,8 +4761,7 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/gensync": { "version": "1.0.0-beta.2", @@ -4737,7 +4776,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -4874,6 +4912,11 @@ "brfs": "^1.2.0", "unicode-trie": "^0.3.1" } + }, + "node_modules/gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" }, "node_modules/har-schema": { "version": "2.0.0", @@ -4902,7 +4945,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -4953,7 +4995,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -4965,7 +5006,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -5578,6 +5618,21 @@ "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-arrayish": { @@ -5695,7 +5750,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -5897,7 +5951,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -6206,8 +6259,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.clone": { "version": "4.5.0", @@ -6218,8 +6270,7 @@ "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -7533,12 +7584,26 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz", "integrity": "sha512-wqdhLpfCUbEsoEwl3FXwGyv8ief1k/1aUdIPCqVnupM6e8l63BEJdiF/0swtn04/8p05tG/T0FrpTlfwvljOdw==", "dev": true + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -7970,6 +8035,16 @@ "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "dev": true + }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } }, "node_modules/posix-character-classes": { "version": "0.1.1", @@ -8873,8 +8948,41 @@ }, "peerDependencies": { "react": "^16.8.6 || ^17" + } + }, + "node_modules/react-bootstrap-typeahead": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-bootstrap-typeahead/-/react-bootstrap-typeahead-5.2.0.tgz", + "integrity": "sha512-tM7HiUX4ra/3lF6YmrXCjIreG5o1RIsAuFQiRKeZKtRWLQ+bQfa+rdKpga2YNj+lvIGgJNsyUtVLSMxyZ3czEg==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "@restart/hooks": "^0.4.0", + "classnames": "^2.2.0", + "fast-deep-equal": "^3.1.1", + "invariant": "^2.2.1", + "lodash.debounce": "^4.0.8", + "prop-types": "^15.5.8", + "react-overlays": "^5.1.0", + "react-popper": "^1.0.0", + "scroll-into-view-if-needed": "^2.2.20", + "warning": "^4.0.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, + "node_modules/react-bootstrap-typeahead/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/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -8940,6 +9048,42 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/react-overlays": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.1.1.tgz", + "integrity": "sha512-eCN2s2/+GVZzpnId4XVWtvDPYYBD2EtOGP74hE+8yDskPzFy9+pV1H3ZZihxuRdEbQzzacySaaDkR7xE0ydl4Q==", + "dependencies": { + "@babel/runtime": "^7.13.8", + "@popperjs/core": "^2.8.6", + "@restart/hooks": "^0.3.26", + "@types/warning": "^3.0.0", + "dom-helpers": "^5.2.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/react-popper": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.11.tgz", + "integrity": "sha512-VSA/bS+pSndSF2fiasHK/PTEEAyOpX60+H5EPAjoArr8JGm+oihu4UbrqcEBpQibJxBVCpYyjAX7abJ+7DoYVg==", + "dependencies": { + "@babel/runtime": "^7.1.2", + "@hypnosphi/create-react-context": "^0.3.1", + "deep-equal": "^1.1.1", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.7", + "warning": "^4.0.2" + }, + "peerDependencies": { + "react": "0.14.x || ^15.0.0 || ^16.0.0 || ^17.0.0" + } }, "node_modules/react-router": { "version": "5.2.1", @@ -9212,6 +9356,21 @@ }, "engines": { "node": ">=0.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", + "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/regexpu-core": { @@ -9711,6 +9870,14 @@ "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" + } + }, + "node_modules/scroll-into-view-if-needed": { + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.28.tgz", + "integrity": "sha512-8LuxJSuFVc92+0AdNv4QOxRL4Abeo1DgLnGNkn1XlaujPH/3cCFz3QI60r2VNu4obJJROzgnIUw5TKQkZvZI1w==", + "dependencies": { + "compute-scroll-into-view": "^1.0.17" } }, "node_modules/semver": { @@ -10896,6 +11063,11 @@ "node": ">= 0.8.0" } }, + "node_modules/typed-styles": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", + "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==" + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -12806,6 +12978,15 @@ "requires": { "@babel/helper-validator-identifier": "^7.14.9", "to-fast-properties": "^2.0.0" + } + }, + "@hypnosphi/create-react-context": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz", + "integrity": "sha512-V1klUed202XahrWJLLOT3EXNeCpFHCcJntdFGI15ntCwau+jfT386w7OFTMaCqOgXUH1fa0w/I1oZs+i/Rfr0A==", + "requires": { + "gud": "^1.0.0", + "warning": "^4.0.3" } }, "@iarna/toml": { @@ -12954,6 +13135,12 @@ "version": "2.2.35", "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz", "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==" + }, + "@types/lodash": { + "version": "4.14.173", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.173.tgz", + "integrity": "sha512-vv0CAYoaEjCw/mLy96GBTnRoZrSxkGE0BKzKimdR8P3OzrNYNvBgtW7p055A+E8C31vXNUhWKoFCbhq7gbyhFg==", + "dev": true }, "@types/mdast": { "version": "3.0.10", @@ -13720,7 +13907,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -14096,6 +14282,11 @@ } } }, + "compute-scroll-into-view": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz", + "integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -14589,6 +14780,19 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } }, "deep-extend": { "version": "0.6.0", @@ -14623,7 +14827,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -15155,8 +15358,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { "version": "2.2.7", @@ -15300,8 +15502,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "gensync": { "version": "1.0.0-beta.2", @@ -15313,7 +15514,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -15422,6 +15622,11 @@ "brfs": "^1.2.0", "unicode-trie": "^0.3.1" } + }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" }, "har-schema": { "version": "2.0.0", @@ -15443,7 +15648,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -15480,14 +15684,12 @@ "has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, "has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -15961,6 +16163,15 @@ "requires": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" + } + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" } }, "is-arrayish": { @@ -16053,7 +16264,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -16183,7 +16393,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -16425,8 +16634,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.clone": { "version": "4.5.0", @@ -16437,8 +16645,7 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, "lodash.memoize": { "version": "4.1.2", @@ -17365,12 +17572,20 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz", "integrity": "sha512-wqdhLpfCUbEsoEwl3FXwGyv8ief1k/1aUdIPCqVnupM6e8l63BEJdiF/0swtn04/8p05tG/T0FrpTlfwvljOdw==", "dev": true + }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object-visit": { "version": "1.0.1", @@ -17721,6 +17936,11 @@ "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "dev": true }, + "popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -18463,6 +18683,34 @@ "integrity": "sha512-QC5q4meHQG0cO9RJzeDLSqZ1gbVa9jxFCpONCE3GYl2FkbAKSyJAEsONlzTApbZ8/oG87gPWq0xAyn5SZ/Jafw==", "requires": { "prop-types": "^15.7.2" + } + }, + "react-bootstrap-typeahead": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-bootstrap-typeahead/-/react-bootstrap-typeahead-5.2.0.tgz", + "integrity": "sha512-tM7HiUX4ra/3lF6YmrXCjIreG5o1RIsAuFQiRKeZKtRWLQ+bQfa+rdKpga2YNj+lvIGgJNsyUtVLSMxyZ3czEg==", + "requires": { + "@babel/runtime": "^7.14.6", + "@restart/hooks": "^0.4.0", + "classnames": "^2.2.0", + "fast-deep-equal": "^3.1.1", + "invariant": "^2.2.1", + "lodash.debounce": "^4.0.8", + "prop-types": "^15.5.8", + "react-overlays": "^5.1.0", + "react-popper": "^1.0.0", + "scroll-into-view-if-needed": "^2.2.20", + "warning": "^4.0.1" + }, + "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" + } + } } }, "react-dom": { @@ -18520,6 +18768,35 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" } + } + }, + "react-overlays": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.1.1.tgz", + "integrity": "sha512-eCN2s2/+GVZzpnId4XVWtvDPYYBD2EtOGP74hE+8yDskPzFy9+pV1H3ZZihxuRdEbQzzacySaaDkR7xE0ydl4Q==", + "requires": { + "@babel/runtime": "^7.13.8", + "@popperjs/core": "^2.8.6", + "@restart/hooks": "^0.3.26", + "@types/warning": "^3.0.0", + "dom-helpers": "^5.2.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + } + }, + "react-popper": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.11.tgz", + "integrity": "sha512-VSA/bS+pSndSF2fiasHK/PTEEAyOpX60+H5EPAjoArr8JGm+oihu4UbrqcEBpQibJxBVCpYyjAX7abJ+7DoYVg==", + "requires": { + "@babel/runtime": "^7.1.2", + "@hypnosphi/create-react-context": "^0.3.1", + "deep-equal": "^1.1.1", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.7", + "warning": "^4.0.2" } }, "react-router": { @@ -18735,6 +19012,15 @@ "is-plain-object": "^2.0.4" } } + } + }, + "regexp.prototype.flags": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", + "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" } }, "regexpu-core": { @@ -19126,6 +19412,14 @@ "object-assign": "^4.1.1" } }, + "scroll-into-view-if-needed": { + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.28.tgz", + "integrity": "sha512-8LuxJSuFVc92+0AdNv4QOxRL4Abeo1DgLnGNkn1XlaujPH/3cCFz3QI60r2VNu4obJJROzgnIUw5TKQkZvZI1w==", + "requires": { + "compute-scroll-into-view": "^1.0.17" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -20103,6 +20397,11 @@ "requires": { "prelude-ls": "~1.1.2" } + }, + "typed-styles": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", + "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==" }, "typedarray": { "version": "0.0.6", diff --git a/chartered-frontend/package.json b/chartered-frontend/package.json index 7e28737..e2e4938 100644 --- a/chartered-frontend/package.json +++ a/chartered-frontend/package.json @@ -12,6 +12,7 @@ "author": "", "license": "0BSD", "devDependencies": { + "@types/lodash": "^4.14.173", "@types/react": "^17.0.20", "@types/react-dom": "^17.0.9", "parcel-bundler": "^1.12.5", @@ -21,9 +22,11 @@ }, "dependencies": { "bootstrap": "^5.1.1", + "lodash": "^4.17.21", "react": "^17.0.2", "react-bootstrap": "^2.0.0-beta.6", "react-bootstrap-icons": "^1.5.0", + "react-bootstrap-typeahead": "^5.2.0", "react-dom": "^17.0.2", "react-human-time": "^1.2.0", "react-markdown": "^7.0.1", diff --git a/chartered-db/src/crates.rs b/chartered-db/src/crates.rs index 0a98e2b..8a19349 100644 --- a/chartered-db/src/crates.rs +++ a/chartered-db/src/crates.rs @@ -222,6 +222,30 @@ .await? } + pub async fn insert_permissions( + self: Arc, + conn: ConnectionPool, + given_user_id: i32, + given_permissions: crate::users::UserCratePermissionValue, + ) -> Result { + tokio::task::spawn_blocking(move || { + use crate::schema::user_crate_permissions::dsl::{ + crate_id, permissions, user_crate_permissions, user_id, + }; + + let conn = conn.get()?; + + Ok(diesel::insert_into(user_crate_permissions) + .values(( + user_id.eq(given_user_id), + crate_id.eq(self.id), + permissions.eq(given_permissions.bits()), + )) + .execute(&conn)?) + }) + .await? + } + pub async fn delete_member( self: Arc, conn: ConnectionPool, @@ -237,7 +261,7 @@ diesel::delete( user_crate_permissions .filter(user_id.eq(given_user_id)) - .filter(crate_id.eq(self.id)) + .filter(crate_id.eq(self.id)), ) .execute(&conn)?; diff --git a/chartered-db/src/users.rs b/chartered-db/src/users.rs index e7f9ba7..be6de05 100644 --- a/chartered-db/src/users.rs +++ a/chartered-db/src/users.rs @@ -16,6 +16,24 @@ } impl User { + pub async fn search( + conn: ConnectionPool, + given_query: String, + limit: i64, + ) -> Result> { + use crate::schema::users::dsl::username; + + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + + Ok(crate::schema::users::table + .filter(username.like(format!("%{}%", given_query))) + .limit(limit) + .load(&conn)?) + }) + .await? + } + pub async fn find_by_username( conn: ConnectionPool, given_username: String, diff --git a/chartered-web/src/main.rs b/chartered-web/src/main.rs index 5ba3a94..1041c6d 100644 --- a/chartered-web/src/main.rs +++ a/chartered-web/src/main.rs @@ -76,12 +76,17 @@ ) .route( "/crates/:crate/members", - patch(endpoints::web_api::crates::update_members) + patch(endpoints::web_api::crates::update_member) ) .route( "/crates/:crate/members", + put(endpoints::web_api::crates::insert_member) + ) + .route( + "/crates/:crate/members", delete(endpoints::web_api::crates::delete_member) ) + .route("/users/search", get(endpoints::web_api::search_users)) .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))) diff --git a/chartered-frontend/src/pages/crate/Members.tsx b/chartered-frontend/src/pages/crate/Members.tsx index 9412f95..f6dca03 100644 --- a/chartered-frontend/src/pages/crate/Members.tsx +++ a/chartered-frontend/src/pages/crate/Members.tsx @@ -1,9 +1,12 @@ import React = require("react"); import { useState } from "react"; import { PersonPlus, Trash, CheckLg, Save, PlusLg } from 'react-bootstrap-icons'; import { authenticatedEndpoint, useAuthenticatedRequest } from "../../util"; import { useAuth } from "../../useAuth"; import { Button, Modal } from "react-bootstrap"; +import { AsyncTypeahead } from "react-bootstrap-typeahead"; +import { debounce } from "lodash"; +import _ = require("lodash"); interface CratesMembersResponse { allowed_permissions: string[], @@ -23,6 +26,15 @@ auth, endpoint: `crates/${crate}/members`, }, [reload]); + const [prospectiveMembers, setProspectiveMembers] = useState([]); + + React.useEffect(() => { + if (response && response.members) { + setProspectiveMembers(prospectiveMembers.filter((prospectiveMember) => { + _.findIndex(response.members, (responseMember) => responseMember.id === prospectiveMember.id) === -1 + })); + } + }, [response]) if (error) { return <>{error}; @@ -37,50 +49,49 @@ const allowedPermissions = response.allowed_permissions; return
-
+
{response.members.map((member, index) => + setReload(reload + 1)} + /> + )} + + {prospectiveMembers.map((member, index) => setReload(reload + 1)} /> )} - - - - - - - - - + setProspectiveMembers([ + ...prospectiveMembers, + { + id: userId, + username, + permissions: ["VISIBLE"], + } + ])} + existingMembers={response.members} + />
-
- -
-
- - - - - -
; } -function MemberListItem({ crate, member, allowedPermissions, onUpdateComplete }: { crate: string, member: Member, allowedPermissions: string[], onUpdateComplete: () => any }) { +function MemberListItem({ crate, member, prospectiveMember, allowedPermissions, onUpdateComplete }: { crate: string, member: Member, prospectiveMember: boolean, allowedPermissions: string[], onUpdateComplete: () => any }) { const auth = useAuth(); const [selectedPermissions, setSelectedPermissions] = useState(member.permissions); const [deleting, setDeleting] = useState(false); @@ -94,7 +105,7 @@ try { let res = await fetch(authenticatedEndpoint(auth, `crates/${crate}/members`), { - method: 'PATCH', + method: prospectiveMember ? 'PUT' : 'PATCH', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', @@ -152,11 +163,11 @@ Loading...
; - } else if (selectedPermissions.indexOf("VISIBLE") === -1) { + } else if (!prospectiveMember && selectedPermissions.indexOf("VISIBLE") === -1) { itemAction = ; - } else if (selectedPermissions.sort().join(',') != member.permissions.sort().join(',')) { + } else if (prospectiveMember || selectedPermissions.sort().join(',') != member.permissions.sort().join(',')) { itemAction = ; @@ -194,6 +205,83 @@ ; +} + +function MemberListInserter({ onInsert, existingMembers }: { existingMembers: Member[], onInsert: (username, user_id) => any }) { + const auth = useAuth(); + const searchRef = React.useRef(null); + const [loading, setLoading] = useState(false); + const [options, setOptions] = useState([]); + const [error, setError] = useState(""); + + const handleSearch = async (query) => { + setLoading(true); + setError(""); + + try { + let res = await fetch(authenticatedEndpoint(auth, `users/search?q=` + encodeURIComponent(query))); + let json = await res.json(); + + if (json.error) { + throw new Error(json.error); + } + + setOptions(json.users || []); + } catch (e) { + setError(e.message); + } finally { + setLoading(false); + } + }; + + const handleChange = (selected) => { + onInsert(selected[0].username, selected[0].user_id); + searchRef.current.clear(); + } + + return + +
+ +
+ + + + _.findIndex(existingMembers, (existing) => option.user_id === existing.id) === -1} + labelKey="username" + options={options} + isLoading={loading} + placeholder="Search for User" + onChange={handleChange} + ref={searchRef} + renderMenuItemChildren={(option, props) => <> + {option.username} + {option.username} + } + /> + +
{error}
+ + + + + + + + + ; } function RenderPermissions({ allowedPermissions, selectedPermissions, userId, onChange }: { allowedPermissions: string[], selectedPermissions: string[], userId: number, onChange: (permissions) => any }) { diff --git a/chartered-web/src/endpoints/web_api/mod.rs b/chartered-web/src/endpoints/web_api/mod.rs index 8920f76..df98705 100644 --- a/chartered-web/src/endpoints/web_api/mod.rs +++ a/chartered-web/src/endpoints/web_api/mod.rs @@ -1,8 +1,10 @@ pub mod crates; mod login; +mod search_users; mod ssh_key; pub use login::handle as login; +pub use search_users::handle as search_users; pub use ssh_key::{ handle_delete as delete_ssh_key, handle_get as get_ssh_keys, handle_put as add_ssh_key, }; diff --git a/chartered-web/src/endpoints/web_api/search_users.rs b/chartered-web/src/endpoints/web_api/search_users.rs new file mode 100644 index 0000000..7fddc45 100644 --- /dev/null +++ a/chartered-web/src/endpoints/web_api/search_users.rs @@ -1,0 +1,54 @@ +use axum::{extract, Json}; +use chartered_db::{users::User, ConnectionPool}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Deserialize)] +pub struct RequestParams { + q: String, +} + +#[derive(Serialize)] +pub struct Response { + users: Vec, +} + +#[derive(Serialize)] +pub struct ResponseUser { + user_id: i32, + username: String, +} + +pub async fn handle( + extract::Extension(db): extract::Extension, + extract::Query(req): extract::Query, +) -> Result, Error> { + let users = User::search(db, req.q, 5) + .await? + .into_iter() + .map(|user| ResponseUser { + user_id: user.id, + username: user.username, + }) + .collect(); + + Ok(Json(Response { users })) +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("Failed to query database")] + Database(#[from] chartered_db::Error), +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + use axum::http::StatusCode; + + match self { + Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/web_api/crates/members.rs b/chartered-web/src/endpoints/web_api/crates/members.rs index de2942b..4252017 100644 --- a/chartered-web/src/endpoints/web_api/crates/members.rs +++ a/chartered-web/src/endpoints/web_api/crates/members.rs @@ -52,7 +52,7 @@ } #[derive(Deserialize)] -pub struct PatchRequest { +pub struct PutOrPatchRequest { user_id: i32, permissions: Permission, } @@ -61,7 +61,7 @@ extract::Path((_session_key, name)): extract::Path<(String, String)>, extract::Extension(db): extract::Extension, extract::Extension(user): extract::Extension>, - extract::Json(req): extract::Json, + extract::Json(req): extract::Json, ) -> Result, Error> { let crate_ = Crate::find_by_name(db.clone(), name) .await? @@ -75,6 +75,25 @@ if affected_rows == 0 { return Err(Error::UpdateConflictRemoved); } + + Ok(Json(ErrorResponse { error: None })) +} + +pub async fn handle_put( + extract::Path((_session_key, name)): extract::Path<(String, String)>, + extract::Extension(db): extract::Extension, + extract::Extension(user): extract::Extension>, + extract::Json(req): extract::Json, +) -> Result, Error> { + let crate_ = Crate::find_by_name(db.clone(), name) + .await? + .ok_or(Error::NoCrate) + .map(std::sync::Arc::new)?; + ensure_has_crate_perm!(db, user, crate_, Permission::VISIBLE | -> Error::NoCrate, Permission::MANAGE_USERS | -> Error::NoPermission); + + crate_ + .insert_permissions(db, req.user_id, req.permissions) + .await?; Ok(Json(ErrorResponse { error: None })) } diff --git a/chartered-web/src/endpoints/web_api/crates/mod.rs b/chartered-web/src/endpoints/web_api/crates/mod.rs index 19bf6bf..6aa79c9 100644 --- a/chartered-web/src/endpoints/web_api/crates/mod.rs +++ a/chartered-web/src/endpoints/web_api/crates/mod.rs @@ -1,7 +1,8 @@ mod info; mod members; pub use info::handle as info; pub use members::{ - handle_delete as delete_member, handle_get as get_members, handle_patch as update_members, + handle_delete as delete_member, handle_get as get_members, handle_patch as update_member, + handle_put as insert_member, }; -- rgit 0.1.3