diff --git a/package-lock.json b/package-lock.json index 110560bfb225df090d54e783967c11562364058b..31fe8f4f0895d07d029f820c4a2e4ee0e0670956 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "rollup-plugin-node-polyfills": "~0.2.1", "sass": "^1.66.0", "typescript": "~5.4.0", - "vite": "~5.2.0", + "vite": "~5.3.0", "vue-tsc": "~2.0.0" } }, @@ -973,7 +973,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=12" } @@ -990,7 +989,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=12" } @@ -1007,7 +1005,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=12" } @@ -1024,7 +1021,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=12" } @@ -1041,7 +1037,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=12" } @@ -1058,7 +1053,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=12" } @@ -1075,7 +1069,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -1092,7 +1085,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -1109,7 +1101,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -1126,7 +1117,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -1143,7 +1133,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -1160,7 +1149,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -1177,7 +1165,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -1194,7 +1181,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -1211,7 +1197,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -1228,7 +1213,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -1245,7 +1229,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -1262,7 +1245,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -1279,7 +1261,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -1296,7 +1277,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=12" } @@ -1313,7 +1293,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=12" } @@ -1330,7 +1309,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=12" } @@ -1347,7 +1325,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=12" } @@ -2478,9 +2455,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", - "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "version": "20.14.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.5.tgz", + "integrity": "sha512-aoRR+fJkZT2l0aGOJhuA8frnCSoNX6W7U2mpNq63+BxBIj5BQFt8rHy627kijCmm63ijdSdwvGgpUsU6MBsZZA==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -2511,16 +2488,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.0.tgz", - "integrity": "sha512-FX1X6AF0w8MdVFLSdqwqN/me2hyhuQg4ykN6ZpVhh1ij/80pTvDKclX1sZB9iqex8SjQfVhwMKs3JtnnMLzG9w==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.1.tgz", + "integrity": "sha512-kZqi+WZQaZfPKnsflLJQCz6Ze9FFSMfXrrIOcyargekQxG37ES7DJNpJUE9Q/X5n3yTIP/WPutVNzgknQ7biLg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.13.0", - "@typescript-eslint/type-utils": "7.13.0", - "@typescript-eslint/utils": "7.13.0", - "@typescript-eslint/visitor-keys": "7.13.0", + "@typescript-eslint/scope-manager": "7.13.1", + "@typescript-eslint/type-utils": "7.13.1", + "@typescript-eslint/utils": "7.13.1", + "@typescript-eslint/visitor-keys": "7.13.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2544,15 +2521,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.13.0.tgz", - "integrity": "sha512-EjMfl69KOS9awXXe83iRN7oIEXy9yYdqWfqdrFAYAAr6syP8eLEFI7ZE4939antx2mNgPRW/o1ybm2SFYkbTVA==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.13.1.tgz", + "integrity": "sha512-1ELDPlnLvDQ5ybTSrMhRTFDfOQEOXNM+eP+3HT/Yq7ruWpciQw+Avi73pdEbA4SooCawEWo3dtYbF68gN7Ed1A==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.13.0", - "@typescript-eslint/types": "7.13.0", - "@typescript-eslint/typescript-estree": "7.13.0", - "@typescript-eslint/visitor-keys": "7.13.0", + "@typescript-eslint/scope-manager": "7.13.1", + "@typescript-eslint/types": "7.13.1", + "@typescript-eslint/typescript-estree": "7.13.1", + "@typescript-eslint/visitor-keys": "7.13.1", "debug": "^4.3.4" }, "engines": { @@ -2572,13 +2549,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.0.tgz", - "integrity": "sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.1.tgz", + "integrity": "sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.0", - "@typescript-eslint/visitor-keys": "7.13.0" + "@typescript-eslint/types": "7.13.1", + "@typescript-eslint/visitor-keys": "7.13.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2589,13 +2566,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.13.0.tgz", - "integrity": "sha512-xMEtMzxq9eRkZy48XuxlBFzpVMDurUAfDu5Rz16GouAtXm0TaAoTFzqWUFPPuQYXI/CDaH/Bgx/fk/84t/Bc9A==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.13.1.tgz", + "integrity": "sha512-aWDbLu1s9bmgPGXSzNCxELu+0+HQOapV/y+60gPXafR8e2g1Bifxzevaa+4L2ytCWm+CHqpELq4CSoN9ELiwCg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.13.0", - "@typescript-eslint/utils": "7.13.0", + "@typescript-eslint/typescript-estree": "7.13.1", + "@typescript-eslint/utils": "7.13.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2616,9 +2593,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.0.tgz", - "integrity": "sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.1.tgz", + "integrity": "sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2629,13 +2606,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.0.tgz", - "integrity": "sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.1.tgz", + "integrity": "sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.0", - "@typescript-eslint/visitor-keys": "7.13.0", + "@typescript-eslint/types": "7.13.1", + "@typescript-eslint/visitor-keys": "7.13.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2657,15 +2634,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.0.tgz", - "integrity": "sha512-jceD8RgdKORVnB4Y6BqasfIkFhl4pajB1wVxrF4akxD2QPM8GNYjgGwEzYS+437ewlqqrg7Dw+6dhdpjMpeBFQ==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.1.tgz", + "integrity": "sha512-h5MzFBD5a/Gh/fvNdp9pTfqJAbuQC4sCN2WzuXme71lqFJsZtLbjxfSk4r3p02WIArOF9N94pdsLiGutpDbrXQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.13.0", - "@typescript-eslint/types": "7.13.0", - "@typescript-eslint/typescript-estree": "7.13.0" + "@typescript-eslint/scope-manager": "7.13.1", + "@typescript-eslint/types": "7.13.1", + "@typescript-eslint/typescript-estree": "7.13.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2679,12 +2656,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.0.tgz", - "integrity": "sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.1.tgz", + "integrity": "sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.0", + "@typescript-eslint/types": "7.13.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2744,12 +2721,12 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.27.tgz", - "integrity": "sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.29.tgz", + "integrity": "sha512-TFKiRkKKsRCKvg/jTSSKK7mYLJEQdUiUfykbG49rubC9SfDyvT2JrzTReopWlz2MxqeLyxh9UZhvxEIBgAhtrg==", "dependencies": { - "@babel/parser": "^7.24.4", - "@vue/shared": "3.4.27", + "@babel/parser": "^7.24.7", + "@vue/shared": "3.4.29", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" @@ -2761,24 +2738,24 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, "node_modules/@vue/compiler-dom": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz", - "integrity": "sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.29.tgz", + "integrity": "sha512-A6+iZ2fKIEGnfPJejdB7b1FlJzgiD+Y/sxxKwJWg1EbJu6ZPgzaPQQ51ESGNv0CP6jm6Z7/pO6Ia8Ze6IKrX7w==", "dependencies": { - "@vue/compiler-core": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-core": "3.4.29", + "@vue/shared": "3.4.29" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz", - "integrity": "sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==", - "dependencies": { - "@babel/parser": "^7.24.4", - "@vue/compiler-core": "3.4.27", - "@vue/compiler-dom": "3.4.27", - "@vue/compiler-ssr": "3.4.27", - "@vue/shared": "3.4.27", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.29.tgz", + "integrity": "sha512-zygDcEtn8ZimDlrEQyLUovoWgKQic6aEQqRXce2WXBvSeHbEbcAsXyCk9oG33ZkyWH4sl9D3tkYc1idoOkdqZQ==", + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/compiler-core": "3.4.29", + "@vue/compiler-dom": "3.4.29", + "@vue/compiler-ssr": "3.4.29", + "@vue/shared": "3.4.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.10", "postcss": "^8.4.38", @@ -2799,12 +2776,12 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz", - "integrity": "sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.29.tgz", + "integrity": "sha512-rFbwCmxJ16tDp3N8XCx5xSQzjhidYjXllvEcqX/lopkoznlNPz3jyy0WGJCyhAaVQK677WWFt3YO/WUEkMMUFQ==", "dependencies": { - "@vue/compiler-dom": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-dom": "3.4.29", + "@vue/shared": "3.4.29" } }, "node_modules/@vue/devtools-api": { @@ -2874,48 +2851,49 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.27.tgz", - "integrity": "sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.29.tgz", + "integrity": "sha512-w8+KV+mb1a8ornnGQitnMdLfE0kXmteaxLdccm2XwdFxXst4q/Z7SEboCV5SqJNpZbKFeaRBBJBhW24aJyGINg==", "dependencies": { - "@vue/shared": "3.4.27" + "@vue/shared": "3.4.29" } }, "node_modules/@vue/runtime-core": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.27.tgz", - "integrity": "sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.29.tgz", + "integrity": "sha512-s8fmX3YVR/Rk5ig0ic0NuzTNjK2M7iLuVSZyMmCzN/+Mjuqqif1JasCtEtmtoJWF32pAtUjyuT2ljNKNLeOmnQ==", "dependencies": { - "@vue/reactivity": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/reactivity": "3.4.29", + "@vue/shared": "3.4.29" } }, "node_modules/@vue/runtime-dom": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.27.tgz", - "integrity": "sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.29.tgz", + "integrity": "sha512-gI10atCrtOLf/2MPPMM+dpz3NGulo9ZZR9d1dWo4fYvm+xkfvRrw1ZmJ7mkWtiJVXSsdmPbcK1p5dZzOCKDN0g==", "dependencies": { - "@vue/runtime-core": "3.4.27", - "@vue/shared": "3.4.27", + "@vue/reactivity": "3.4.29", + "@vue/runtime-core": "3.4.29", + "@vue/shared": "3.4.29", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.27.tgz", - "integrity": "sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.29.tgz", + "integrity": "sha512-HMLCmPI2j/k8PVkSBysrA2RxcxC5DgBiCdj7n7H2QtR8bQQPqKAe8qoaxLcInzouBmzwJ+J0x20ygN/B5mYBng==", "dependencies": { - "@vue/compiler-ssr": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-ssr": "3.4.29", + "@vue/shared": "3.4.29" }, "peerDependencies": { - "vue": "3.4.27" + "vue": "3.4.29" } }, "node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==" + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==" }, "node_modules/@vue/tsconfig": { "version": "0.5.1", @@ -2924,9 +2902,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -3680,7 +3658,6 @@ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -6057,9 +6034,9 @@ } }, "node_modules/sass": { - "version": "1.77.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.4.tgz", - "integrity": "sha512-vcF3Ckow6g939GMA4PeU7b2K/9FALXk2KF9J87txdHzXbUF9XRQRwSxcAs/fGaTnJeBFd7UoV22j3lzMLdM0Pw==", + "version": "1.77.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", + "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -6643,12 +6620,12 @@ } }, "node_modules/vite": { - "version": "5.2.13", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz", - "integrity": "sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", + "integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==", "dev": true, "dependencies": { - "esbuild": "^0.20.1", + "esbuild": "^0.21.3", "postcss": "^8.4.38", "rollup": "^4.13.0" }, @@ -6697,412 +6674,6 @@ } } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" - } - }, "node_modules/vscode-uri": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", @@ -7110,15 +6681,15 @@ "dev": true }, "node_modules/vue": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.27.tgz", - "integrity": "sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==", - "dependencies": { - "@vue/compiler-dom": "3.4.27", - "@vue/compiler-sfc": "3.4.27", - "@vue/runtime-dom": "3.4.27", - "@vue/server-renderer": "3.4.27", - "@vue/shared": "3.4.27" + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.29.tgz", + "integrity": "sha512-8QUYfRcYzNlYuzKPfge1UWC6nF9ym0lx7mpGVPJYNhddxEf3DD0+kU07NTL0sXuiT2HuJuKr/iEO8WvXvT0RSQ==", + "dependencies": { + "@vue/compiler-dom": "3.4.29", + "@vue/compiler-sfc": "3.4.29", + "@vue/runtime-dom": "3.4.29", + "@vue/server-renderer": "3.4.29", + "@vue/shared": "3.4.29" }, "peerDependencies": { "typescript": "*" diff --git a/package.json b/package.json index fece0f765c7be49bec6b7c3abae08de28887b33c..1d9479a6f5d65fd4741320d34c7233fe5a4f218d 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "rollup-plugin-node-polyfills": "~0.2.1", "sass": "^1.66.0", "typescript": "~5.4.0", - "vite": "~5.2.0", + "vite": "~5.3.0", "vue-tsc": "~2.0.0" } } diff --git a/src/App.vue b/src/App.vue index d48b90e30e90cbab56645584b3f1e2e547849cf8..f679a7aebb1acd2a24d1ef92ab348399f7b1cf3b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -26,28 +26,31 @@ const s3KeyRepository = useS3KeyStore(); onBeforeMount(() => { OpenAPI.BASE = environment.API_BASE_URL; + userRepository.updateJWT(cookies.get("clowm-jwt")); + if (userRepository.authenticated) { + userRepository.getCurrentUser(); + } axios.interceptors.response.use( (res) => res, (err) => { if ( - (err.response.status === 400 || err.response.status === 403) && - err.response.data.detail?.includes("JWT") + err.response.status === 401 || + (err.response.status === 400 && + err.response.data.detail?.includes("JWT")) ) { userRepository.logout(); - cookies.remove("bearer"); router.push({ name: "login", query: { login_error: - err.response.status === 400 ? "token_invalid" : "token_expired", - next: route.name != "login" ? encodeURI(route.path) : undefined, + err.response.status === 401 ? "token_invalid" : "token_expired", + next: route.name !== "login" ? encodeURI(route.path) : undefined, }, }); } return Promise.reject(err); }, ); - userRepository.setToken(cookies.get("bearer")); router.afterEach((to, from) => { window._paq.push(["setReferrerUrl", from.path]); window._paq.push(["deleteCustomVariables", "page"]); @@ -82,7 +85,7 @@ onBeforeMount(() => { return { name: "workflows" }; } else if ( to.meta.requiresReviewerRole && - !(userRepository.rewiewer || userRepository.admin) + !(userRepository.reviewer || userRepository.admin) ) { return "/"; } else if ( diff --git a/src/assets/main.css b/src/assets/main.css index cb6974cba0e2a5c6b64ede79fec9c1befe81c2f9..bdbb663e010b6c5e9b4ab67ecc0de411dbeded73 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -33,3 +33,6 @@ pre { .hover-info:hover { color: var(--bs-info) !important; } +.hover-danger:hover { + color: var(--bs-danger) !important; +} diff --git a/src/client/index.ts b/src/client/index.ts index 4e8c4b36cd6a701bba0ad2f7b46e078f6fcd73e9..98752ac488193a71a44ea94354a30ae46310a955 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -8,6 +8,9 @@ export { OpenAPI } from './core/OpenAPI'; export type { OpenAPIConfig } from './core/OpenAPI'; export type { AnonymizedWorkflowExecution } from './models/AnonymizedWorkflowExecution'; +export type { ApiTokenIn } from './models/ApiTokenIn'; +export type { ApiTokenOut } from './models/ApiTokenOut'; +export type { ApiTokenPrivateOut } from './models/ApiTokenPrivateOut'; export type { Body_Bucket_update_bucket_public_state } from './models/Body_Bucket_update_bucket_public_state'; export type { Body_Workflow_Version_upload_workflow_version_icon } from './models/Body_Workflow_Version_upload_workflow_version_icon'; export type { BucketIn } from './models/BucketIn'; @@ -34,6 +37,7 @@ export type { ResourceVersionOut } from './models/ResourceVersionOut'; export { ResourceVersionStatus } from './models/ResourceVersionStatus'; export { RoleEnum } from './models/RoleEnum'; export type { S3Key } from './models/S3Key'; +export { ScopeEnum } from './models/ScopeEnum'; export type { UserIn } from './models/UserIn'; export type { UserOut } from './models/UserOut'; export type { UserOutExtended } from './models/UserOutExtended'; @@ -57,6 +61,7 @@ export type { WorkflowVersion } from './models/WorkflowVersion'; export { WorkflowVersionStatus } from './models/WorkflowVersionStatus'; export type { WorkflowVersionStatusSchema } from './models/WorkflowVersionStatusSchema'; +export { ApiTokenService } from './services/ApiTokenService'; export { AuthService } from './services/AuthService'; export { BucketService } from './services/BucketService'; export { BucketPermissionService } from './services/BucketPermissionService'; diff --git a/src/client/models/ApiTokenIn.ts b/src/client/models/ApiTokenIn.ts new file mode 100644 index 0000000000000000000000000000000000000000..38a68cae7c7cb1a5d59cc3983b88d7442ba74ffc --- /dev/null +++ b/src/client/models/ApiTokenIn.ts @@ -0,0 +1,20 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ScopeEnum } from './ScopeEnum'; +export type ApiTokenIn = { + /** + * Short name for the API token + */ + name: string; + /** + * Unix timestamp when the token should expire + */ + expires_at?: (number | null); + /** + * List of scopes this Api token has + */ + scopes: Array<ScopeEnum>; +}; + diff --git a/src/client/models/ApiTokenOut.ts b/src/client/models/ApiTokenOut.ts new file mode 100644 index 0000000000000000000000000000000000000000..9935a1280b6382682564752519c431ed19c0068f --- /dev/null +++ b/src/client/models/ApiTokenOut.ts @@ -0,0 +1,36 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ScopeEnum } from './ScopeEnum'; +export type ApiTokenOut = { + /** + * Short name for the API token + */ + name: string; + /** + * Unix timestamp when the token should expire + */ + expires_at?: (number | null); + /** + * List of scopes this Api token has + */ + scopes: Array<ScopeEnum>; + /** + * The ID of the token + */ + token_id: string; + /** + * The ID of the owner + */ + uid: string; + /** + * The UNIX timestamp when this token was created + */ + created_at: number; + /** + * The UNIX timestamp when this token was used the last time + */ + last_used?: (number | null); +}; + diff --git a/src/client/models/ApiTokenPrivateOut.ts b/src/client/models/ApiTokenPrivateOut.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec2b60922e80a0eef21707eaec4e9c6f3e342cb0 --- /dev/null +++ b/src/client/models/ApiTokenPrivateOut.ts @@ -0,0 +1,40 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ScopeEnum } from './ScopeEnum'; +export type ApiTokenPrivateOut = { + /** + * Short name for the API token + */ + name: string; + /** + * Unix timestamp when the token should expire + */ + expires_at?: (number | null); + /** + * List of scopes this Api token has + */ + scopes: Array<ScopeEnum>; + /** + * The ID of the token + */ + token_id: string; + /** + * The ID of the owner + */ + uid: string; + /** + * The UNIX timestamp when this token was created + */ + created_at: number; + /** + * The UNIX timestamp when this token was used the last time + */ + last_used?: (number | null); + /** + * The actual token used for authentication + */ + token: string; +}; + diff --git a/src/client/models/ScopeEnum.ts b/src/client/models/ScopeEnum.ts new file mode 100644 index 0000000000000000000000000000000000000000..b68957318bf3c25a225479669b054b9e74c61103 --- /dev/null +++ b/src/client/models/ScopeEnum.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export enum ScopeEnum { + READ = 'read', + WRITE = 'write', +} diff --git a/src/client/models/UserOutExtended.ts b/src/client/models/UserOutExtended.ts index 4b0aa20a5c17d903740c1b1cfef598e178b62337..a05ecb616dfa92c76dde11b89fa03407d39cd2dd 100644 --- a/src/client/models/UserOutExtended.ts +++ b/src/client/models/UserOutExtended.ts @@ -24,5 +24,9 @@ export type UserOutExtended = { * Timestamp when the invitation token was created as UNIX timestamp */ invitation_token_created_at?: (number | null); + /** + * URL to the gravatar avatar based on the users email + */ + gravatar_url?: (string | null); }; diff --git a/src/client/services/ApiTokenService.ts b/src/client/services/ApiTokenService.ts new file mode 100644 index 0000000000000000000000000000000000000000..a088864e6bfe05b57cc85b40635d80da8ed11188 --- /dev/null +++ b/src/client/services/ApiTokenService.ts @@ -0,0 +1,121 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ApiTokenIn } from '../models/ApiTokenIn'; +import type { ApiTokenOut } from '../models/ApiTokenOut'; +import type { ApiTokenPrivateOut } from '../models/ApiTokenPrivateOut'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class ApiTokenService { + /** + * List API token + * List meta information about all API token. + * + * Permissions `api_token:list_all` required. See parameter `uid` for exception. + * @param uid UID of the user to filter for. Permission `api_token:list` required if current users is the target. + * @returns ApiTokenOut Successful Response + * @throws ApiError + */ + public static apiTokenListToken( + uid?: string, + ): CancelablePromise<Array<ApiTokenOut>> { + return __request(OpenAPI, { + method: 'GET', + url: '/tokens', + query: { + 'uid': uid, + }, + errors: { + 400: `Error decoding JWT Token`, + 401: `Not Authenticated`, + 403: `Not Authorized`, + 404: `Entity not Found`, + 422: `Validation Error`, + }, + }); + } + /** + * Create new API token + * Create a new API token for the current user. + * + * Permission `api_token:create` required. + * @param requestBody + * @returns ApiTokenPrivateOut Successful Response + * @throws ApiError + */ + public static apiTokenCreateToken( + requestBody: ApiTokenIn, + ): CancelablePromise<ApiTokenPrivateOut> { + return __request(OpenAPI, { + method: 'POST', + url: '/tokens', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error decoding JWT Token`, + 401: `Not Authenticated`, + 403: `Not Authorized`, + 404: `Entity not Found`, + 422: `Validation Error`, + }, + }); + } + /** + * Get API token + * Get an API token by id. + * + * Permission `api_token:read` required if the current user is the owner of the API token, + * otherwise `api_token:read_any` required. + * @param tid ID of an API token + * @returns ApiTokenOut Successful Response + * @throws ApiError + */ + public static apiTokenGetToken( + tid: string, + ): CancelablePromise<ApiTokenOut> { + return __request(OpenAPI, { + method: 'GET', + url: '/tokens/{tid}', + path: { + 'tid': tid, + }, + errors: { + 400: `Error decoding JWT Token`, + 401: `Not Authenticated`, + 403: `Not Authorized`, + 404: `Entity not Found`, + 422: `Validation Error`, + }, + }); + } + /** + * Delete API token + * Delete an API token by id. + * + * Permission `api_token:delete` required if the current user is the owner of the API token, + * otherwise `api_token:delete_any` required. + * @param tid ID of an API token + * @returns void + * @throws ApiError + */ + public static apiTokenDeleteToken( + tid: string, + ): CancelablePromise<void> { + return __request(OpenAPI, { + method: 'DELETE', + url: '/tokens/{tid}', + path: { + 'tid': tid, + }, + errors: { + 400: `Error decoding JWT Token`, + 401: `Not Authenticated`, + 403: `Not Authorized`, + 404: `Entity not Found`, + 422: `Validation Error`, + }, + }); + } +} diff --git a/src/client/services/WorkflowCredentialsService.ts b/src/client/services/WorkflowCredentialsService.ts index 4d3b74cb483295567066b646fbc2e683753c0e6e..01e4e44bec76bf5ee746fa2d9edfab0e5f49a688 100644 --- a/src/client/services/WorkflowCredentialsService.ts +++ b/src/client/services/WorkflowCredentialsService.ts @@ -70,7 +70,7 @@ export class WorkflowCredentialsService { * Delete the credentials of a workflow * Delete the credentials for the repository of a workflow. * - * Permission `workflow:delete` required if the developer of the workflow is the same as the the current user, + * Permission `workflow:delete` required if the developer of the workflow is the same as the current user, * other `workflow:delete_any`. * @param wid ID of a workflow * @returns void diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index e8799490aeb9b59ad9ce9dc4575ab055f0b2012b..331c790cda5193e6e020161ee04d0035325acada 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -1,18 +1,16 @@ <script setup lang="ts"> -import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import { useUserStore } from "@/stores/users"; -import { useRoute } from "vue-router"; +import { useRoute, useRouter } from "vue-router"; import { watch, ref, computed } from "vue"; -import BootstrapModal from "@/components/modals/BootstrapModal.vue"; import { OpenAPI } from "@/client"; -import CopyToClipboardIcon from "@/components/CopyToClipboardIcon.vue"; -import dayjs from "dayjs"; const userRepository = useUserStore(); const route = useRoute(); +const router = useRouter(); function logout() { userRepository.logout(); + router.push({ name: "login" }); } const activeRoute = ref(""); @@ -136,7 +134,7 @@ watch( <li v-if=" userRepository.workflowDev || - userRepository.rewiewer || + userRepository.reviewer || userRepository.admin " > @@ -149,7 +147,7 @@ watch( >My Workflows </router-link> </li> - <li v-if="userRepository.rewiewer || userRepository.admin"> + <li v-if="userRepository.reviewer || userRepository.admin"> <router-link class="dropdown-item" :to="{ name: 'workflows-reviewer' }" @@ -183,7 +181,7 @@ watch( <li v-if=" userRepository.resourceMaintainer || - userRepository.rewiewer || + userRepository.reviewer || userRepository.admin " > @@ -198,7 +196,7 @@ watch( >My Resources </router-link> </li> - <li v-if="userRepository.rewiewer || userRepository.admin"> + <li v-if="userRepository.reviewer || userRepository.admin"> <router-link class="dropdown-item" :to="{ name: 'resource-review' }" @@ -265,21 +263,23 @@ watch( data-bs-toggle="dropdown" aria-expanded="false" > - <strong class="me-2">{{ userRepository.user?.display_name }}</strong> - <font-awesome-icon icon="fa-solid fa-circle-user" class="fs-5" /> + <strong class="me-2">{{ userRepository.user.display_name }}</strong> + <img + :src="userRepository.user.gravatar_url + '?d=mp&s=32'" + class="rounded-circle" + height="32" + width="32" + alt="profile picture" + /> </a> <ul class="dropdown-menu text-small shadow" aria-labelledby="dropdownUser1" > <li> - <a - href="#" - class="dropdown-item" - data-bs-toggle="modal" - data-bs-target="#advancedUsageModal" - >Advanced Usage</a - > + <router-link class="dropdown-item" :to="{ name: 'api-tokens' }" + >API tokens + </router-link> </li> <li> <hr class="dropdown-divider" /> @@ -307,62 +307,6 @@ watch( </div> </nav> </header> - <bootstrap-modal - modal-id="advancedUsageModal" - modal-label="Advanced Usage Modal" - v-if="userRepository.authenticated" - size-modifier-modal="lg" - > - <template v-slot:header> - <h3>Advanced Usage</h3> - </template> - <template v-slot:body> - <table class="table table-borderless table-sm"> - <tbody> - <tr> - <td class="text-end">API:</td> - <td> - <a :href="OpenAPI.BASE + '/docs'" target="_blank"> - <font-awesome-icon - class="me-1" - icon="fa-solid fa-arrow-up-right-from-square" - /> - {{ OpenAPI.BASE }}</a - > - </td> - </tr> - </tbody> - </table> - <div class="mt-4"> - <label for="clowm-jwt" class="form-label" - >JWT for Services (expires at - {{ - dayjs - .unix(userRepository.decodedToken!.exp) - .format("DD.MM.YYYY HH:mm") - }})</label - > - <div class="input-group"> - <input - type="text" - readonly - class="form-control text-truncate" - id="clowm-jwt" - :value="userRepository.token" - aria-describedby="clowm-jwt-copy" - /> - <span class="input-group-text" id="clowm-jwt-copy" - ><copy-to-clipboard-icon :text="userRepository.token ?? ''" - /></span> - </div> - </div> - </template> - <template v-slot:footer> - <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> - Close - </button> - </template> - </bootstrap-modal> </template> <style scoped> diff --git a/src/components/user/CreateApiTokenModal.vue b/src/components/user/CreateApiTokenModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..4304fdcfea040beafe05474ff491755f76c21ccb --- /dev/null +++ b/src/components/user/CreateApiTokenModal.vue @@ -0,0 +1,192 @@ +<script setup lang="ts"> +import { onMounted, reactive, ref } from "vue"; +import { type ApiTokenIn, type ApiTokenPrivateOut, ScopeEnum } from "@/client"; +import { Modal } from "bootstrap"; +import BootstrapModal from "@/components/modals/BootstrapModal.vue"; +import dayjs from "dayjs"; +import { useUserStore } from "@/stores/users"; +import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; + +const props = defineProps<{ + modalId: string; +}>(); + +const tokenIn = reactive<ApiTokenIn>({ + name: "", + expires_at: undefined, + scopes: [], +}); + +const formState = reactive<{ + validated: boolean; + loading: boolean; +}>({ + validated: false, + loading: false, +}); + +const userRepository = useUserStore(); +const tokenCreateForm = ref<HTMLFormElement | undefined>(undefined); +let createTokenModal: Modal | null = null; + +const scopeDescription: Record<ScopeEnum, string> = { + read: "Grants read access to the CloWM API. All GET requests are allowed with this scope.", + write: + "Grants write access to the CloWM API. All POST, DELETE and PUT requests are allowed with this scope.", +}; + +const emit = defineEmits<{ + (e: "token-created", token: ApiTokenPrivateOut): void; +}>(); + +function modalClosed() { + formState.validated = false; +} + +function expiresAtTimestampChanged(target?: HTMLInputElement | null) { + tokenIn.expires_at = target?.value ? dayjs(target?.value).unix() : undefined; +} + +function createToken() { + formState.validated = true; + if (tokenCreateForm.value?.checkValidity()) { + formState.loading = true; + userRepository + .createApiToken(tokenIn) + .then((token) => { + emit("token-created", token); + createTokenModal?.hide(); + tokenIn.expires_at = undefined; + tokenIn.name = ""; + tokenIn.scopes = []; + }) + .finally(() => { + formState.loading = false; + }); + } +} + +onMounted(() => { + createTokenModal = new Modal("#" + props.modalId); +}); +</script> + +<template> + <bootstrap-modal + :modalId="modalId" + static-backdrop + modal-label="Create API Token Modal" + v-on="{ 'hidden.bs.modal': modalClosed }" + size-modifier-modal="lg" + > + <template #header> Create personal API token</template> + <template #body> + <form + id="api-token-create-form" + :class="{ 'was-validated': formState.validated }" + ref="tokenCreateForm" + @submit.prevent="createToken" + > + <div class="mb-3"> + <label for="token-name-input" class="form-label">Token name</label> + <input + type="text" + class="form-control" + id="token-name-input" + required + minlength="3" + maxlength="63" + pattern="^[a-z\d\-]+$" + v-model="tokenIn.name" + /> + <div class="invalid-feedback"> + <div> + Name must ... + <ul> + <li>be at least 3 Characters long</li> + <li>contains the characters a-z 0-9 and /</li> + </ul> + </div> + </div> + </div> + <div class="mb-3"> + <label for="expires-at-input" class="form-label" + >Expiration date</label + > + <div :class="{ 'input-group': tokenIn.expires_at != undefined }"> + <input + class="form-control" + id="expires-at-input" + type="date" + :min="dayjs().add(1, 'day').format('YYYY-MM-DD')" + :value=" + tokenIn.expires_at + ? dayjs.unix(tokenIn.expires_at).format('YYYY-MM-DD') + : undefined + " + @input=" + (event) => + expiresAtTimestampChanged(event.target as HTMLInputElement) + " + /> + <div + v-if="tokenIn.expires_at" + class="input-group-text cursor-pointer hover-danger" + @click="tokenIn.expires_at = undefined" + > + <font-awesome-icon icon="fa-solid fa-xmark" class="fs-5" /> + </div> + </div> + </div> + <div class="mb-3"> + <div>Select scopes</div> + <div + v-for="scope in Object.values(ScopeEnum)" + :key="scope" + class="form-check mb-2" + > + <input + class="form-check-input" + type="checkbox" + :value="scope" + :id="`scope-input-${scope}`" + v-model="tokenIn.scopes" + /> + <label + class="form-check-label pt-1" + style="line-height: 0.8rem" + :for="`scope-input-${scope}`" + > + <span>{{ scope.toLowerCase() }}</span> + <br /> + <span class="text-secondary" style="font-size: 0.8rem"> + {{ scopeDescription[scope] }} + </span> + </label> + </div> + </div> + </form> + </template> + <template v-slot:footer> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> + Close + </button> + <button + type="submit" + form="api-token-create-form" + class="btn btn-primary" + :disabled="formState.loading || tokenIn.scopes.length === 0" + > + <span + v-if="formState.loading" + class="spinner-border spinner-border-sm" + role="status" + aria-hidden="true" + ></span> + Save + </button> + </template> + </bootstrap-modal> +</template> + +<style scoped></style> diff --git a/src/components/workflows/modals/ArbitraryWorkflowModal.vue b/src/components/workflows/modals/ArbitraryWorkflowModal.vue index c3c4a7aecaa7bc17b30a098425cdb829ed9b9be3..cff6610c9d2cf0930ce31d2a8028bbd9726db9f9 100644 --- a/src/components/workflows/modals/ArbitraryWorkflowModal.vue +++ b/src/components/workflows/modals/ArbitraryWorkflowModal.vue @@ -15,6 +15,7 @@ import { environment } from "@/environment"; import { useS3KeyStore } from "@/stores/s3keys"; import { useResourceStore } from "@/stores/resources"; import { createDownloadUrl } from "@/utils/DownloadJson"; +import dayjs from "dayjs"; const props = defineProps<{ modalId: string; @@ -72,6 +73,7 @@ const formState = reactive<{ allowUpload: boolean; missingFiles: string[]; unsupportedRepository: boolean; + ratelimit_reset: number; }>({ validated: false, allowUpload: false, @@ -79,6 +81,7 @@ const formState = reactive<{ checkRepoLoading: false, missingFiles: [], unsupportedRepository: false, + ratelimit_reset: 0, }); watch( @@ -178,6 +181,7 @@ docker { } // Disable unwanted features +conda.enabled = False weblog.enabled = false shifter.enabled = false singularity.enabled = false @@ -299,6 +303,7 @@ docker { } // Disable unwanted features +conda.enabled = False tower.enabled = false weblog.enabled = false shifter.enabled = false @@ -329,6 +334,7 @@ function checkRepository() { workflow.repository_url = workflow.repository_url .trim() .replace(/(^\/+|\/+$)/g, ""); + formState.ratelimit_reset = 0; if (arbitraryWorkflowForm.value?.checkValidity() && !formState.allowUpload) { formState.unsupportedRepository = false; formState.missingFiles = []; @@ -352,14 +358,19 @@ function checkRepository() { formState.allowUpload = true; }) .catch((e: Error) => { - formState.missingFiles = e.message.split(","); - // Allow execution of the workflow if main.nf and parameter schema are not missing - if ( - formState.missingFiles.findIndex( - (file) => file === "main.nf" || file === "nextflow_schema.json", - ) < 0 - ) { - formState.allowUpload = true; + try { + const headers = JSON.parse(e.message); + formState.ratelimit_reset = parseInt(headers["x-ratelimit-reset"]); + } catch { + formState.missingFiles = e.message.split(","); + // Allow execution of the workflow if main.nf and parameter schema are not missing + if ( + formState.missingFiles.findIndex( + (file) => file === "main.nf" || file === "nextflow_schema.json", + ) < 0 + ) { + formState.allowUpload = true; + } } }); } catch (e) { @@ -518,13 +529,25 @@ onMounted(() => { ref="workflowGitCommitHashElement" maxlength="40" minlength="40" - pattern="[0-9a-f]{40}" + pattern="^[0-9a-f]$" v-model="workflow.git_commit_hash" @change="formState.allowUpload = false" /> </div> </div> - <div v-if="formState.missingFiles.length > 0" class="text-danger"> + <div v-if="formState.ratelimit_reset > 0" class="text-danger"> + Can't check GitHub repository because the default + <a + href="https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#primary-rate-limit-for-unauthenticated-users" + target="_blank" + >rate-limit</a + > + for your IP address was exhausted. You could mark this repository as + private repository to bypass this constraint. <br /> + Rate-limit resets + {{ dayjs.unix(formState.ratelimit_reset).fromNow() }} + </div> + <div v-else-if="formState.missingFiles.length > 0" class="text-danger"> The following files are missing in the repository <ul> <li v-for="file in formState.missingFiles" :key="file"> diff --git a/src/components/workflows/modals/CreateWorkflowModal.vue b/src/components/workflows/modals/CreateWorkflowModal.vue index 5eb0dd416bcad9e74601a3c1a3c1c1ceeab9c223..67905a071eaa14a144b12dcab309b88532e6329b 100644 --- a/src/components/workflows/modals/CreateWorkflowModal.vue +++ b/src/components/workflows/modals/CreateWorkflowModal.vue @@ -14,6 +14,7 @@ import { valid } from "semver"; import WorkflowModeTransitionGroup from "@/components/transitions/WorkflowModeTransitionGroup.vue"; import { useWorkflowStore } from "@/stores/workflows"; import BootstrapToast from "@/components/BootstrapToast.vue"; +import dayjs from "dayjs"; const workflowRepository = useWorkflowStore(); // Emitted Events @@ -69,6 +70,7 @@ const formState = reactive<{ allowUpload: boolean; missingFiles: string[]; unsupportedRepository: boolean; + ratelimit_reset: number; }>({ validated: false, allowUpload: false, @@ -76,6 +78,7 @@ const formState = reactive<{ checkRepoLoading: false, missingFiles: [], unsupportedRepository: false, + ratelimit_reset: 0, }); const repositoryCredentials = reactive<{ @@ -251,10 +254,15 @@ function checkRepository() { formState.allowUpload = true; }) .catch((e: Error) => { - formState.missingFiles = e.message.split(","); - workflowGitCommitHashElement.value?.setCustomValidity( - "Files are missing in the repository", - ); + try { + const headers = JSON.parse(e.message); + formState.ratelimit_reset = parseInt(headers["x-ratelimit-reset"]); + } catch { + formState.missingFiles = e.message.split(","); + workflowGitCommitHashElement.value?.setCustomValidity( + "Files are missing in the repository", + ); + } }); } catch (e) { formState.unsupportedRepository = true; @@ -328,6 +336,7 @@ onMounted(() => { :static-backdrop="true" modal-label="Create Workflow Modal" v-on="{ 'hidden.bs.modal': modalClosed }" + size-modifier-modal="lg" > <template #header> Create new Workflow</template> <template #body> @@ -426,7 +435,8 @@ onMounted(() => { required ref="workflowGitCommitHashElement" maxlength="40" - pattern="[0-9a-f]{40}" + minlength="40" + pattern="^[0-9a-f]$" v-model="workflow.git_commit_hash" @change="formState.allowUpload = false" /> @@ -439,7 +449,21 @@ onMounted(() => { <font-awesome-icon icon="fa-solid fa-circle-question" /> </div> </div> - <div v-if="formState.missingFiles.length > 0" class="text-danger"> + <div v-if="formState.ratelimit_reset > 0" class="text-danger"> + Can't check GitHub repository because the default + <a + href="https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#primary-rate-limit-for-unauthenticated-users" + target="_blank" + >rate-limit</a + > + for your IP address was exhausted <br /> + Rate-limit resets + {{ dayjs.unix(formState.ratelimit_reset).fromNow() }} + </div> + <div + v-else-if="formState.missingFiles.length > 0" + class="text-danger" + > The following files are missing in the repository <ul> <li v-for="file in formState.missingFiles" :key="file"> diff --git a/src/components/workflows/modals/UpdateWorkflowCredentialsModal.vue b/src/components/workflows/modals/UpdateWorkflowCredentialsModal.vue index 27ccc333dbe71c7fe47906c5359eaeb6037f81b6..e49fbc3081cbf310b2ea94e82a6c1da400f15c30 100644 --- a/src/components/workflows/modals/UpdateWorkflowCredentialsModal.vue +++ b/src/components/workflows/modals/UpdateWorkflowCredentialsModal.vue @@ -72,30 +72,31 @@ function updateCredentials() { props.workflow.versions[0].workflow_version_id, credentials.token, ); - repo.checkFileExist("main.nf").then((result: boolean) => { - if (result) { - workflowRepository - .updateWorkflowCredentials(props.workflow.workflow_id, credentials) - .then(() => { - formState.updateCredentials = true; - successToast?.show(); - updateCredentialsModal?.hide(); - }) - .catch((error) => { - console.error(error); - }) - .finally(() => { - formState.loading = false; - }); - } else { - formState.error = true; - credentialsInputElement.value?.setCustomValidity( - "Can't access repository.", - ); - formState.loading = false; - } - }); formState.loading = true; + repo + .checkFileExist("main.nf") + .then((result: boolean) => { + if (result) { + workflowRepository + .updateWorkflowCredentials(props.workflow.workflow_id, credentials) + .then(() => { + formState.updateCredentials = true; + successToast?.show(); + updateCredentialsModal?.hide(); + }) + .catch((error) => { + console.error(error); + }); + } else { + formState.error = true; + credentialsInputElement.value?.setCustomValidity( + "Can't access repository.", + ); + } + }) + .finally(() => { + formState.loading = false; + }); } } diff --git a/src/components/workflows/modals/UpdateWorkflowModal.vue b/src/components/workflows/modals/UpdateWorkflowModal.vue index ed2af5e554b6dd89c81c6b1e9fdbaa3570aada61..50814482608398fe2fc724f4df6c6474ecf0faee 100644 --- a/src/components/workflows/modals/UpdateWorkflowModal.vue +++ b/src/components/workflows/modals/UpdateWorkflowModal.vue @@ -22,6 +22,7 @@ import { latestVersion as calculateLatestVersion } from "@/utils/Workflow"; import WorkflowModeTransitionGroup from "@/components/transitions/WorkflowModeTransitionGroup.vue"; import { useWorkflowStore } from "@/stores/workflows"; import BootstrapToast from "@/components/BootstrapToast.vue"; +import dayjs from "dayjs"; const workflowRepository = useWorkflowStore(); // Bootstrap Elements @@ -77,6 +78,7 @@ const formState = reactive<{ loadCredentials: boolean; workflowToken?: string; modesEnabled: boolean; + ratelimit_reset: number; }>({ loading: false, checkRepoLoading: false, @@ -86,6 +88,7 @@ const formState = reactive<{ loadCredentials: false, workflowToken: undefined, modesEnabled: false, + ratelimit_reset: 0, }); watch( @@ -193,10 +196,15 @@ function checkRepository() { formState.allowUpload = true; }) .catch((e: Error) => { - formState.missingFiles = e.message.split(","); - workflowGitCommitHashElement.value?.setCustomValidity( - "Files are missing in the repository", - ); + try { + const headers = JSON.parse(e.message); + formState.ratelimit_reset = parseInt(headers["x-ratelimit-reset"]); + } catch { + formState.missingFiles = e.message.split(","); + workflowGitCommitHashElement.value?.setCustomValidity( + "Files are missing in the repository", + ); + } }); } } @@ -368,12 +376,27 @@ onMounted(() => { required ref="workflowGitCommitHashElement" maxlength="40" - pattern="[0-9a-f]{40}" + minlength="40" + pattern="^[0-9a-f]$" v-model="workflowUpdate.git_commit_hash" @change="formState.allowUpload = false" /> </div> - <div v-if="formState.missingFiles.length > 0" class="text-danger"> + <div v-if="formState.ratelimit_reset > 0" class="text-danger"> + Can't check GitHub repository because the default + <a + href="https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#primary-rate-limit-for-unauthenticated-users" + target="_blank" + >rate-limit</a + > + for your IP address was exhausted <br /> + Rate-limit resets + {{ dayjs.unix(formState.ratelimit_reset).fromNow() }} + </div> + <div + v-else-if="formState.missingFiles.length > 0" + class="text-danger" + > The following files are missing in the repository <ul> <li v-for="file in formState.missingFiles" :key="file"> diff --git a/src/router/index.ts b/src/router/index.ts index a6397578765f5c79e56599db5439a329a4a84d6d..227a77970998e57ee73327f430f3a9c858d998b7 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -5,6 +5,7 @@ import { workflowRoutes } from "@/router/workflowRoutes"; import { s3Routes } from "@/router/s3Routes"; import { resourceRoutes } from "@/router/resourceRoutes"; import { adminRoutes } from "@/router/adminRoutes"; +import { userRoutes } from "@/router/userRoutes"; import ImprintView from "@/views/ImprintView.vue"; import PrivacyPolicyView from "@/views/PrivacyPolicyView.vue"; import TermsOfUsageView from "@/views/TermsOfUsageView.vue"; @@ -21,6 +22,7 @@ const router = createRouter({ ...s3Routes, ...workflowRoutes, ...adminRoutes, + ...userRoutes, ], }, { diff --git a/src/router/userRoutes.ts b/src/router/userRoutes.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ca6ca2e9ffa57e212fa8f8c2fa1f8362d269c73 --- /dev/null +++ b/src/router/userRoutes.ts @@ -0,0 +1,12 @@ +import type { RouteRecordRaw } from "vue-router"; + +export const userRoutes: RouteRecordRaw[] = [ + { + path: "api-tokens", + name: "api-tokens", + component: () => import("../views/user/ListApiTokenView.vue"), + meta: { + title: "API Tokens", + }, + }, +]; diff --git a/src/stores/users.ts b/src/stores/users.ts index 259749fe1e00891ec6a8f38397f7680ee29609ba..fd2581db96e904071eec41219b99d2373feac51f 100644 --- a/src/stores/users.ts +++ b/src/stores/users.ts @@ -1,6 +1,13 @@ import { defineStore } from "pinia"; -import type { UserOutExtended, UserOut, UserIn } from "@/client"; -import { OpenAPI, UserService, RoleEnum } from "@/client"; +import type { + UserOutExtended, + UserOut, + UserIn, + ApiTokenOut, + ApiTokenIn, + ApiTokenPrivateOut, +} from "@/client"; +import { UserService, RoleEnum, ApiTokenService } from "@/client"; import { useWorkflowExecutionStore } from "@/stores/workflowExecutions"; import { useBucketStore } from "@/stores/buckets"; import { useWorkflowStore } from "@/stores/workflows"; @@ -35,56 +42,69 @@ export const useUserStore = defineStore({ id: "user", state: () => ({ - token: null, - decodedToken: null, user: null, + jwt: null, + roles: [], + apiTokensMapping: {}, }) as { - token: string | null; - decodedToken: DecodedToken | null; user: UserOutExtended | null; + jwt: DecodedToken | null; + roles: RoleEnum[]; + apiTokensMapping: Record<string, ApiTokenOut>; }, getters: { roles(): string[] { - return this.user?.roles?.map((role) => role.toString()) ?? []; + return ( + this.user?.roles?.map((role) => role.toString()) ?? + JSON.parse(localStorage.getItem("roles") ?? "[]") + ); + }, + apiTokens(): ApiTokenOut[] { + return Object.values(this.apiTokensMapping); + }, + authenticated(): boolean { + return this.jwt != null || this.user != null; }, - authenticated: (state) => state.token != null, currentUID(): string { - return this.user?.uid ?? this.decodedToken?.["sub"] ?? ""; - }, - foreignUser: (state) => (state.user?.roles ?? []).length == 0, - normalUser: (state) => state.user?.roles.includes(RoleEnum.USER) ?? false, - rewiewer: (state) => state.user?.roles.includes(RoleEnum.REVIEWER) ?? false, - workflowDev: (state) => - state.user?.roles.includes(RoleEnum.DEVELOPER) ?? false, - resourceMaintainer: (state) => - state.user?.roles.includes(RoleEnum.DB_MAINTAINER) ?? false, - admin: (state) => - state.user?.roles.includes(RoleEnum.ADMINISTRATOR) ?? false, + return this.user?.uid ?? this.jwt?.sub ?? ""; + }, + foreignUser(): boolean { + return this.roles.length === 0; + }, + normalUser(): boolean { + return this.roles.includes(RoleEnum.USER); + }, + reviewer(): boolean { + return this.roles.includes(RoleEnum.REVIEWER); + }, + workflowDev(): boolean { + return this.roles.includes(RoleEnum.DEVELOPER); + }, + resourceMaintainer(): boolean { + return this.roles.includes(RoleEnum.DB_MAINTAINER); + }, + admin(): boolean { + return this.roles.includes(RoleEnum.ADMINISTRATOR); + }, }, actions: { - setToken(token: string | null) { - if (token != null) { - this.token = token; - this.decodedToken = parseJwt(token); - OpenAPI.TOKEN = token; - UserService.userGetLoggedInUser() - .then((user) => { - this.updateUser(user); - }) - .catch(() => { - this.token = null; - }); - } else { - this.logout(); - } + getCurrentUser(): Promise<UserOutExtended> { + return UserService.userGetLoggedInUser().then((user) => { + this.updateUser(user); + return user; + }); + }, + updateJWT(jwt: string | null) { + this.jwt = jwt != null ? parseJwt(jwt) : null; }, updateUser(user: UserOutExtended) { this.user = user; + localStorage.setItem("currentUser", user.uid); + localStorage.setItem("roles", JSON.stringify(user.roles)); useNameStore().addNameToMapping(user.uid, user.display_name); }, logout() { window._paq.push(["resetUserId"]); - OpenAPI.TOKEN = undefined; this.$reset(); localStorage.clear(); dbclear(); @@ -108,6 +128,33 @@ export const useUserStore = defineStore({ }, ); }, + fetchApiTokens( + uid?: string, + onFinally?: () => void, + ): Promise<ApiTokenOut[]> { + if (this.apiTokens.length > 0) { + onFinally?.(); + } + return ApiTokenService.apiTokenListToken(uid) + .then((tokens) => { + for (const token of tokens) { + this.apiTokensMapping[token.token_id] = token; + } + return tokens; + }) + .finally(onFinally); + }, + createApiToken(tokenIn: ApiTokenIn): Promise<ApiTokenPrivateOut> { + return ApiTokenService.apiTokenCreateToken(tokenIn).then((token) => { + this.apiTokensMapping[token.token_id] = token as ApiTokenOut; + return token; + }); + }, + deleteApiToken(tokenId: string): Promise<void> { + return ApiTokenService.apiTokenDeleteToken(tokenId).then(() => { + delete this.apiTokensMapping[tokenId]; + }); + }, updateUserRoles(uid: string, roles: RoleEnum[]): Promise<UserOutExtended> { return UserService.userUpdateRoles(uid, { roles: roles, diff --git a/src/utils/GitRepository.ts b/src/utils/GitRepository.ts index 5953f28b25ee9140d213264efb3cb4e04fefd1c1..1ab756513b1f92f126b345113d91d044cc27506f 100644 --- a/src/utils/GitRepository.ts +++ b/src/utils/GitRepository.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios, { AxiosError } from "axios"; import type { AxiosInstance, AxiosBasicCredentials } from "axios"; import type { WorkflowModeOut, WorkflowModeIn } from "@/client"; @@ -57,8 +57,24 @@ export abstract class GitRepository { async checkFileExist(filepath: string): Promise<boolean> { try { - await this.httpClient.head(this.fileUrl(filepath)); + await this.httpClient.get(this.fileUrl(filepath)); } catch (e) { + if ( + e instanceof AxiosError && + e.response != undefined && + parseInt(e.response?.headers["x-ratelimit-remaining"] ?? "0") == 0 + ) { + const filtered: Record<string, string> = Object.keys(e.response.headers) + .filter((key) => key.startsWith("x-")) + .reduce( + (obj, key) => { + obj[key] = e.response?.headers[key] ?? ""; + return obj; + }, + {} as Record<string, string>, + ); + throw new Error(JSON.stringify(filtered)); + } return false; } return true; @@ -69,6 +85,23 @@ export abstract class GitRepository { try { return await this.httpClient.get(await this.downloadFileUrl(filepath)); } catch (e) { + if ( + e instanceof AxiosError && + e.response != undefined && + parseInt(e.response.headers["x-ratelimit-remaining"] ?? "1") == 0 + ) { + //console.log(parseInt(e.response.headers["x-ratelimit-reset"] ?? "0")); + const filtered: Record<string, string> = Object.keys(e.response.headers) + .filter((key) => key.startsWith("x-")) + .reduce( + (obj, key) => { + obj[key] = e.response?.headers[key] ?? ""; + return obj; + }, + {} as Record<string, string>, + ); + throw new Error(JSON.stringify(filtered)); + } return ""; } } diff --git a/src/views/admin/AdminUsersView.vue b/src/views/admin/AdminUsersView.vue index 378cee0370ee0a909a7d0bc3712e42628b772048..693fed2781f04184a059112de4b6d6a43b963590 100644 --- a/src/views/admin/AdminUsersView.vue +++ b/src/views/admin/AdminUsersView.vue @@ -211,7 +211,15 @@ onMounted(() => { </tbody> <tbody v-else> <tr v-for="(user, index) in userState.users" :key="user.uid"> - <th scope="row">{{ user.display_name }}</th> + <th scope="row"> + <img + :src="user.gravatar_url + '?d=mp&s=32'" + class="rounded-circle me-2" + height="32" + width="32" + alt="profile picture" + />{{ user.display_name }} + </th> <td>{{ user.uid }}</td> <td v-for="role in Object.values(RoleEnum)" diff --git a/src/views/resources/ReviewResourceView.vue b/src/views/resources/ReviewResourceView.vue index 61da4124bb1183717d5c3ae88cedd155bd465216..9e57c25867b317b044cbf91e52413274f788089a 100644 --- a/src/views/resources/ReviewResourceView.vue +++ b/src/views/resources/ReviewResourceView.vue @@ -130,7 +130,7 @@ onMounted(() => { > <button type="button" - class="btn btn-primary btn-light me-2 shadow-sm border w-fit" + class="btn btn-light me-2 shadow-sm border w-fit" :disabled="resourceState.loading" @click="clickRefreshResources" > diff --git a/src/views/user/ListApiTokenView.vue b/src/views/user/ListApiTokenView.vue new file mode 100644 index 0000000000000000000000000000000000000000..b2a773f01243c758a0852fac630ddf7d00a9b093 --- /dev/null +++ b/src/views/user/ListApiTokenView.vue @@ -0,0 +1,202 @@ +<script setup lang="ts"> +import { onMounted, reactive } from "vue"; +import { useUserStore } from "@/stores/users"; +import dayjs from "dayjs"; +import DeleteModal from "@/components/modals/DeleteModal.vue"; +import CreateApiTokenModal from "@/components/user/CreateApiTokenModal.vue"; +import type { ApiTokenPrivateOut } from "@/client"; +import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; +import CopyToClipboardIcon from "@/components/CopyToClipboardIcon.vue"; +import { environment } from "../../environment"; + +const userRepository = useUserStore(); + +const state = reactive<{ + loading: boolean; + deleteTokenId: string; + createdToken?: ApiTokenPrivateOut; + showCreatedToken: boolean; +}>({ + loading: false, + deleteTokenId: "", + createdToken: undefined, + showCreatedToken: false, +}); + +function fetchTokens() { + state.loading = true; + userRepository.fetchApiTokens(userRepository.currentUID, () => { + state.loading = false; + }); +} + +function deleteApiToken(tokenId: string) { + state.deleteTokenId = tokenId; +} + +function confirmedDeleteApiToken() { + userRepository.deleteApiToken(state.deleteTokenId).finally(() => { + state.deleteTokenId = ""; + }); +} + +function apiTokenCreated(token: ApiTokenPrivateOut) { + state.createdToken = token; +} + +function closeSuccessAlert() { + state.createdToken = undefined; + state.showCreatedToken = false; +} + +onMounted(() => { + fetchTokens(); +}); +</script> + +<template> + <create-api-token-modal + modal-id="create-personal-api-token-modal" + @token-created="apiTokenCreated" + /> + <delete-modal + modal-id="delete-personal-api-token-modal" + @confirm-delete="confirmedDeleteApiToken" + :object-name-delete=" + userRepository.apiTokensMapping[state.deleteTokenId]?.name ?? '' + " + /> + <div + class="row border-bottom mb-4 justify-content-between align-items-center" + > + <h2 class="w-fit">Personal API tokens</h2> + <span class="w-fit" tabindex="0"> + <button + type="button" + class="btn btn-primary" + data-bs-toggle="modal" + data-bs-target="#create-personal-api-token-modal" + > + Create token + </button> + </span> + </div> + <h6 class="text-warning mb-3"> + <font-awesome-icon icon="fa-solid fa-triangle-exclamation" /> + If you don't know what an API token is used for then you probably don't need + it. + <font-awesome-icon icon="fa-solid fa-triangle-exclamation" /> + </h6> + <p> + When using the Rest API of CloWM, you need to authenticate yourself with API + tokens. With an API token you can make an request to the API in the name of + your user account. To make a request to the REST API with an API token, + write the token in the HTTP header with the key + <code>X-CLOWM-TOKEN</code>.<br /> + You can find an online documentation of the CloWM API at + <a :href="environment.API_BASE_URL + '/docs'" target="_blank">{{ + environment.API_BASE_URL + "/docs" + }}</a + >. + </p> + <div + v-if="state.createdToken" + class="alert alert-success alert-dismissible" + role="alert" + style="min-width: 50%" + > + <strong>Your new API token</strong> + <button + type="button" + class="btn-close" + aria-label="Close" + @click="closeSuccessAlert" + ></button> + <br /> + <div class="input-group my-2 w-fit"> + <input + :type="state.showCreatedToken ? 'text' : 'password'" + class="form-control" + readonly + :value="state.createdToken.token" + /> + <div + class="input-group-text cursor-pointer hover-info" + @click="state.showCreatedToken = !state.showCreatedToken" + > + <font-awesome-icon + :icon="`fa-solid fa-eye${state.showCreatedToken ? '-slash' : ''}`" + /> + </div> + <div class="input-group-text"> + <copy-to-clipboard-icon :text="state.createdToken.token" /> + </div> + </div> + <div>Make sure you save it - you won't be able to access it again.</div> + </div> + <table class="table table-striped align-middle caption-top"> + <caption> + Displaying + {{ + userRepository.apiTokens.length + }} + API tokens + </caption> + <thead> + <tr> + <th scope="col"><b>Name</b></th> + <th scope="col">Scopes</th> + <th scope="col">Created</th> + <th scope="col">Last used</th> + <th scope="col">Expires</th> + <th scope="col"></th> + </tr> + </thead> + <tbody v-if="state.loading"> + <tr> + <td colspan="6" class="text-center fst-italic fw-light">Loading ...</td> + </tr> + </tbody> + <tbody v-else-if="userRepository.apiTokens.length === 0"> + <tr> + <td colspan="6" class="text-center fst-italic fw-light"> + No personal API tokens + </td> + </tr> + </tbody> + <tbody v-else> + <tr v-for="token in userRepository.apiTokens" :key="token.token_id"> + <th scope="row">{{ token.name }}</th> + <td>{{ token.scopes.join(", ") }}</td> + <td> + {{ dayjs.unix(token.created_at).format("MMM DD, YYYY") }} + </td> + <td v-if="token.last_used"> + {{ dayjs.unix(token.last_used).format("MMM DD, YYYY HH:mm") }} + </td> + <td v-else>Never</td> + <td v-if="token.expires_at"> + {{ + dayjs + .duration(token.expires_at - dayjs().unix(), "seconds") + .humanize(true) + }} + </td> + <td v-else>Never</td> + <td class="text-end"> + <button + type="button" + class="btn btn-danger btn-sm" + data-bs-toggle="modal" + data-bs-target="#delete-personal-api-token-modal" + @click="deleteApiToken(token.token_id)" + > + Delete + </button> + </td> + </tr> + </tbody> + </table> +</template> + +<style scoped></style> diff --git a/src/views/workflows/ArbitraryWorkflowView.vue b/src/views/workflows/ArbitraryWorkflowView.vue index 4ef84be317ba3757605ffaac7f5f8116e837bd4b..45f1b3e40275759697cd8b148da29c7a8459bf0a 100644 --- a/src/views/workflows/ArbitraryWorkflowView.vue +++ b/src/views/workflows/ArbitraryWorkflowView.vue @@ -15,6 +15,7 @@ import type { WorkflowMetaParameters, FlatWorkflowParameters, } from "@/types/WorkflowParameters"; +import dayjs from "dayjs"; const props = defineProps<{ wid: string; @@ -34,9 +35,11 @@ const workflowState = reactive<{ outputMarkdown?: string; parameterSchema?: Record<string, never>; repo: GitRepository; + ratelimit_reset: number; }>({ loading: true, workflow: undefined, + ratelimit_reset: 0, repo: GitRepository.buildRepository( "https://github.de/eample/example", "0123456789abcdef", @@ -73,9 +76,16 @@ function downloadVersionFiles() { } }), ), - ).finally(() => { - workflowState.loading = false; - }); + ) + .catch((e) => { + const headers = JSON.parse(e.message) as Record<string, string>; + workflowState.ratelimit_reset = parseInt( + headers["x-ratelimit-reset"] ?? "0", + ); + }) + .finally(() => { + workflowState.loading = false; + }); } } @@ -154,7 +164,7 @@ onMounted(() => { toast-id="arbitraryWorkflowExecutionViewErrorToast" color-class="danger" > - <template #default> Error starting workflow </template> + <template #default> Error starting workflow</template> <template #body> <template v-if="workflowExecutionState.errorType === 'limit'"> You have too many active workflow executions to start a new one. @@ -197,8 +207,11 @@ onMounted(() => { <span class="align-middle">Launch</span> </a> </div> + <div v-if="workflowState.ratelimit_reset > 0"> + Ratelimit till {{ dayjs.unix(workflowState.ratelimit_reset).fromNow() }} + </div> <workflow-documentation-tabs - v-if="showDocumentation" + v-else-if="showDocumentation" :loading="workflowState.loading" :output-markdown="workflowState.outputMarkdown" :usage-markdown="workflowState.usageMarkdown" diff --git a/src/views/workflows/WorkflowView.vue b/src/views/workflows/WorkflowView.vue index f99d1d2d2741ca39d5bda18b3e8b4c6fded9e789..f0c7afda308dd9f4482e4180b9babe66b9f8469c 100644 --- a/src/views/workflows/WorkflowView.vue +++ b/src/views/workflows/WorkflowView.vue @@ -98,7 +98,7 @@ const gitIcon = computed<string>(() => const allowVersionDeprecation = computed<boolean>(() => { if (activeVersion.value?.status === WorkflowVersionStatus.PUBLISHED) { - if (userRepository.rewiewer || userRepository.admin) { + if (userRepository.reviewer || userRepository.admin) { return true; } else if ( userRepository.workflowDev &&