diff --git a/.gitignore b/.gitignore index 1ae1622..5c9b7bc 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ build/Release # Dependency directories node_modules/ jspm_packages/ +mongo-backup/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ diff --git a/client/.env.development b/client/.env.development index e37f392..d1374e8 100644 --- a/client/.env.development +++ b/client/.env.development @@ -1,2 +1,3 @@ VITE_BACKEND_URL=http://localhost:4400 VITE_BACKEND_SOCKET_URL=http://localhost:4400 +VITE_IMG_URL=http://localhost:4400 diff --git a/client/.env.example b/client/.env.example index 944dcbb..787c0f6 100644 --- a/client/.env.example +++ b/client/.env.example @@ -1,2 +1,3 @@ VITE_BACKEND_URL=http://localhost:4400 -VITE_AZURE_BACKEND_URL=http://localhost:4400 \ No newline at end of file +VITE_AZURE_BACKEND_URL=http://localhost:4400 +VITE_IMG_URL=http://localhost:4400 \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index ae2f273..4e40e71 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -46,6 +46,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", + "@testing-library/user-event": "^14.6.1", "@types/jest": "^29.5.13", "@types/node": "^22.13.5", "@types/react": "^18.2.15", @@ -420,26 +421,24 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", - "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dependencies": { - "@babel/types": "^7.26.9" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1894,10 +1893,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", - "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1906,14 +1904,13 @@ } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -1947,10 +1944,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", - "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" @@ -2538,7 +2534,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -2557,7 +2553,7 @@ "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -2567,7 +2563,7 @@ "version": "0.19.2", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", @@ -2582,7 +2578,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2593,7 +2589,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2606,7 +2602,7 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -2619,7 +2615,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -2643,7 +2639,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2654,7 +2650,7 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -2667,7 +2663,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2680,7 +2676,7 @@ "version": "9.21.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2690,7 +2686,7 @@ "version": "2.1.6", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2700,7 +2696,7 @@ "version": "0.2.7", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.12.0", @@ -2807,7 +2803,7 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18.18.0" @@ -2817,7 +2813,7 @@ "version": "0.16.6", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", @@ -2831,7 +2827,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -2845,7 +2841,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=12.22" @@ -2859,7 +2855,7 @@ "version": "0.4.2", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -3861,7 +3857,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3875,7 +3870,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3889,7 +3883,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3903,7 +3896,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3917,7 +3909,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3931,7 +3922,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3945,7 +3935,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3959,7 +3948,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3973,7 +3961,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3987,7 +3974,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4001,7 +3987,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4015,7 +4000,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4029,7 +4013,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4043,7 +4026,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4057,7 +4039,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4071,7 +4052,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4085,7 +4065,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4099,7 +4078,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4113,7 +4091,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4154,7 +4131,7 @@ "version": "1.11.5", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.5.tgz", "integrity": "sha512-EVY7zfpehxhTZXOfy508gb3D78ihoGGmvyiTWtlBPjgIaidP1Xw0naHMD78CWiFlZmeDjKXJufGtsEGOnZdmNA==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -4196,7 +4173,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4213,7 +4189,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4230,7 +4205,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4247,7 +4221,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4264,7 +4237,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4281,7 +4253,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4298,7 +4269,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4315,7 +4285,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4332,7 +4301,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4349,7 +4317,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4363,14 +4330,14 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@swc/types": { "version": "0.1.19", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.19.tgz", "integrity": "sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" @@ -4401,7 +4368,6 @@ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", "dev": true, - "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", @@ -4466,6 +4432,19 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -4564,7 +4543,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, "license": "MIT" }, "node_modules/@types/graceful-fs": { @@ -4671,7 +4649,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/katex": { @@ -4720,7 +4698,6 @@ "version": "18.3.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -5054,7 +5031,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -5088,7 +5065,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -5179,7 +5156,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, + "devOptional": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -5368,10 +5345,9 @@ } }, "node_modules/axios": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", - "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", - "license": "MIT", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -5565,7 +5541,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/brace-expansion": { @@ -5900,7 +5876,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/convert-source-map": { @@ -5999,7 +5975,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -6170,7 +6146,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/deepmerge": { @@ -6736,7 +6712,7 @@ "version": "9.21.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -6966,7 +6942,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -6983,7 +6959,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -6996,7 +6972,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7007,7 +6983,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7020,7 +6996,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -7033,7 +7009,7 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.14.0", @@ -7051,7 +7027,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7077,7 +7053,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -7090,7 +7066,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -7183,7 +7159,7 @@ "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, + "devOptional": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -7220,14 +7196,14 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fastq": { @@ -7268,7 +7244,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" @@ -7322,7 +7298,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -7339,7 +7315,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", @@ -7353,7 +7329,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/follow-redirects": { @@ -7418,7 +7394,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -7602,7 +7577,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -7887,7 +7862,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 4" @@ -7933,7 +7908,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -8127,7 +8102,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8192,7 +8167,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -8427,7 +8402,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -9539,7 +9514,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -9609,7 +9584,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { @@ -9622,14 +9597,14 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json5": { @@ -9689,7 +9664,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -9719,7 +9694,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", @@ -9739,7 +9714,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -9776,7 +9751,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/longest-streak": { @@ -10566,7 +10541,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/node-int64": { @@ -10748,7 +10723,7 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "deep-is": "^0.1.3", @@ -10784,7 +10759,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -10800,7 +10775,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -10868,7 +10843,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -10888,7 +10863,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -11032,7 +11007,6 @@ "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -11061,7 +11035,6 @@ "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, "funding": [ { "type": "github", @@ -11080,7 +11053,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -11600,7 +11573,6 @@ "version": "4.34.8", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.6" @@ -11804,7 +11776,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -11817,7 +11789,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -11997,7 +11969,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -12229,7 +12200,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -12513,7 +12484,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" @@ -12626,7 +12597,6 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -12872,7 +12842,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -12958,11 +12928,9 @@ } }, "node_modules/vite": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", - "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", - "dev": true, - "license": "MIT", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz", + "integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==", "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", @@ -13239,7 +13207,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -13343,7 +13311,7 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13491,7 +13459,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/client/package.json b/client/package.json index d5279b1..82f2d57 100644 --- a/client/package.json +++ b/client/package.json @@ -50,6 +50,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", + "@testing-library/user-event": "^14.6.1", "@types/jest": "^29.5.13", "@types/node": "^22.13.5", "@types/react": "^18.2.15", diff --git a/client/src/Types/ImageType.tsx b/client/src/Types/ImageType.tsx new file mode 100644 index 0000000..d0a1541 --- /dev/null +++ b/client/src/Types/ImageType.tsx @@ -0,0 +1,17 @@ +export interface ImageType { + id: string; + file_content: string; + file_name: string; + mime_type: string; +} + +export interface ImagesResponse { + images: ImageType[]; + total: number; +} + +export interface ImagesParams { + page: number; + limit: number; + uid?: string; +} \ No newline at end of file diff --git a/client/src/Types/Images.tsx b/client/src/Types/Images.tsx new file mode 100644 index 0000000..8cfe170 --- /dev/null +++ b/client/src/Types/Images.tsx @@ -0,0 +1,17 @@ +export interface Images { + id: string; + file_content: string; + file_name: string; + mime_type: string; +} + +export interface ImagesResponse { + images: Images[]; + total: number; +} + +export interface ImagesParams { + page: number; + limit: number; + uid?: string; +} \ No newline at end of file diff --git a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx new file mode 100644 index 0000000..5586a6a --- /dev/null +++ b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx @@ -0,0 +1,147 @@ +import React, { act } from "react"; +import "@testing-library/jest-dom"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import ImageGallery from "../../../components/ImageGallery/ImageGallery"; +import ApiService from "../../../services/ApiService"; +import { Images } from "../../../Types/Images"; +import userEvent from "@testing-library/user-event"; + +jest.mock("../../../services/ApiService"); + +const mockImages: Images[] = [ + { id: "1", file_name: "image1.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content1" }, + { id: "2", file_name: "image2.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content2" }, + { id: "3", file_name: "image3.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content3" }, +]; + +beforeAll(() => { + global.URL.createObjectURL = jest.fn(() => 'mockedObjectUrl'); + Object.assign(navigator, { + clipboard: { + writeText: jest.fn(), + }, + }); +}); + +describe("ImageGallery", () => { + let mockHandleDelete: jest.Mock; + + beforeEach(async () => { + (ApiService.getUserImages as jest.Mock).mockResolvedValue({ images: mockImages, total: 3 }); + (ApiService.deleteImage as jest.Mock).mockResolvedValue(true); + (ApiService.uploadImage as jest.Mock).mockResolvedValue('mockImageUrl'); + await act(async () => { + render(); + }); + mockHandleDelete = jest.fn(); + }); + + it("should render images correctly", async () => { + await act(async () => { + await screen.findByText("Galerie"); + }); + + expect(screen.getByAltText("Image image1.jpg")).toBeInTheDocument(); + expect(screen.getByAltText("Image image2.jpg")).toBeInTheDocument(); + }); + + it("should handle copy action", async () => { + const handleCopyMock = jest.fn(); + await act(async () => { + render(); + }); + + const copyButtons = await waitFor(() => screen.findAllByTestId(/gallery-tab-copy-/)); + await act(async () => { + fireEvent.click(copyButtons[0]); + }); + + expect(navigator.clipboard.writeText).toHaveBeenCalled(); + }); + + it("should delete an image and update the gallery", async () => { + const fetchImagesMock = jest.fn().mockResolvedValue({ images: mockImages.filter((image) => image.id !== "1"), total: 2 }); + + (ApiService.getUserImages as jest.Mock).mockImplementation(fetchImagesMock); + + await act(async () => { + render(); + }); + + await act(async () => { + await screen.findByAltText("Image image1.jpg"); + }); + + const deleteButtons = await waitFor(() => screen.findAllByTestId(/gallery-tab-delete-/)); + fireEvent.click(deleteButtons[0]); + + await waitFor(() => { + expect(screen.getByText("Voulez-vous supprimer cette image?")).toBeInTheDocument(); + }); + + const confirmDeleteButton = screen.getByText("Delete"); + await act(async () => { + fireEvent.click(confirmDeleteButton); + }); + + await waitFor(() => { + expect(ApiService.deleteImage).toHaveBeenCalledWith("1"); + }); + + await waitFor(() => { + expect(screen.queryByAltText("Image image1.jpg")).toBeNull(); + expect(screen.getByText("Image supprimée avec succès!")).toBeInTheDocument(); + }); + }); + + it("should upload an image and display success message", async () => { + const importTab = screen.getByRole("tab", { name: /import/i }); + fireEvent.click(importTab); + + const fileInputs = await screen.findAllByTestId("file-input"); + const fileInput = fileInputs[1]; + + expect(fileInput).toBeInTheDocument(); + + const file = new File(["image"], "image.jpg", { type: "image/jpeg" }); + await userEvent.upload(fileInput, file); + + + await waitFor(() => screen.getByAltText("Preview")); + const previewImage = screen.getByAltText("Preview"); + + expect(previewImage).toBeInTheDocument(); + + const uploadButton = screen.getByRole('button', { name: /téléverser/i }); + fireEvent.click(uploadButton); + const successMessage = await screen.findByText(/téléversée avec succès/i); + expect(successMessage).toBeInTheDocument(); + }); + + it("should close the image preview dialog when close button is clicked", async () => { + const imageCard = screen.getByAltText("Image image1.jpg"); + fireEvent.click(imageCard); + + const dialogImage = await screen.findByAltText("Enlarged view"); + expect(dialogImage).toBeInTheDocument(); + + const closeButton = screen.getByTestId("close-button"); + fireEvent.click(closeButton); + + await waitFor(() => { + expect(screen.queryByAltText("Enlarged view")).not.toBeInTheDocument(); + }); + }); + + it("should show an error message when no file is selected", async () => { + const importTab = screen.getByRole("tab", { name: /import/i }); + fireEvent.click(importTab); + const uploadButton = screen.getByRole('button', { name: /téléverser/i }); + fireEvent.click(uploadButton); + + await waitFor(() => { + expect(screen.getByText("Veuillez choisir une image à téléverser.")).toBeInTheDocument(); + }); + }); + +}); diff --git a/client/src/__tests__/components/ImageGallery/ImageGalleryModal.test.tsx b/client/src/__tests__/components/ImageGallery/ImageGalleryModal.test.tsx new file mode 100644 index 0000000..f5a65a6 --- /dev/null +++ b/client/src/__tests__/components/ImageGallery/ImageGalleryModal.test.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import ImageGalleryModal from "../../../components/ImageGallery/ImageGalleryModal/ImageGalleryModal"; +import "@testing-library/jest-dom"; + +jest.mock("../../../components/ImageGallery/ImageGallery", () => ({ + __esModule: true, + default: jest.fn(() =>
), +})); + +describe("ImageGalleryModal", () => { + + it("renders button correctly", () => { + render(); + + const button = screen.getByLabelText(/images-open/i); + expect(button).toBeInTheDocument(); + }); + + it("opens the modal when button is clicked", () => { + render(); + + const button = screen.getByRole("button", { name: /images/i }); + fireEvent.click(button); + + const dialog = screen.getByRole("dialog"); + expect(dialog).toBeInTheDocument(); + }); + + + it("closes the modal when close button is clicked", async () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: /images/i })); + + const closeButton = screen.getByRole("button", { name: /close/i }); + fireEvent.click(closeButton); + + await waitFor(() => { + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + +}); diff --git a/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx b/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx new file mode 100644 index 0000000..7027eaf --- /dev/null +++ b/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { render, screen, fireEvent, act } from '@testing-library/react'; +import ShareQuizModal from '../../../components/ShareQuizModal/ShareQuizModal.tsx'; +import { QuizType } from '../../../Types/QuizType'; +import '@testing-library/jest-dom'; + +describe('ShareQuizModal', () => { + const mockQuiz: QuizType = { + _id: '123', + folderId: 'folder-123', + folderName: 'Test Folder', + userId: 'user-123', + title: 'Test Quiz', + content: ['Question 1', 'Question 2'], + created_at: new Date(), + updated_at: new Date(), + }; + + beforeAll(() => { + // Properly mock the clipboard API + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: jest.fn().mockImplementation(() => Promise.resolve()), + }, + writable: true, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders the share button', () => { + render(); + expect(screen.getByLabelText('partager quiz')).toBeInTheDocument(); + expect(screen.getByTestId('ShareIcon')).toBeInTheDocument(); + }); + + it('copies the quiz URL to clipboard when share button is clicked', async () => { + render(); + const shareButton = screen.getByLabelText('partager quiz'); + + await act(async () => { + fireEvent.click(shareButton); + }); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + `${window.location.origin}/teacher/share/${mockQuiz._id}` + ); + + // Check for feedback dialog content + expect(screen.getByText(/L'URL de partage pour le quiz/i)).toBeInTheDocument(); + expect(screen.getByText(mockQuiz.title)).toBeInTheDocument(); + expect(screen.getByText(/a été copiée\./i)).toBeInTheDocument(); + }); + + it('shows error message when clipboard write fails', async () => { + // Override the mock to reject + (navigator.clipboard.writeText as jest.Mock).mockRejectedValueOnce(new Error('Clipboard write failed')); + + render(); + const shareButton = screen.getByLabelText('partager quiz'); + + await act(async () => { + fireEvent.click(shareButton); + }); + + expect(screen.getByText(/Une erreur est survenue lors de la copie de l'URL\./i)).toBeInTheDocument(); + }); + + it('displays the quiz title in the success message', async () => { + render(); + const shareButton = screen.getByLabelText('partager quiz'); + + await act(async () => { + fireEvent.click(shareButton); + }); + + expect(screen.getByText(mockQuiz.title)).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx b/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx index 1a708a8..aca1d64 100644 --- a/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx +++ b/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx @@ -299,6 +299,37 @@ describe('ManageRoom', () => { expect(screen.queryByText('Student 1')).not.toBeInTheDocument(); }); }); + + test('terminates the quiz and navigates to teacher dashboard when the "Terminer le quiz" button is clicked', async () => { + await act(async () => { + render( + + + + ); + }); + + await act(async () => { + const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1]; + createSuccessCallback('Test Room'); + }); + + fireEvent.click(screen.getByText('Lancer')); + fireEvent.click(screen.getByText('Rythme du professeur')); + fireEvent.click(screen.getAllByText('Lancer')[1]); + + await waitFor(() => { + expect(screen.getByText('Test Quiz')).toBeInTheDocument(); + }); + + const finishQuizButton = screen.getByText('Terminer le quiz'); + fireEvent.click(finishQuizButton); + + await waitFor(() => { + expect(navigate).toHaveBeenCalledWith('/teacher/dashboard'); + }); + }); + test("Affiche la modale QR Code lorsqu’on clique sur le bouton", async () => { render(); diff --git a/client/src/__tests__/pages/Teacher/Share/Share.test.tsx b/client/src/__tests__/pages/Teacher/Share/Share.test.tsx new file mode 100644 index 0000000..9cc3203 --- /dev/null +++ b/client/src/__tests__/pages/Teacher/Share/Share.test.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { MemoryRouter, Route, Routes, useParams } from 'react-router-dom'; +import Share from '../../../../pages/Teacher/Share/Share.tsx'; +import ApiService from '../../../../services/ApiService'; +import '@testing-library/jest-dom'; + +// Mock the ApiService and react-router-dom +jest.mock('../../../../services/ApiService'); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), + useNavigate: jest.fn(), +})); + +describe('Share Component', () => { + const mockNavigate = jest.fn(); + const mockUseParams = useParams as jest.Mock; + const mockApiService = ApiService as jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + mockUseParams.mockReturnValue({ id: 'quiz123' }); + require('react-router-dom').useNavigate.mockReturnValue(mockNavigate); + }); + + const renderComponent = (initialEntries = ['/share/quiz123']) => { + return render( + + + } /> + Dashboard
} /> + Login} /> + + + ); + }; + + it('should show loading state initially', () => { + mockApiService.getAllQuizIds.mockResolvedValue([]); + mockApiService.getUserFolders.mockResolvedValue([]); + mockApiService.getSharedQuiz.mockResolvedValue('Test Quiz'); + + renderComponent(); + expect(screen.getByText('Chargement...')).toBeInTheDocument(); + }); + + it('should redirect to login if not authenticated', async () => { + mockApiService.isLoggedIn.mockReturnValue(false); + + renderComponent(); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('/login'); + }); + }); + + it('should show "quiz already exists" message when quiz exists', async () => { + mockApiService.isLoggedIn.mockReturnValue(true); + mockApiService.getAllQuizIds.mockResolvedValue(['quiz123']); + + renderComponent(); + + await waitFor(() => { + expect(screen.getByText('Quiz déjà existant')).toBeInTheDocument(); + expect(screen.getByText(/Le quiz que vous essayez d'importer existe déjà sur votre compte/i)).toBeInTheDocument(); + expect(screen.getByText('Retour au tableau de bord')).toBeInTheDocument(); + }); + }); + + it('should navigate to dashboard when clicking return button in "quiz exists" view', async () => { + mockApiService.isLoggedIn.mockReturnValue(true); + mockApiService.getAllQuizIds.mockResolvedValue(['quiz123']); + + renderComponent(); + + await waitFor(() => { + fireEvent.click(screen.getByText('Retour au tableau de bord')); + expect(mockNavigate).toHaveBeenCalledWith('/teacher/dashboard'); + }); + }); + + it('should show error when no folders exist', async () => { + mockApiService.isLoggedIn.mockReturnValue(true); + mockApiService.getAllQuizIds.mockResolvedValue([]); + mockApiService.getUserFolders.mockResolvedValue([]); + + renderComponent(); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('/teacher/dashboard'); + }); + }); + +}); \ No newline at end of file diff --git a/client/src/__tests__/services/ShareQuizService.test.tsx b/client/src/__tests__/services/ShareQuizService.test.tsx new file mode 100644 index 0000000..78c1f65 --- /dev/null +++ b/client/src/__tests__/services/ShareQuizService.test.tsx @@ -0,0 +1,42 @@ +import axios from 'axios'; +import ApiService from '../../services/ApiService'; +import { ENV_VARIABLES } from '../../constants'; + +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + + describe('getSharedQuiz', () => { + it('should call the API to get a shared quiz and return the quiz data on success', async () => { + const quizId = '123'; + const quizData = 'Quiz data'; + const response = { status: 200, data: { data: quizData } }; + mockedAxios.get.mockResolvedValue(response); + + const result = await ApiService.getSharedQuiz(quizId); + + expect(mockedAxios.get).toHaveBeenCalledWith( + `${ENV_VARIABLES.VITE_BACKEND_URL}/api/quiz/getShare/${quizId}`, + { headers: expect.any(Object) } + ); + expect(result).toBe(quizData); + }); + + it('should return an error message if the API call fails', async () => { + const quizId = '123'; + const errorMessage = 'An unexpected error occurred.'; + mockedAxios.get.mockRejectedValue({ response: { data: { error: errorMessage } } }); + + const result = await ApiService.getSharedQuiz(quizId); + + expect(result).toBe(errorMessage); + }); + + it('should return a generic error message if an unexpected error occurs', async () => { + const quizId = '123'; + mockedAxios.get.mockRejectedValue(new Error('Unexpected error')); + + const result = await ApiService.getSharedQuiz(quizId); + + expect(result).toBe('An unexpected error occurred.'); + }); + }); \ No newline at end of file diff --git a/client/src/__tests__/services/getAllQuizIdsService.test.tsx b/client/src/__tests__/services/getAllQuizIdsService.test.tsx new file mode 100644 index 0000000..222e7db --- /dev/null +++ b/client/src/__tests__/services/getAllQuizIdsService.test.tsx @@ -0,0 +1,42 @@ +import axios from 'axios'; +import ApiService from '../../services/ApiService'; +import { FolderType } from '../../Types/FolderType'; +import { QuizType } from '../../Types/QuizType'; + +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +describe('ApiService', () => { + describe('getAllQuizIds', () => { + it('should return all quiz IDs from all folders', async () => { + const folders: FolderType[] = [ + { _id: 'folder1', title: 'Folder 1', userId: 'user1', created_at: new Date().toISOString() }, + { _id: 'folder2', title: 'Folder 2', userId: 'user2', created_at: new Date().toISOString() } + ]; + const quizzesFolder1: QuizType[] = [ + { _id: 'quiz1', title: 'Quiz 1', content: [], folderId: 'folder1', folderName: 'Folder 1', userId: 'user1', created_at: new Date(), updated_at: new Date() }, + { _id: 'quiz2', title: 'Quiz 2', content: [], folderId: 'folder1', folderName: 'Folder 1', userId: 'user1', created_at: new Date(), updated_at: new Date() } + ]; + const quizzesFolder2: QuizType[] = [ + { _id: 'quiz3', title: 'Quiz 3', content: [], folderId: 'folder2', folderName: 'Folder 2', userId: 'user2', created_at: new Date(), updated_at: new Date() } + ]; + + mockedAxios.get + .mockResolvedValueOnce({ status: 200, data: { data: folders } }) + .mockResolvedValueOnce({ status: 200, data: { data: quizzesFolder1 } }) + .mockResolvedValueOnce({ status: 200, data: { data: quizzesFolder2 } }); + + const result = await ApiService.getAllQuizIds(); + + expect(result).toEqual(['quiz1', 'quiz2', 'quiz3']); + }); + + it('should return an empty array if no folders are found', async () => { + mockedAxios.get.mockResolvedValueOnce({ status: 200, data: { data: [] } }); + + const result = await ApiService.getAllQuizIds(); + + expect(result).toEqual([]); + }); + }); +}); \ No newline at end of file diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx new file mode 100644 index 0000000..ff5c6e8 --- /dev/null +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -0,0 +1,309 @@ +import React, { useState, useEffect } from "react"; +import { + Box, + CircularProgress, + Button, + IconButton, + Card, + CardContent, + Dialog, + DialogContent, + DialogActions, + DialogTitle, + DialogContentText, + Tabs, + Tab, + TextField, Snackbar, + Alert +} from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import CloseIcon from "@mui/icons-material/Close"; +import { ImageType } from "../../Types/ImageType"; +import ApiService from "../../services/ApiService"; +import { Upload } from "@mui/icons-material"; +import { ENV_VARIABLES } from '../../constants'; +import { escapeForGIFT } from "src/utils/giftUtils"; + +interface ImagesProps { + handleCopy?: (id: string) => void; + handleDelete?: (id: string) => void; +} + +const ImageGallery: React.FC = ({ handleCopy, handleDelete }) => { + const [images, setImages] = useState([]); + const [totalImg, setTotalImg] = useState(0); + const [imgPage, setImgPage] = useState(1); + const [imgLimit] = useState(6); + const [loading, setLoading] = useState(false); + const [selectedImage, setSelectedImage] = useState(null); + const [openDeleteDialog, setOpenDeleteDialog] = useState(false); + const [imageToDelete, setImageToDelete] = useState(null); + const [tabValue, setTabValue] = useState(0); + const [importedImage, setImportedImage] = useState(null); + const [preview, setPreview] = useState(null); + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(""); + const [snackbarSeverity, setSnackbarSeverity] = useState<"success" | "error">("success"); + + const fetchImages = async () => { + setLoading(true); + const data = await ApiService.getUserImages(imgPage, imgLimit); + setImages(data.images); + setTotalImg(data.total); + setLoading(false); + }; + + useEffect(() => { + fetchImages(); + }, [imgPage]); + + const defaultHandleDelete = async (id: string) => { + if (imageToDelete) { + setLoading(true); + const isDeleted = await ApiService.deleteImage(id); + setLoading(false); + + if (isDeleted) { + setImgPage(1); + fetchImages(); + setSnackbarMessage("Image supprimée avec succès!"); + setSnackbarSeverity("success"); + } else { + setSnackbarMessage("Erreur lors de la suppression de l'image. Veuillez réessayer."); + setSnackbarSeverity("error"); + } + + setSnackbarOpen(true); + setSelectedImage(null); + setImageToDelete(null); + setOpenDeleteDialog(false); + } + }; + + const defaultHandleCopy = (id: string) => { + if (navigator.clipboard) { + const link = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`; + const imgTag = `[markdown]![alt_text](${escapeForGIFT(link)} "texte de l'infobulle") {T}`; + setSnackbarMessage("Le lien Markdown de l’image a été copié dans le presse-papiers"); + setSnackbarSeverity("success"); + setSnackbarOpen(true); + navigator.clipboard.writeText(imgTag); + } + if(handleCopy) { + handleCopy(id); + } + }; + + const handleDeleteFunction = handleDelete || defaultHandleDelete; + + const handleImageUpload = (event: React.ChangeEvent) => { + const file = event.target.files ? event.target.files[0] : null; + setImportedImage(file); + if (file) { + const objectUrl = URL.createObjectURL(file); + setPreview(objectUrl); + } + }; + + const handleSaveImage = async () => { + try { + if (!importedImage) { + setSnackbarMessage("Veuillez choisir une image à téléverser."); + setSnackbarSeverity("error"); + setSnackbarOpen(true); + return; + } + + const imageUrl = await ApiService.uploadImage(importedImage); + + if (imageUrl.includes("ERROR")) { + setSnackbarMessage("Une erreur est survenue. Veuillez réessayer plus tard."); + setSnackbarSeverity("error"); + setSnackbarOpen(true); + return; + } + fetchImages(); + + setSnackbarMessage("Téléversée avec succès !"); + setSnackbarSeverity("success"); + setSnackbarOpen(true); + + setImportedImage(null); + setPreview(null); + setTabValue(0); + } catch (error) { + setSnackbarMessage(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`); + setSnackbarSeverity("error"); + setSnackbarOpen(true); + } + }; + + const handleCloseSnackbar = () => { + setSnackbarOpen(false); + }; + + + return ( + + setTabValue(newValue)}> + + + + {tabValue === 0 && ( + <> + {loading ? ( + + + + ) : ( + <> + + {images.map((obj) => ( + setSelectedImage(obj)}> + + {`Image + + + { + e.stopPropagation(); + defaultHandleCopy(obj.id); + }} + color="primary" + data-testid={`gallery-tab-copy-${obj.id}`} > + + + + + { + e.stopPropagation(); + setImageToDelete(obj); + setOpenDeleteDialog(true); + }} + color="error" + data-testid={`gallery-tab-delete-${obj.id}`} > + + + + + + ))} + + + + + + + )} + + )} + {tabValue === 1 && ( + + {/* Image Preview at the top */} + {preview && ( + + Preview + + )} + + + + + + )} + setSelectedImage(null)} maxWidth="md"> + setSelectedImage(null)} sx={{ position: "absolute", right: 8, top: 8, zIndex: 1 }} + data-testid="close-button"> + + + + {selectedImage && ( + Enlarged view + )} + + + + {/* Delete Confirmation Dialog */} + setOpenDeleteDialog(false)}> + Supprimer + + Voulez-vous supprimer cette image? + + + + + + + + + + {snackbarMessage} + + + + ); +}; + +export default ImageGallery; diff --git a/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx b/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx new file mode 100644 index 0000000..f960352 --- /dev/null +++ b/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx @@ -0,0 +1,55 @@ +import React, { useState } from "react"; +import { + Button, + IconButton, + Dialog, + DialogContent, +} from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import ImageGallery from "../ImageGallery"; +import { ImageSearch } from "@mui/icons-material"; + + +interface ImageGalleryModalProps { + handleCopy?: (id: string) => void; +} + + +const ImageGalleryModal: React.FC = ({ handleCopy }) => { + const [open, setOpen] = useState(false); + + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + + return ( + <> + + + + + + + + + + + + ); +}; + +export default ImageGalleryModal; diff --git a/client/src/components/ShareQuizModal/ShareQuizModal.tsx b/client/src/components/ShareQuizModal/ShareQuizModal.tsx new file mode 100644 index 0000000..7d752bc --- /dev/null +++ b/client/src/components/ShareQuizModal/ShareQuizModal.tsx @@ -0,0 +1,94 @@ +import React, { useState } from 'react'; +import { Dialog, DialogTitle, DialogActions, Button, Tooltip, IconButton, Typography, Box } from '@mui/material'; +import { Share } from '@mui/icons-material'; +import { QuizType } from '../../Types/QuizType'; + +interface ShareQuizModalProps { + quiz: QuizType; +} + +const ShareQuizModal: React.FC = ({ quiz }) => { + const [_open, setOpen] = useState(false); + const [feedback, setFeedback] = useState({ + open: false, + title: '', + isError: false + }); + + const handleCloseModal = () => setOpen(false); + + const handleShareByUrl = () => { + const quizUrl = `${window.location.origin}/teacher/share/${quiz._id}`; + navigator.clipboard.writeText(quizUrl) + .then(() => { + setFeedback({ + open: true, + title: 'L\'URL de partage pour le quiz', + isError: false + }); + }) + .catch(() => { + setFeedback({ + open: true, + title: 'Une erreur est survenue lors de la copie de l\'URL.', + isError: true + }); + }); + + handleCloseModal(); + }; + + const closeFeedback = () => { + setFeedback(prev => ({ ...prev, open: false })); + }; + + return ( + <> + + + + + + + {/* Feedback Dialog */} + + + + {feedback.isError ? ( + + {feedback.title} + + ) : ( + <> + + L'URL de partage pour le quiz{' '} + + + {quiz.title} + + + {' '}a été copiée. + + + )} + + + + + + + + ); +}; + +export default ShareQuizModal; \ No newline at end of file diff --git a/client/src/constants.tsx b/client/src/constants.tsx index ad5b80b..0cbbb9f 100644 --- a/client/src/constants.tsx +++ b/client/src/constants.tsx @@ -2,6 +2,7 @@ const ENV_VARIABLES = { MODE: process.env.MODE || "production", VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "", + IMG_URL: process.env.MODE == "development" ? process.env.VITE_BACKEND_URL : process.env.VITE_IMG_URL, BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}`:''}` : process.env.VITE_BACKEND_URL || '', FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}`:''}` : '' }; diff --git a/client/src/pages/Teacher/Dashboard/Dashboard.tsx b/client/src/pages/Teacher/Dashboard/Dashboard.tsx index cdae619..3d4cc65 100644 --- a/client/src/pages/Teacher/Dashboard/Dashboard.tsx +++ b/client/src/pages/Teacher/Dashboard/Dashboard.tsx @@ -39,9 +39,8 @@ import { FolderCopy, ContentCopy, Edit, - Share - // DriveFileMove } from '@mui/icons-material'; +import ShareQuizModal from 'src/components/ShareQuizModal/ShareQuizModal'; // Create a custom-styled Card component const CustomCard = styled(Card)({ @@ -401,28 +400,6 @@ const Dashboard: React.FC = () => { } }; - const handleShareQuiz = async (quiz: QuizType) => { - try { - const email = prompt( - `Veuillez saisir l'email de la personne avec qui vous souhaitez partager ce quiz`, - '' - ); - - if (email) { - const result = await ApiService.ShareQuiz(quiz._id, email); - - if (!result) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`); - return; - } - - window.alert(`Quiz partagé avec succès!`); - } - } catch (error) { - console.error('Erreur lors du partage du quiz:', error); - } - }; - return (
Tableau de bord
@@ -587,7 +564,7 @@ const Dashboard: React.FC = () => { {quizzesByFolder[folderName].map((quiz: QuizType) => (
- +
- + downloadTxtFile(quiz)} @@ -613,7 +590,7 @@ const Dashboard: React.FC = () => { - + handleEditQuiz(quiz)} @@ -623,7 +600,7 @@ const Dashboard: React.FC = () => { - + handleDuplicateQuiz(quiz)} @@ -633,7 +610,7 @@ const Dashboard: React.FC = () => { - + { - - handleShareQuiz(quiz)} - > - {' '} - {' '} - - +
+ +
))} diff --git a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx index 89f822a..ab7ea13 100644 --- a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx +++ b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx @@ -1,5 +1,5 @@ // EditorQuiz.tsx -import React, { useState, useEffect, useRef, CSSProperties } from 'react'; +import React, { useState, useEffect, CSSProperties } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { FolderType } from '../../../Types/FolderType'; @@ -11,12 +11,13 @@ import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview import { QuizType } from '../../../Types/QuizType'; import './editorQuiz.css'; -import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material'; +import { Button, TextField, NativeSelect, Divider } from '@mui/material'; import ReturnButton from 'src/components/ReturnButton/ReturnButton'; +import ImageGalleryModal from 'src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal'; import ApiService from '../../../services/ApiService'; import { escapeForGIFT } from '../../../utils/giftUtils'; -import { Upload } from '@mui/icons-material'; +import { ENV_VARIABLES } from '../../../constants'; interface EditQuizParams { id: string; @@ -38,8 +39,6 @@ const QuizForm: React.FC = () => { const handleSelectFolder = (event: React.ChangeEvent) => { setSelectedFolder(event.target.value); }; - const fileInputRef = useRef(null); - const [dialogOpen, setDialogOpen] = useState(false); const [showScrollButton, setShowScrollButton] = useState(false); const scrollToTop = () => { @@ -166,44 +165,15 @@ const QuizForm: React.FC = () => { return
Chargement...
; } - const handleSaveImage = async () => { - try { - const inputElement = document.getElementById('file-input') as HTMLInputElement; - - if (!inputElement?.files || inputElement.files.length === 0) { - setDialogOpen(true); - return; - } - - if (!inputElement.files || inputElement.files.length === 0) { - window.alert("Veuillez d'abord choisir une image à téléverser.") - return; - } - - const imageUrl = await ApiService.uploadImage(inputElement.files[0]); - - // Check for errors - if(imageUrl.indexOf("ERROR") >= 0) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) - return; - } - - setImageLinks(prevLinks => [...prevLinks, imageUrl]); - - // Reset the file input element - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - } catch (error) { - window.alert(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`) - - } - }; - const handleCopyToClipboard = async (link: string) => { navigator.clipboard.writeText(link); } + const handleCopyImage = (id: string) => { + const escLink = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`; + setImageLinks(prevLinks => [...prevLinks, escLink]); + } + return (
@@ -258,37 +228,11 @@ const QuizForm: React.FC = () => { onEditorChange={handleUpdatePreview} />
-
- - setDialogOpen(false)} > - Erreur - - Veuillez d'abord choisir une image à téléverser. - - - - - +
+

Mes images :

+
- -

Mes images :

+
(Voir section
@@ -302,7 +246,7 @@ const QuizForm: React.FC = () => {
    {imageLinks.map((link, index) => { - const imgTag = `![alt_text](${escapeForGIFT(link)} "texte de l'infobulle")`; + const imgTag = `[markdown]![alt_text](${escapeForGIFT(link)} "texte de l'infobulle") {T}`; return (
  • { } }; + const finishQuiz = () => { + disconnectWebSocket(); + navigate('/teacher/dashboard'); + }; + const handleReturn = () => { disconnectWebSocket(); navigate('/teacher/dashboard'); @@ -567,6 +572,11 @@ const ManageRoom: React.FC = () => {
)} +
+ +
) : ( { - console.log('Component rendered'); const navigate = useNavigate(); const { id } = useParams(); const [quizTitle, setQuizTitle] = useState(''); const [selectedFolder, setSelectedFolder] = useState(''); - const [folders, setFolders] = useState([]); + const [quizExists, setQuizExists] = useState(false); + const [loading, setLoading] = useState(true); useEffect(() => { const fetchData = async () => { - console.log("QUIZID : " + id) - if (!id) { - window.alert(`Une erreur est survenue.\n Le quiz n'a pas été trouvé\nVeuillez réessayer plus tard`) - console.error('Quiz not found for id:', id); + try { + if (!id) { + console.error('Quiz not found for id:', id); + navigate('/teacher/dashboard'); + return; + } + + if (!ApiService.isLoggedIn()) { + navigate("/login"); + return; + } + + const quizIds = await ApiService.getAllQuizIds(); + + if (quizIds.includes(id)) { + setQuizExists(true); + setLoading(false); + return; + } + + const userFolders = await ApiService.getUserFolders(); + + if (userFolders.length == 0) { + navigate('/teacher/dashboard'); + return; + } + + setFolders(userFolders as FolderType[]); + + const title = await ApiService.getSharedQuiz(id); + + if (!title) { + console.error('Quiz not found for id:', id); + navigate('/teacher/dashboard'); + return; + } + + setQuizTitle(title); + setLoading(false); + } catch (error) { + console.error('Error fetching data:', error); + setLoading(false); navigate('/teacher/dashboard'); - return; } - - if (!ApiService.isLoggedIn()) { - window.alert(`Vous n'êtes pas connecté.\nVeuillez vous connecter et revenir à ce lien`); - navigate("/login"); - return; - } - - const userFolders = await ApiService.getUserFolders(); - - if (userFolders.length == 0) { - window.alert(`Vous n'avez aucun dossier.\nVeuillez en créer un et revenir à ce lien`) - navigate('/teacher/dashboard'); - return; - } - - setFolders(userFolders as FolderType[]); - - const title = await ApiService.getSharedQuiz(id); - - if (!title) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) - console.error('Quiz not found for id:', id); - navigate('/teacher/dashboard'); - return; - } - - setQuizTitle(title); }; fetchData(); - }, []); + }, [id, navigate]); const handleSelectFolder = (event: React.ChangeEvent) => { setSelectedFolder(event.target.value); @@ -68,14 +73,12 @@ const Share: React.FC = () => { const handleQuizSave = async () => { try { - if (selectedFolder == '') { alert("Veuillez choisir un dossier"); return; } if (!id) { - window.alert(`Une erreur est survenue.\n Le quiz n'a pas été trouvé\nVeuillez réessayer plus tard`) console.error('Quiz not found for id:', id); navigate('/teacher/dashboard'); return; @@ -85,49 +88,91 @@ const Share: React.FC = () => { navigate('/teacher/dashboard'); } catch (error) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) console.log(error) } }; + if (loading) { + return
Chargement...
; + } + + if (quizExists) { + return ( +
+
+ +
+
Quiz déjà existant
+
+
+
+ +
+ + + Le quiz que vous essayez d'importer existe déjà sur votre compte. + + + + + + Si vous souhaitiez créer une copie de ce quiz, + vous pouvez utiliser la fonction "Dupliquer" disponible + dans votre tableau de bord. + + +
+
+ ); + } + return (
-
- -
Importer quiz: {quizTitle}
- +
+
Importation du Quiz: {quizTitle}
+
+ Vous êtes sur le point d'importer le quiz {quizTitle}, choisissez un dossier dans lequel enregistrer ce nouveau quiz. +
+
- -
- +
- {folders.map((folder: FolderType) => ( ))} - -
-
-
); }; -export default Share; +export default Share; \ No newline at end of file diff --git a/client/src/pages/Teacher/Share/share.css b/client/src/pages/Teacher/Share/share.css index 119b645..146f318 100644 --- a/client/src/pages/Teacher/Share/share.css +++ b/client/src/pages/Teacher/Share/share.css @@ -3,19 +3,58 @@ display: flex; flex-direction: row; justify-content: space-between; - align-content: stretch + align-content: stretch; } + .quizImport .importHeader .returnButton { flex-basis: 10%; - display: flex; justify-content: center; } -.quizImport .importHeader .title { +.quizImport .importHeader .titleContainer { flex-basis: auto; + text-align: center; /* Center the title and subtitle */ +} + +.quizImport .importHeader .mainTitle { + font-size: 44px; /* Larger font size for the main title */ + font-weight: bold; /* Remove bold */ + color: #333; /* Slightly paler color */ +} + +.quizImport .importHeader .subTitle { + font-size: 14px; /* Smaller font size for the subtitle */ + color: #666; /* Pale gray color */ + margin-top: 8px; /* Add some space between the title and subtitle */ } .quizImport .importHeader .dumb { flex-basis: 10%; +} + +.quizImport .editSection { + display: flex; + justify-content: center; + align-items: center; + margin-top: 20px; +} + +.quizImport .editSection .formContainer { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; /* Adds space between the select and button */ + width: 100%; + max-width: 400px; /* Limits the width of the form container */ +} + +.quizImport .editSection .folderSelect { + width: 100%; /* Ensures the select element takes the full width of its container */ +} + +.quizImport .editSection .saveButton { + width: 100%; /* Makes the button take the full width of its container */ + padding: 10px 20px; /* Increases the button's padding for a larger appearance */ + font-size: 16px; /* Increases the font size for better visibility */ } \ No newline at end of file diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index b13a369..48b9124 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -3,6 +3,7 @@ import { jwtDecode } from 'jwt-decode'; import { ENV_VARIABLES } from '../constants'; import { FolderType } from 'src/Types/FolderType'; +import { ImagesResponse, ImagesParams } from '../Types/Images'; import { QuizType } from 'src/Types/QuizType'; import { RoomType } from 'src/Types/RoomType'; @@ -74,8 +75,6 @@ class ApiService { return false; } - console.log("ApiService: isLoggedIn: Token:", token); - // Update token expiry this.saveToken(token); @@ -91,7 +90,6 @@ class ApiService { } try { - console.log("ApiService: isLoggedInTeacher: Token:", token); const decodedToken = jwtDecode(token) as { roles: string[] }; /////// REMOVE BELOW @@ -103,7 +101,6 @@ class ApiService { const userRoles = decodedToken.roles; const requiredRole = 'teacher'; - console.log("ApiService: isLoggedInTeacher: UserRoles:", userRoles); if (!userRoles || !userRoles.includes(requiredRole)) { return false; } @@ -142,6 +139,22 @@ class ApiService { return object.username; } + public getUserID(): string { + const token = localStorage.getItem("jwt"); + + if (!token) { + return ""; + } + + const jsonObj = jwtDecode(token) as { userId: string }; + + if (!jsonObj.userId) { + return ""; + } + + return jsonObj.userId; + } + // Route to know if rooms need authentication to join public async getRoomsRequireAuth(): Promise { const url: string = this.constructRequestUrl(`/auth/getRoomsRequireAuth`); @@ -178,7 +191,6 @@ class ApiService { const result: AxiosResponse = await axios.post(url, body, { headers: headers }); - console.log(result); if (result.status == 200) { //window.location.href = result.request.responseURL; window.location.href = '/login'; @@ -190,7 +202,6 @@ class ApiService { return true; } catch (error) { - console.log("Error details: ", error); if (axios.isAxiosError(error)) { const err = error as AxiosError; @@ -553,7 +564,6 @@ public async login(email: string, password: string): Promise { const headers = this.constructRequestHeaders(); const body = { folderId }; - console.log(headers); const result: AxiosResponse = await axios.post(url, body, { headers: headers }); if (result.status !== 200) { @@ -843,36 +853,6 @@ public async login(email: string, password: string): Promise { } } - async ShareQuiz(quizId: string, email: string): Promise { - try { - if (!quizId || !email) { - throw new Error(`quizId and email are required.`); - } - - const url: string = this.constructRequestUrl(`/quiz/Share`); - const headers = this.constructRequestHeaders(); - const body = { quizId, email }; - - const result: AxiosResponse = await axios.put(url, body, { headers: headers }); - - if (result.status !== 200) { - throw new Error(`Update and share quiz failed. Status: ${result.status}`); - } - - return true; - } catch (error) { - console.log("Error details: ", error); - - if (axios.isAxiosError(error)) { - const err = error as AxiosError; - const data = err.response?.data as { error: string } | undefined; - return data?.error || 'Unknown server error during request.'; - } - - return `An unexpected error occurred.`; - } - } - async getSharedQuiz(quizId: string): Promise { try { if (!quizId) { @@ -1167,7 +1147,126 @@ public async login(email: string, password: string): Promise { return `ERROR : Une erreur inattendue s'est produite.` } } - // NOTE : Get Image pas necessaire + + public async getImages(page: number, limit: number): Promise { + try { + const url: string = this.constructRequestUrl(`/image/getImages`); + const headers = this.constructRequestHeaders(); + let params : ImagesParams = { page: page, limit: limit }; + + const result: AxiosResponse = await axios.get(url, { params: params, headers: headers }); + + if (result.status !== 200) { + throw new Error(`L'affichage des images a échoué. Status: ${result.status}`); + } + const images = result.data; + + return images; + + } catch (error) { + console.log("Error details: ", error); + + if (axios.isAxiosError(error)) { + const err = error as AxiosError; + const data = err.response?.data as { error: string } | undefined; + const msg = data?.error || 'Erreur serveur inconnue lors de la requête.'; + throw new Error(`L'enregistrement a échoué. Status: ${msg}`); + } + + throw new Error(`ERROR : Une erreur inattendue s'est produite.`); + } + } + + public async getUserImages(page: number, limit: number): Promise { + try { + const url: string = this.constructRequestUrl(`/image/getUserImages`); + const headers = this.constructRequestHeaders(); + let params : ImagesParams = { page: page, limit: limit }; + + const uid = this.getUserID(); + if(uid !== ''){ + params.uid = uid; + } + + const result: AxiosResponse = await axios.get(url, { params: params, headers: headers }); + + if (result.status !== 200) { + throw new Error(`L'affichage des images de l'utilisateur a échoué. Status: ${result.status}`); + } + const images = result.data; + + return images; + + } catch (error) { + console.log("Error details: ", error); + + if (axios.isAxiosError(error)) { + const err = error as AxiosError; + const data = err.response?.data as { error: string } | undefined; + const msg = data?.error || 'Erreur serveur inconnue lors de la requête.'; + throw new Error(`L'enregistrement a échoué. Status: ${msg}`); + } + + throw new Error(`ERROR : Une erreur inattendue s'est produite.`); + } + } + + public async deleteImage(imgId: string): Promise { + try { + const url: string = this.constructRequestUrl(`/image/delete`); + const headers = this.constructRequestHeaders(); + const uid = this.getUserID(); + let params = { uid: uid, imgId: imgId }; + + const result: AxiosResponse = await axios.delete(url, { params: params, headers: headers }); + + if (result.status !== 200) { + throw new Error(`La suppression de l'image a échoué. Status: ${result.status}`); + } + + const deleted = result.data.deleted; + return deleted; + + } catch (error) { + console.log("Error details: ", error); + + if (axios.isAxiosError(error)) { + const err = error as AxiosError; + const data = err.response?.data as { error: string } | undefined; + const msg = data?.error || 'Erreur serveur inconnue lors de la requête.'; + throw new Error(`L'enregistrement a échoué. Status: ${msg}`); + } + + throw new Error(`ERROR : Une erreur inattendue s'est produite.`); + } + } + + public async getAllQuizIds(): Promise { + try { + const folders = await this.getUserFolders(); + + const allQuizIds: string[] = []; + + if (Array.isArray(folders)) { + for (const folder of folders) { + const folderQuizzes = await this.getFolderContent(folder._id); + + if (Array.isArray(folderQuizzes)) { + allQuizIds.push(...folderQuizzes.map(quiz => quiz._id)); + } + } + } else { + console.error('Failed to get user folders:', folders); + } + + return allQuizIds; + } catch (error) { + console.error('Failed to get all quiz ids:', error); + throw error; + } + } + + } diff --git a/docker-compose-local.yaml b/docker-compose-local.yaml index 0d8d61a..b77c340 100644 --- a/docker-compose-local.yaml +++ b/docker-compose-local.yaml @@ -7,6 +7,8 @@ services: context: ./client dockerfile: Dockerfile container_name: frontend + environment: + VITE_IMG_URL: http://localhost ports: - "5173:5173" restart: always @@ -54,7 +56,7 @@ services: image: mongo container_name: mongo ports: - - "27017:27017" + - "27019:27017" tty: true volumes: - mongodb_data:/data/db diff --git a/docker-compose.yaml b/docker-compose.yaml index 539c800..d133982 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,6 +10,7 @@ services: - VITE_BACKEND_URL= # Define empty VITE_BACKEND_SOCKET_URL so it will default to window.location.host - VITE_BACKEND_SOCKET_URL= + - VITE_IMG_URL=https://evalsa.etsmtl.ca ports: - "5173:5173" restart: always diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..144d2f8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,329 @@ +{ + "name": "EvalueTonSavoir", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "axios-mock-adapter": "^2.1.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "peer": true + }, + "node_modules/axios": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", + "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "peer": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios-mock-adapter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.1.0.tgz", + "integrity": "sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "peer": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "peer": true, + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "peer": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a8332a4 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "axios-mock-adapter": "^2.1.0" + } +} diff --git a/server/__tests__/image.test.js b/server/__tests__/image.test.js index 488d27b..7ce4ad1 100644 --- a/server/__tests__/image.test.js +++ b/server/__tests__/image.test.js @@ -7,6 +7,9 @@ // const BASE_URL = '/image' +const Images = require('../models/images'); +const ObjectId = require('mongodb').ObjectId; + describe.skip("POST /upload", () => { describe("when the jwt is not sent", () => { @@ -64,3 +67,289 @@ describe.skip("GET /get", () => { }) }) + +jest.mock('mongodb', () => { + const originalModule = jest.requireActual('mongodb'); + return { + ...originalModule, + ObjectId: { + ...originalModule.ObjectId, + createFromHexString: jest.fn().mockReturnValue('507f191e810c19729de860ea'), // Return a valid 24-character ObjectId string + }, + }; +}); + +describe('Images', () => { + let db; + let images; + let dbConn; + let mockImagesCollection; + let mockFindCursor; + + beforeEach(() => { + + const mockImagesCursor = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + toArray: jest.fn() + }; + + const mockFilesCursor = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + toArray: jest.fn() + }; + + mockImagesCollection = { + insertOne: jest.fn().mockResolvedValue({ insertedId: 'image123' }), + findOne: jest.fn(), + find: jest.fn().mockReturnValue(mockImagesCursor), + countDocuments: jest.fn(), + deleteOne: jest.fn() + }; + + mockFilesCollection = { + find: jest.fn().mockReturnValue(mockFilesCursor) + }; + + dbConn = { + collection: jest.fn((name) => { + if (name === 'images') { + return mockImagesCollection; + } else if (name === 'files') { + return mockFilesCollection; + } + }) + }; + + db = { + connect: jest.fn().mockResolvedValue(), + getConnection: jest.fn().mockReturnValue(dbConn) + }; + + images = new Images(db); + }); + + describe('upload', () => { + it('should upload an image and return the inserted ID', async () => { + const testFile = { originalname: 'test.png', buffer: Buffer.from('dummydata'), mimetype: 'image/png' }; + const userId = 'user123'; + + const result = await images.upload(testFile, userId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(dbConn.collection).toHaveBeenCalledWith('images'); + expect(mockImagesCollection.insertOne).toHaveBeenCalledWith({ + userId: userId, + file_name: 'test.png', + file_content: testFile.buffer.toString('base64'), + mime_type: 'image/png', + created_at: expect.any(Date) + }); + expect(result).toBe('image123'); + }); + }); + + describe('get', () => { + it('should retrieve the image if found', async () => { + const imageId = '65d9a8f9b5e8d1a5e6a8c9f0'; + const testImage = { + file_name: 'test.png', + file_content: Buffer.from('dummydata').toString('base64'), + mime_type: 'image/png' + }; + mockImagesCollection.findOne.mockResolvedValue(testImage); + + const result = await images.get(imageId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(dbConn.collection).toHaveBeenCalledWith('images'); + expect(mockImagesCollection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromHexString(imageId) }); + expect(result).toEqual({ + file_name: 'test.png', + file_content: Buffer.from('dummydata'), + mime_type: 'image/png' + }); + }); + + it('should return null if image is not found', async () => { + const imageId = '65d9a8f9b5e8d1a5e6a8c9f0'; + mockImagesCollection.findOne.mockResolvedValue(null); + + const result = await images.get(imageId); + + expect(result).toBeNull(); + }); + }); + + describe('getImages', () => { + it('should retrieve a paginated list of images', async () => { + const mockImages = [ + { _id: '1', userId: 'user1', file_name: 'image1.png', file_content: Buffer.from('data1'), mime_type: 'image/png' }, + { _id: '2', userId: 'user2', file_name: 'image2.png', file_content: Buffer.from('data2'), mime_type: 'image/png' } + ]; + + mockImagesCollection.countDocuments.mockResolvedValue(2); + // Create a mock cursor for images collection + const mockFindCursor = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + toArray: jest.fn().mockResolvedValue(mockImages), // Return mock images when toArray is called + }; + + // Mock the find method to return the mock cursor + mockImagesCollection.find.mockReturnValue(mockFindCursor); + const result = await images.getImages(1, 10); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(dbConn.collection).toHaveBeenCalledWith('images'); + expect(mockImagesCollection.find).toHaveBeenCalledWith({}); + expect(mockFindCursor.sort).toHaveBeenCalledWith({ created_at: 1 }); + expect(mockFindCursor.skip).toHaveBeenCalledWith(0); + expect(mockFindCursor.limit).toHaveBeenCalledWith(10); + expect(result).toEqual({ + images: [ + { id: '1', user: 'user1', file_name: 'image1.png', file_content: 'ZGF0YTE=', mime_type: 'image/png' }, + { id: '2', user: 'user2', file_name: 'image2.png', file_content: 'ZGF0YTI=', mime_type: 'image/png' } + ], + total: 2, + }); + }); + + it('should return an empty array if no images are found', async () => { + mockImagesCollection.countDocuments.mockResolvedValue(0); + + const result = await images.getImages(1, 10); + + expect(result).toEqual({ images: [], total: 0 }); + }); + }); + + describe('getUserImages', () => { + it('should return empty images array when no images exist', async () => { + mockImagesCollection.countDocuments.mockResolvedValue(0); + + const result = await images.getUserImages(1, 10, 'user123'); + + expect(result).toEqual({ images: [], total: 0 }); + expect(db.connect).toHaveBeenCalled(); + expect(mockImagesCollection.countDocuments).toHaveBeenCalledWith({ userId: 'user123' }); + }); + + it('should return images when they exist', async () => { + const mockImages = [ + { + _id: 'img1', + userId: 'user123', + file_name: 'image1.png', + file_content: Buffer.from('testdata'), + mime_type: 'image/png' + } + ]; + + mockImagesCollection.countDocuments.mockResolvedValue(1); + mockImagesCollection.find.mockReturnValue({ + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + toArray: jest.fn().mockResolvedValue(mockImages) + }); + + const result = await images.getUserImages(1, 10, 'user123'); + + expect(result).toEqual({ + images: [ + { + id: 'img1', + user: 'user123', + file_name: 'image1.png', + file_content: Buffer.from('testdata').toString('base64'), + mime_type: 'image/png' + } + ], + total: 1 + }); + expect(db.connect).toHaveBeenCalled(); + expect(mockImagesCollection.countDocuments).toHaveBeenCalledWith({ userId: 'user123' }); + }); + }); + describe('delete', () => { + it('should not delete the image when it exists in the files collection', async () => { + const uid = 'user123'; + const imgId = '507f191e810c19729de860ea'; // A valid 24-character ObjectId string + + // Mock the files collection cursor to simulate an image found + const mockFilesCursor = { + toArray: jest.fn().mockResolvedValue([{ _id: imgId }]) // Image found + }; + + mockFilesCollection.find.mockReturnValue(mockFilesCursor); + mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 0 }); + + const result = await images.delete(uid, imgId); + + // Ensure the files collection is queried + expect(dbConn.collection).toHaveBeenCalledWith('files'); + expect(mockFilesCollection.find).toHaveBeenCalledWith({ + userId: uid, + content: { $regex: new RegExp(`/api/image/get/${imgId}`) }, + }); + + // Ensure the images collection is queried for deletion + expect(dbConn.collection).toHaveBeenCalledWith('files'); + expect(mockImagesCollection.deleteOne).not.toHaveBeenCalledWith({ + _id: ObjectId.createFromHexString(imgId), // Ensure the ObjectId is created correctly + }); + + expect(result).toEqual({ deleted: false }); + }); + + it('should delete the image if not found in the files collection', async () => { + const uid = 'user123'; + const imgId = '507f191e810c19729de860ea'; + + // Mock the files collection cursor to simulate the image not being found + const mockFindCursor = { + toArray: jest.fn().mockResolvedValue([]) // Empty array means image not found + }; + + mockFilesCollection.find.mockReturnValue(mockFindCursor); + mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 1 }); + + const result = await images.delete(uid, imgId); + + // Ensure the deleteOne is not called if the image is not found + expect(mockImagesCollection.deleteOne).toHaveBeenCalled(); + expect(result).toEqual({ deleted: true }); + }); + + it('should return false if the delete operation fails in the images collection', async () => { + const uid = 'user123'; + const imgId = '507f191e810c19729de860ea'; + + // Mock the files collection cursor to simulate the image being found + const mockFindCursor = { + toArray: jest.fn().mockResolvedValue([{ _id: imgId }]) // Image found + }; + + mockFilesCollection.find.mockReturnValue(mockFindCursor); + mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 0 }); // Simulate failure + + const result = await images.delete(uid, imgId); + + // Ensure the images collection deletion is called + expect(mockImagesCollection.deleteOne).not.toHaveBeenCalledWith({ + _id: ObjectId.createFromHexString(imgId), // Ensure the ObjectId is created correctly + }); + + expect(result).toEqual({ deleted: false }); + }); + + }); +}); \ No newline at end of file diff --git a/server/controllers/images.js b/server/controllers/images.js index b77ed96..7de02ad 100644 --- a/server/controllers/images.js +++ b/server/controllers/images.js @@ -50,6 +50,58 @@ class ImagesController { } }; + getImages = async (req, res, next) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 5; + const images = await this.images.getImages(page, limit); + + if (!images || images.length === 0) { + throw new AppError(IMAGE_NOT_FOUND); + } + + res.setHeader('Content-Type', 'application/json'); + return res.status(200).json(images); + } catch (error) { + return next(error); + } + }; + + getUserImages = async (req, res, next) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 5; + const uid = req.query.uid; + const images = await this.images.getUserImages(page, limit, uid); + + if (!images || images.length === 0) { + throw new AppError(IMAGE_NOT_FOUND); + } + + res.setHeader('Content-Type', 'application/json'); + return res.status(200).json(images); + } catch (error) { + return next(error); + } + }; + + delete = async (req, res, next) => { + try { + const uid = req.query.uid; + const imgId = req.query.imgId; + + if (!uid || !imgId) { + throw new AppError(MISSING_REQUIRED_PARAMETER); + } + const images = await this.images.delete(uid, imgId); + + res.setHeader('Content-Type', 'application/json'); + return res.status(200).json(images); + } catch (error) { + return next(error); + } + }; + } module.exports = ImagesController; diff --git a/server/models/images.js b/server/models/images.js index 67a6583..e3b6afd 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -42,6 +42,84 @@ class Images { }; } + async getImages(page, limit) { + await this.db.connect() + const conn = this.db.getConnection(); + + const imagesCollection = conn.collection('images'); + + + const total = await imagesCollection.countDocuments(); + if (!total || total === 0) return { images: [], total }; + + const result = await imagesCollection.find({}) + .sort({ created_at: 1 }) + .skip((page - 1) * limit) + .limit(limit) + .toArray(); + + const objImages = result.map(image => ({ + id: image._id, + user: image.userId, + file_name: image.file_name, + file_content: image.file_content.toString('base64'), + mime_type: image.mime_type + })); + + let respObj = { + images: objImages, + total: total + } + + return respObj; + } + + async getUserImages(page, limit, uid) { + await this.db.connect() + const conn = this.db.getConnection(); + const imagesCollection = conn.collection('images'); + const total = await imagesCollection.countDocuments({ userId: uid }); + if (!total || total === 0) return { images: [], total }; + + const result = await imagesCollection.find({ userId: uid }) + .sort({ created_at: -1 }) + .skip((page - 1) * limit) + .limit(limit) + .toArray(); + + const objImages = result.map(image => ({ + id: image._id, + user: image.userId, + file_name: image.file_name, + file_content: image.file_content.toString('base64'), + mime_type: image.mime_type + })); + + let respObj = { + images: objImages, + total: total + } + + return respObj; + } + + async delete(uid, imgId) { + let resp = false; + await this.db.connect() + const conn = this.db.getConnection(); + const quizColl = conn.collection('files'); + const rgxImg = new RegExp(`/api/image/get/${imgId}`); + + const result = await quizColl.find({ userId: uid, content: { $regex: rgxImg }}).toArray(); + if(!result || result.length < 1){ + const imgsColl = conn.collection('images'); + const isDeleted = await imgsColl.deleteOne({ _id: ObjectId.createFromHexString(imgId) }); + if(isDeleted){ + resp = true; + } + } + return { deleted: resp }; + } } module.exports = Images; diff --git a/server/package-lock.json b/server/package-lock.json index 7f33922..66bd45a 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -39,7 +39,7 @@ "supertest": "^6.3.4" }, "engines": { - "node": "20.x" + "node": "22.x" } }, "node_modules/@ampproject/remapping": { @@ -56,68 +56,20 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/@babel/compat-data": { "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", @@ -343,19 +295,21 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -370,88 +324,28 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/@babel/parser": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", - "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.10" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -637,14 +531,15 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" @@ -705,14 +600,14 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", - "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -4773,7 +4668,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "3.14.1", @@ -6703,15 +6599,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/server/package.json b/server/package.json index b419a8f..0eaf6d2 100644 --- a/server/package.json +++ b/server/package.json @@ -43,7 +43,7 @@ "supertest": "^6.3.4" }, "engines": { - "node": "20.x" + "node": "22.x" }, "jest": { "testEnvironment": "node", diff --git a/server/routers/images.js b/server/routers/images.js index 06e2830..94d2802 100644 --- a/server/routers/images.js +++ b/server/routers/images.js @@ -12,5 +12,8 @@ const upload = multer({ storage: storage }); router.post("/upload", jwt.authenticate, upload.single('image'), asyncHandler(images.upload)); router.get("/get/:id", asyncHandler(images.get)); +router.get("/getImages", asyncHandler(images.getImages)); +router.get("/getUserImages", jwt.authenticate, asyncHandler(images.getUserImages)); +router.delete("/delete", jwt.authenticate, asyncHandler(images.delete)); module.exports = router;