From 796b4379fdeff972b43fb6b26261641a0965a775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 09:36:01 +0400 Subject: [PATCH 01/28] test: ajouter Vitest au backend --- package.json | 7 +- vitest.config.mjs | 7 + yarn.lock | 443 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 453 insertions(+), 4 deletions(-) create mode 100644 vitest.config.mjs diff --git a/package.json b/package.json index b3da853..1f682ef 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,12 @@ "dev": "strapi develop", "start": "strapi start", "build": "strapi build", - "strapi": "strapi" + "strapi": "strapi", + "test": "vitest run" + }, + "devDependencies": { + "vitest": "^4.1.9" }, - "devDependencies": {}, "dependencies": { "@strapi/plugin-users-permissions": "5.44.0", "@strapi/provider-email-nodemailer": "^4.26.1", diff --git a/vitest.config.mjs b/vitest.config.mjs new file mode 100644 index 0000000..e28d08c --- /dev/null +++ b/vitest.config.mjs @@ -0,0 +1,7 @@ +import {defineConfig} from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'node', + }, +}) diff --git a/yarn.lock b/yarn.lock index 7e60de6..520f2f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -661,6 +661,21 @@ dependencies: tslib "^2.0.0" +"@emnapi/core@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.1.tgz#b9e1064f3a6b1631e241e638eb48d736bfd372a6" + integrity sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ== + dependencies: + "@emnapi/wasi-threads" "1.2.2" + tslib "^2.4.0" + +"@emnapi/runtime@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.1.tgz#58f1f3d5d81a9b12f793ab688c96371901027c24" + integrity sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw== + dependencies: + tslib "^2.4.0" + "@emnapi/runtime@^1.2.0": version "1.10.0" resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.10.0.tgz#4b260c0d3534204e98c6110b8db1a987d26ec87c" @@ -668,6 +683,13 @@ dependencies: tslib "^2.4.0" +"@emnapi/wasi-threads@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz#4c93becf5bfa3b13d1bbdcc06aee38321ad8139a" + integrity sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA== + dependencies: + tslib "^2.4.0" + "@emotion/babel-plugin@^11.10.5": version "11.10.5" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz#65fa6e1790ddc9e23cc22658a4c5dea423c55c3c" @@ -1352,7 +1374,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -"@jridgewell/sourcemap-codec@^1.5.0": +"@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": version "1.5.5" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== @@ -1460,6 +1482,13 @@ hls.js "~1.5.11" mux-embed "^5.3.1" +"@napi-rs/wasm-runtime@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz#ed33806d0f9be98dc76d0c3d4fd872fda701b5d5" + integrity sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg== + dependencies: + "@tybys/wasm-util" "^0.10.3" + "@noble/hashes@^1.1.5": version "1.8.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" @@ -1491,6 +1520,11 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== +"@oxc-project/types@=0.138.0": + version "0.138.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.138.0.tgz#ce9690ec80144b4c38dfed44e76a67b911df9aca" + integrity sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA== + "@paralleldrive/cuid2@2.2.2": version "2.2.2" resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz#7f91364d53b89e2c9cb9e02e8dd0f129e834455f" @@ -2200,6 +2234,90 @@ resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.23.2.tgz#156c4b481c0bee22a19f7924728a67120de06971" integrity sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w== +"@rolldown/binding-android-arm64@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz#6c31f378f42d5ea75b37970d2add539682264792" + integrity sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw== + +"@rolldown/binding-darwin-arm64@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz#1dbbbbaf2b00d13b32127cbf3a379d6ef69fd647" + integrity sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ== + +"@rolldown/binding-darwin-x64@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz#3b8f76dfd462e28e8914e7ded63647194866329f" + integrity sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg== + +"@rolldown/binding-freebsd-x64@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz#7f3f82c6a7b9fcec4e24c29e89d8343c9c72d403" + integrity sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ== + +"@rolldown/binding-linux-arm-gnueabihf@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz#baee435c7addf84e58fee72c70e98acc603aba3e" + integrity sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA== + +"@rolldown/binding-linux-arm64-gnu@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz#cc8c63913bac68bf1f49a0d095634e09008741ce" + integrity sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w== + +"@rolldown/binding-linux-arm64-musl@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz#aaa1d02f5ee705bd6812ff84ab1f7752ba4e4497" + integrity sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng== + +"@rolldown/binding-linux-ppc64-gnu@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz#9e26bb9ba2a846039902270b778d1d3ba0b62033" + integrity sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg== + +"@rolldown/binding-linux-s390x-gnu@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz#d9ec4d5b1bd6f8029072670da9f0d58e728e8c44" + integrity sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ== + +"@rolldown/binding-linux-x64-gnu@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz#6cc9cad61ea020ac2191037755a36f8fcfa11434" + integrity sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw== + +"@rolldown/binding-linux-x64-musl@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz#d23df39a184be427877da934fc3c30e3f25cd686" + integrity sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ== + +"@rolldown/binding-openharmony-arm64@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz#189d1a062624f57f9b0a52f94163d51b6aa692ab" + integrity sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA== + +"@rolldown/binding-wasm32-wasi@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz#98c634b91a9abf5de74b8402aa8063fb5044efd4" + integrity sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg== + dependencies: + "@emnapi/core" "1.11.1" + "@emnapi/runtime" "1.11.1" + "@napi-rs/wasm-runtime" "^1.1.6" + +"@rolldown/binding-win32-arm64-msvc@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz#1c8c8515c811ae28fc26355079b80f617fdddb0b" + integrity sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA== + +"@rolldown/binding-win32-x64-msvc@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz#e14b1ef53473f51a4180d9b46c5f0cea9f1713cb" + integrity sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ== + +"@rolldown/pluginutils@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be" + integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw== + "@rollup/rollup-android-arm-eabi@4.60.1": version "4.60.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz#043f145716234529052ef9e1ce1d847ffbe9e674" @@ -2361,7 +2479,7 @@ escape-string-regexp "^2.0.0" lodash.deburr "^4.1.0" -"@standard-schema/spec@^1.0.0": +"@standard-schema/spec@^1.0.0", "@standard-schema/spec@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== @@ -3179,6 +3297,13 @@ resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== +"@tybys/wasm-util@^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz#015cba9e9dd47ce14d03d2a8c5d547bfb169665d" + integrity sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg== + dependencies: + tslib "^2.4.0" + "@types/accepts@*": version "1.3.7" resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.7.tgz#3b98b1889d2b2386604c2bbbe62e4fb51e95b265" @@ -3214,6 +3339,14 @@ "@types/node" "*" "@types/responselike" "*" +"@types/chai@^5.2.2": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a" + integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== + dependencies: + "@types/deep-eql" "*" + assertion-error "^2.0.1" + "@types/co-body@^6.1.0": version "6.1.3" resolved "https://registry.yarnpkg.com/@types/co-body/-/co-body-6.1.3.tgz#201796c6389066b400cfcb4e1ec5c3db798265a2" @@ -3251,6 +3384,11 @@ dependencies: "@types/ms" "*" +"@types/deep-eql@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd" + integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== + "@types/eslint-scope@^3.7.7": version "3.7.7" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" @@ -3697,6 +3835,66 @@ dependencies: "@swc/core" "^1.3.107" +"@vitest/expect@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.1.9.tgz#ba1af73ae53262e3dc9b518cb7b76fb614e0ef53" + integrity sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA== + dependencies: + "@standard-schema/spec" "^1.1.0" + "@types/chai" "^5.2.2" + "@vitest/spy" "4.1.9" + "@vitest/utils" "4.1.9" + chai "^6.2.2" + tinyrainbow "^3.1.0" + +"@vitest/mocker@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.1.9.tgz#a483de79b358aba3dd8f319a0d8ab17c89f5c75d" + integrity sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw== + dependencies: + "@vitest/spy" "4.1.9" + estree-walker "^3.0.3" + magic-string "^0.30.21" + +"@vitest/pretty-format@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.1.9.tgz#885cfe9fcb6ff3df4409ea66192cc1fb23d62fae" + integrity sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A== + dependencies: + tinyrainbow "^3.1.0" + +"@vitest/runner@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.1.9.tgz#bb742947ce4841dfb2d8984a2f9014850be10f51" + integrity sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg== + dependencies: + "@vitest/utils" "4.1.9" + pathe "^2.0.3" + +"@vitest/snapshot@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.1.9.tgz#bdfb670ae5617613ea8776e93d0666a66defeeb7" + integrity sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA== + dependencies: + "@vitest/pretty-format" "4.1.9" + "@vitest/utils" "4.1.9" + magic-string "^0.30.21" + pathe "^2.0.3" + +"@vitest/spy@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.1.9.tgz#bfc40d48fb9bd1a1228bfbfde7f5555e7f6b3867" + integrity sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA== + +"@vitest/utils@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.1.9.tgz#0184c7e6eb3234739b2b6b3b985f78d1ed823ee1" + integrity sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA== + dependencies: + "@vitest/pretty-format" "4.1.9" + convert-source-map "^2.0.0" + tinyrainbow "^3.1.0" + "@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": version "1.14.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" @@ -4082,6 +4280,11 @@ asn1.js@^5.3.0: minimalistic-assert "^1.0.0" safer-buffer "^2.1.0" +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + ast-types@^0.16.1: version "0.16.1" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.16.1.tgz#7a9da1617c9081bc121faafe91711b4c8bb81da2" @@ -4451,6 +4654,11 @@ ce-la-react@^0.3.2: resolved "https://registry.yarnpkg.com/ce-la-react/-/ce-la-react-0.3.2.tgz#66c1454e024c3b9f65ebac05724d0f50756ff057" integrity sha512-QJ6k4lOD/btI08xG8jBPxRCGXvCnusGGkTsiXk0u3NqUu/W+BXRnFD4PYjwtqh8AWmGa5LDbGk0fLQsqr0nSMA== +chai@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e" + integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg== + chalk@4.1.2, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -5675,6 +5883,13 @@ estree-util-is-identifier-name@^3.0.0: resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz#0b5ef4c4ff13508b34dcd01ecfa945f61fce5dbd" integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -5717,6 +5932,11 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" +expect-type@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.4.0.tgz#24edf7f0cc69a44d008567ba4594ab96f3c3a3d6" + integrity sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA== + extend@^3.0.0, extend@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -7439,6 +7659,80 @@ liftoff@^5.0.1: rechoir "^0.8.0" resolve "^1.20.0" +lightningcss-android-arm64@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968" + integrity sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg== + +lightningcss-darwin-arm64@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz#50b71871b01c8199584b649e292547faea7af9b5" + integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ== + +lightningcss-darwin-x64@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz#35f3e97332d130b9ca181e11b568ded6aebc6d5e" + integrity sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w== + +lightningcss-freebsd-x64@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz#9777a76472b64ed6ff94342ad64c7bafd794a575" + integrity sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig== + +lightningcss-linux-arm-gnueabihf@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz#13ae652e1ab73b9135d7b7da172f666c410ad53d" + integrity sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw== + +lightningcss-linux-arm64-gnu@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz#417858795a94592f680123a1b1f9da8a0e1ef335" + integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ== + +lightningcss-linux-arm64-musl@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz#6be36692e810b718040802fd809623cffe732133" + integrity sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg== + +lightningcss-linux-x64-gnu@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz#0b7803af4eb21cfd38dd39fe2abbb53c7dd091f6" + integrity sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA== + +lightningcss-linux-x64-musl@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz#88dc8ba865ddddb1ac5ef04b0f161804418c163b" + integrity sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg== + +lightningcss-win32-arm64-msvc@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz#4f30ba3fa5e925f5b79f945e8cc0d176c3b1ab38" + integrity sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw== + +lightningcss-win32-x64-msvc@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz#141aa5605645064928902bb4af045fa7d9f4220a" + integrity sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q== + +lightningcss@^1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.32.0.tgz#b85aae96486dcb1bf49a7c8571221273f4f1e4a9" + integrity sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ== + dependencies: + detect-libc "^2.0.3" + optionalDependencies: + lightningcss-android-arm64 "1.32.0" + lightningcss-darwin-arm64 "1.32.0" + lightningcss-darwin-x64 "1.32.0" + lightningcss-freebsd-x64 "1.32.0" + lightningcss-linux-arm-gnueabihf "1.32.0" + lightningcss-linux-arm64-gnu "1.32.0" + lightningcss-linux-arm64-musl "1.32.0" + lightningcss-linux-x64-gnu "1.32.0" + lightningcss-linux-x64-musl "1.32.0" + lightningcss-win32-arm64-msvc "1.32.0" + lightningcss-win32-x64-msvc "1.32.0" + limiter@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" @@ -7649,6 +7943,13 @@ lz-string@^1.5.0: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== +magic-string@^0.30.21: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + mailcomposer@3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/mailcomposer/-/mailcomposer-3.12.0.tgz#9c5e1188aa8e1c62ec8b86bd43468102b639e8f9" @@ -8345,6 +8646,11 @@ nanoid@^3.3.11: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== +nanoid@^3.3.12: + version "3.3.15" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.15.tgz#36c490fad8c6e86c824c940dfdde999b69ed4316" + integrity sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA== + nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" @@ -8553,6 +8859,11 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" +obug@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/obug/-/obug-2.1.3.tgz#c02c60f95abd603409330e767db7f2823193331e" + integrity sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg== + on-finished@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -8874,6 +9185,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + pause@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" @@ -8904,6 +9220,11 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.3: + version "4.0.5" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.5.tgz#51ea57a17d86f605f81039595fbc40ed06a55fab" + integrity sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A== + picomatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589" @@ -9043,6 +9364,15 @@ postcss@^8.4.43: picocolors "^1.1.1" source-map-js "^1.2.1" +postcss@^8.5.16: + version "8.5.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.16.tgz#1230ce0b5df354c24c0ea45f99ce5f6a88279d28" + integrity sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg== + dependencies: + nanoid "^3.3.12" + picocolors "^1.1.1" + source-map-js "^1.2.1" + prebuild-install@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" @@ -9818,6 +10148,30 @@ rimraf@6.1.3: glob "^13.0.3" package-json-from-dist "^1.0.1" +rolldown@~1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.1.4.tgz#7e15fb26997353808330109ef6bffda274aaa027" + integrity sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA== + dependencies: + "@oxc-project/types" "=0.138.0" + "@rolldown/pluginutils" "^1.0.0" + optionalDependencies: + "@rolldown/binding-android-arm64" "1.1.4" + "@rolldown/binding-darwin-arm64" "1.1.4" + "@rolldown/binding-darwin-x64" "1.1.4" + "@rolldown/binding-freebsd-x64" "1.1.4" + "@rolldown/binding-linux-arm-gnueabihf" "1.1.4" + "@rolldown/binding-linux-arm64-gnu" "1.1.4" + "@rolldown/binding-linux-arm64-musl" "1.1.4" + "@rolldown/binding-linux-ppc64-gnu" "1.1.4" + "@rolldown/binding-linux-s390x-gnu" "1.1.4" + "@rolldown/binding-linux-x64-gnu" "1.1.4" + "@rolldown/binding-linux-x64-musl" "1.1.4" + "@rolldown/binding-openharmony-arm64" "1.1.4" + "@rolldown/binding-wasm32-wasi" "1.1.4" + "@rolldown/binding-win32-arm64-msvc" "1.1.4" + "@rolldown/binding-win32-x64-msvc" "1.1.4" + rollup@^4.20.0: version "4.60.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.60.1.tgz#b4aa2bcb3a5e1437b5fad40d43fe42d4bde7a42d" @@ -10146,6 +10500,11 @@ sift@16.0.1: resolved "https://registry.yarnpkg.com/sift/-/sift-16.0.1.tgz#e9c2ccc72191585008cf3e36fc447b2d2633a053" integrity sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ== +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -10335,6 +10694,11 @@ stack-trace@0.0.x: resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + stackframe@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" @@ -10355,6 +10719,11 @@ statuses@~2.0.2: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== +std-env@^4.0.0-rc.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-4.1.0.tgz#45899abc590d86d682e87f0acd1033a75084cd3f" + integrity sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ== + stream-chain@2.2.5, stream-chain@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/stream-chain/-/stream-chain-2.2.5.tgz#b30967e8f14ee033c5b9a19bbe8a2cba90ba0d09" @@ -10692,6 +11061,16 @@ tiny-warning@^1.0.2, tiny-warning@^1.0.3: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + +tinyexec@^1.0.2: + version "1.2.4" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.2.4.tgz#ae45bb2edebda94c70f4ea897e0f1243e470db71" + integrity sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg== + tinyglobby@^0.2.15: version "0.2.16" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6" @@ -10700,6 +11079,19 @@ tinyglobby@^0.2.15: fdir "^6.5.0" picomatch "^4.0.4" +tinyglobby@^0.2.17: + version "0.2.17" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" + integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.4" + +tinyrainbow@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-3.1.0.tgz#1d8a623893f95cf0a2ddb9e5d11150e191409421" + integrity sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw== + title-case@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/title-case/-/title-case-4.3.2.tgz#498d22ca7083dcfb9add4f4b8d98c000d05fceb3" @@ -11139,6 +11531,45 @@ vite@5.4.21: optionalDependencies: fsevents "~2.3.3" +"vite@^6.0.0 || ^7.0.0 || ^8.0.0": + version "8.1.3" + resolved "https://registry.yarnpkg.com/vite/-/vite-8.1.3.tgz#ce05b445b501014a10e5a8073811759c223e8ac2" + integrity sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA== + dependencies: + lightningcss "^1.32.0" + picomatch "^4.0.4" + postcss "^8.5.16" + rolldown "~1.1.3" + tinyglobby "^0.2.17" + optionalDependencies: + fsevents "~2.3.3" + +vitest@^4.1.9: + version "4.1.9" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.1.9.tgz#98f22fbd70e2a18c4a92bb20624bc92e5dfac5f3" + integrity sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ== + dependencies: + "@vitest/expect" "4.1.9" + "@vitest/mocker" "4.1.9" + "@vitest/pretty-format" "4.1.9" + "@vitest/runner" "4.1.9" + "@vitest/snapshot" "4.1.9" + "@vitest/spy" "4.1.9" + "@vitest/utils" "4.1.9" + es-module-lexer "^2.0.0" + expect-type "^1.3.0" + magic-string "^0.30.21" + obug "^2.1.1" + pathe "^2.0.3" + picomatch "^4.0.3" + std-env "^4.0.0-rc.1" + tinybench "^2.9.0" + tinyexec "^1.0.2" + tinyglobby "^0.2.15" + tinyrainbow "^3.1.0" + vite "^6.0.0 || ^7.0.0 || ^8.0.0" + why-is-node-running "^2.3.0" + vscode-oniguruma@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b" @@ -11279,6 +11710,14 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + widest-line@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" From f592d4ded7035fdd879bb245b062b85a36cf9a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 09:36:17 +0400 Subject: [PATCH 02/28] =?UTF-8?q?fix:=20stopper=20l'ex=C3=A9cution=20apr?= =?UTF-8?q?=C3=A8s=20un=20refus=20d'autorisation=20dans=20parole.create?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/__tests__/parole.test.js | 91 +++++++++++++++++++ src/api/parole/controllers/parole.js | 10 +- 2 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 src/api/parole/controllers/__tests__/parole.test.js diff --git a/src/api/parole/controllers/__tests__/parole.test.js b/src/api/parole/controllers/__tests__/parole.test.js new file mode 100644 index 0000000..0820cf4 --- /dev/null +++ b/src/api/parole/controllers/__tests__/parole.test.js @@ -0,0 +1,91 @@ +import {describe, it, expect, vi} from 'vitest' + +const {default: createController} = await import('../parole.js') + +function buildStrapi({jwtUserId, dbUser, artiste}) { + const paroleDocuments = { + findMany: vi.fn(async () => []), + create: vi.fn(async ({data}) => ({id: 42, ...data})) + } + const userDocuments = { + findOne: vi.fn(async () => dbUser), + update: vi.fn(async () => {}) + } + const artisteDocuments = { + findOne: vi.fn(async () => artiste) + } + + const strapi = { + contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'})), + service: vi.fn(() => ({ + validateParoles: vi.fn(), + translateLyrics: vi.fn() + })), + plugins: { + 'users-permissions': { + services: { + jwt: { + getToken: vi.fn(async () => ({id: jwtUserId})) + } + } + } + }, + documents: vi.fn(uid => { + if (uid === 'plugin::users-permissions.user') return userDocuments + if (uid === 'api::artiste.artiste') return artisteDocuments + if (uid === 'api::parole.parole') return paroleDocuments + throw new Error(`unexpected uid: ${uid}`) + }) + } + + return {strapi, paroleDocuments, userDocuments, artisteDocuments} +} + +function buildCtx(data) { + return { + request: { + body: {data}, + header: {authorization: 'Bearer faketoken'} + }, + unauthorized: vi.fn(), + badRequest: vi.fn(), + notFound: vi.fn() + } +} + +const dbUser = {id: 1, documentId: 'user-doc-1', username: 'foo', email: 'foo@bar.com'} +const artiste = {documentId: 'artiste-doc-1'} + +function buildData(overrides = {}) { + return { + titre: 'Test', + transcription: 'Paroles...', + user: {...dbUser}, + artistes: [{documentId: 'artiste-doc-1'}], + ...overrides + } +} + +describe('parole.create', () => { + it('refuse et ne crée rien quand le user du JWT ne correspond pas au user du payload', async () => { + const {strapi, paroleDocuments} = buildStrapi({jwtUserId: 999, dbUser, artiste}) + const controller = createController({strapi}) + const ctx = buildCtx(buildData()) + + await controller.create(ctx) + + expect(ctx.unauthorized).toHaveBeenCalled() + expect(paroleDocuments.create).not.toHaveBeenCalled() + }) + + it('crée la parole quand le user du JWT correspond au user du payload', async () => { + const {strapi, paroleDocuments} = buildStrapi({jwtUserId: 1, dbUser, artiste}) + const controller = createController({strapi}) + const ctx = buildCtx(buildData()) + + await controller.create(ctx) + + expect(ctx.unauthorized).not.toHaveBeenCalled() + expect(paroleDocuments.create).toHaveBeenCalled() + }) +}) diff --git a/src/api/parole/controllers/parole.js b/src/api/parole/controllers/parole.js index 8bdfb08..b275864 100644 --- a/src/api/parole/controllers/parole.js +++ b/src/api/parole/controllers/parole.js @@ -78,10 +78,10 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ ].services.jwt.getToken(ctx) if (id !== data.user.id) { - ctx.unauthorized('Opération non autorisée') + return ctx.unauthorized('Opération non autorisée') } } catch (err) { - ctx.unauthorized(ctx, err, 'Opération non autorisée') + return ctx.unauthorized('Opération non autorisée') } } @@ -90,11 +90,11 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ }) if (!user) { - ctx.notFound('Utilisateur introuvable.') + return ctx.notFound('Utilisateur introuvable.') } if (user.id !== data.user.id || user.username !== data.user.username || user.email !== data.user.email) { - ctx.badRequest('Informations non valides.') + return ctx.badRequest('Informations non valides.') } const artiste = await strapi.documents('api::artiste.artiste').findOne({ @@ -102,7 +102,7 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ }) if (!artiste) { - ctx.notFound('Artiste introuvable.') + return ctx.notFound('Artiste introuvable.') } const currentUserParole = await strapi.documents('api::parole.parole').findMany({ From 8b17882d6b1ea4924f214fa1601d6329e44d8f34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 09:37:37 +0400 Subject: [PATCH 03/28] fix: corriger le ReferenceError dans artiste.create --- .../controllers/__tests__/artiste.test.js | 84 +++++++++++++++++++ src/api/artiste/controllers/artiste.js | 8 +- 2 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 src/api/artiste/controllers/__tests__/artiste.test.js diff --git a/src/api/artiste/controllers/__tests__/artiste.test.js b/src/api/artiste/controllers/__tests__/artiste.test.js new file mode 100644 index 0000000..c053605 --- /dev/null +++ b/src/api/artiste/controllers/__tests__/artiste.test.js @@ -0,0 +1,84 @@ +import {describe, it, expect, vi} from 'vitest' + +const {default: createController} = await import('../artiste.js') + +function buildStrapi({jwtUserId, dbUser, existingArtiste = null}) { + const dbQuery = { + findOne: vi.fn(async () => existingArtiste) + } + const artisteDocuments = { + create: vi.fn(async ({data}) => ({id: 42, ...data})) + } + const userDocuments = { + findOne: vi.fn(async () => dbUser) + } + + const strapi = { + contentType: vi.fn(() => ({uid: 'api::artiste.artiste', kind: 'collectionType'})), + plugins: { + 'users-permissions': { + services: { + jwt: { + getToken: vi.fn(async () => ({id: jwtUserId})) + } + } + } + }, + db: { + query: vi.fn(() => dbQuery) + }, + documents: vi.fn(uid => { + if (uid === 'plugin::users-permissions.user') return userDocuments + if (uid === 'api::artiste.artiste') return artisteDocuments + throw new Error(`unexpected uid: ${uid}`) + }) + } + + return {strapi, artisteDocuments} +} + +function buildCtx(data) { + return { + request: { + body: {data}, + header: {authorization: 'Bearer faketoken'} + }, + unauthorized: vi.fn(), + badRequest: vi.fn(), + notFound: vi.fn() + } +} + +const dbUser = {id: 1, documentId: 'user-doc-1', username: 'foo', email: 'foo@bar.com'} + +function buildData(overrides = {}) { + return { + alias: 'Test Artist', + user: {...dbUser}, + ...overrides + } +} + +describe('artiste.create', () => { + it('refuse et ne crée rien quand le user du JWT ne correspond pas au user du payload, sans planter', async () => { + const {strapi, artisteDocuments} = buildStrapi({jwtUserId: 999, dbUser}) + const controller = createController({strapi}) + const ctx = buildCtx(buildData()) + + await controller.create(ctx) + + expect(ctx.unauthorized).toHaveBeenCalled() + expect(artisteDocuments.create).not.toHaveBeenCalled() + }) + + it('crée l\'artiste quand le user du JWT correspond au user du payload', async () => { + const {strapi, artisteDocuments} = buildStrapi({jwtUserId: 1, dbUser}) + const controller = createController({strapi}) + const ctx = buildCtx(buildData()) + + await controller.create(ctx) + + expect(ctx.unauthorized).not.toHaveBeenCalled() + expect(artisteDocuments.create).toHaveBeenCalled() + }) +}) diff --git a/src/api/artiste/controllers/artiste.js b/src/api/artiste/controllers/artiste.js index b3bee3b..a5e053b 100644 --- a/src/api/artiste/controllers/artiste.js +++ b/src/api/artiste/controllers/artiste.js @@ -19,10 +19,10 @@ module.exports = createCoreController('api::artiste.artiste', ({strapi}) => ({ ].services.jwt.getToken(ctx) if (id !== data.user.id) { - throw new UnauthorizedError('Opération non autorisée') + return ctx.unauthorized('Opération non autorisée') } } catch (err) { - throw new UnauthorizedError(ctx, err, 'Opération non autorisée') + return ctx.unauthorized('Opération non autorisée') } } @@ -31,11 +31,11 @@ module.exports = createCoreController('api::artiste.artiste', ({strapi}) => ({ }) if (!user) { - throw new NotFoundError('Utilisateur introuvable.') + return ctx.notFound('Utilisateur introuvable.') } if (user.id !== data.user.id || user.username !== data.user.username || user.email !== data.user.email) { - ctx.badRequest('Informations non valides.') + return ctx.badRequest('Informations non valides.') } const artiste = await strapi.db.query('api::artiste.artiste').findOne({ From ddb6d3f2f0fbe5b4ff2d69c036afdd2ff452f8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 09:43:28 +0400 Subject: [PATCH 04/28] =?UTF-8?q?fix:=20corriger=20les=20mauvais=20identif?= =?UTF-8?q?iants=20utilis=C3=A9s=20(id=20vs=20documentId)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/__tests__/commentaire.test.js | 93 +++++++++++++++++++ .../commentaire/controllers/commentaire.js | 11 +-- .../parole/__tests__/lifecycles.test.js | 41 ++++++++ .../parole/content-types/parole/lifecycles.js | 4 +- 4 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 src/api/commentaire/controllers/__tests__/commentaire.test.js create mode 100644 src/api/parole/content-types/parole/__tests__/lifecycles.test.js diff --git a/src/api/commentaire/controllers/__tests__/commentaire.test.js b/src/api/commentaire/controllers/__tests__/commentaire.test.js new file mode 100644 index 0000000..53f7b65 --- /dev/null +++ b/src/api/commentaire/controllers/__tests__/commentaire.test.js @@ -0,0 +1,93 @@ +import {describe, it, expect, vi} from 'vitest' + +const {default: createController} = await import('../commentaire.js') + +const dbUser = {id: 1, username: 'foo', email: 'foo@bar.com'} +const dbParole = {id: 7, documentId: 'parole-doc-7'} + +function buildStrapi({jwtUserId, existingParole = dbParole}) { + const commentaireDocuments = { + create: vi.fn(async ({data}) => ({id: 99, ...data})) + } + const paroleDocuments = { + update: vi.fn(async () => {}) + } + const userDbQuery = { + findOne: vi.fn(async ({where}) => (where.id === dbUser.id ? dbUser : null)) + } + const paroleDbQuery = { + findOne: vi.fn(async ({where}) => (where.id === existingParole?.id ? existingParole : null)) + } + + const strapi = { + contentType: vi.fn(() => ({uid: 'api::commentaire.commentaire', kind: 'collectionType'})), + plugins: { + 'users-permissions': { + services: { + jwt: { + getToken: vi.fn(async () => ({id: jwtUserId})) + } + } + } + }, + db: { + query: vi.fn(uid => { + if (uid === 'plugin::users-permissions.user') return userDbQuery + if (uid === 'api::parole.parole') return paroleDbQuery + throw new Error(`unexpected uid: ${uid}`) + }) + }, + documents: vi.fn(uid => { + if (uid === 'api::commentaire.commentaire') return commentaireDocuments + if (uid === 'api::parole.parole') return paroleDocuments + throw new Error(`unexpected uid: ${uid}`) + }) + } + + return {strapi, commentaireDocuments, paroleDocuments, userDbQuery, paroleDbQuery} +} + +function buildCtx(data) { + return { + request: { + body: {data}, + header: {authorization: 'Bearer faketoken'} + } + } +} + +function buildData(overrides = {}) { + return { + contenu: 'Un commentaire', + datePublication: '2026-07-04', + parole: dbParole.id, + user: {...dbUser}, + ...overrides + } +} + +describe('commentaire.create', () => { + it('retrouve la parole par son id (pas par le documentId du user) et l\'associe correctement', async () => { + const {strapi, commentaireDocuments, paroleDocuments, paroleDbQuery} = buildStrapi({jwtUserId: dbUser.id}) + const controller = createController({strapi}) + const ctx = buildCtx(buildData()) + + await controller.create(ctx) + + expect(paroleDbQuery.findOne).toHaveBeenCalledWith({where: {id: dbParole.id}}) + expect(commentaireDocuments.create).toHaveBeenCalled() + expect(paroleDocuments.update).toHaveBeenCalledWith({ + documentId: dbParole.documentId, + data: {commentaires: [99]} + }) + }) + + it('rejette quand la parole ciblée n\'existe pas', async () => { + const {strapi, commentaireDocuments} = buildStrapi({jwtUserId: dbUser.id, existingParole: null}) + const controller = createController({strapi}) + const ctx = buildCtx(buildData()) + + await expect(controller.create(ctx)).rejects.toThrow('Texte introuvable.') + expect(commentaireDocuments.create).not.toHaveBeenCalled() + }) +}) diff --git a/src/api/commentaire/controllers/commentaire.js b/src/api/commentaire/controllers/commentaire.js index 806e563..76d75fa 100644 --- a/src/api/commentaire/controllers/commentaire.js +++ b/src/api/commentaire/controllers/commentaire.js @@ -21,8 +21,8 @@ module.exports = createCoreController('api::commentaire.commentaire', ({strapi}) throw new UnauthorizedError(ctx, err, 'Opération non autorisée') } } - const user = await strapi.documents('plugin::users-permissions.user').findOne({ - documentId: data.user.documentId + const user = await strapi.db.query('plugin::users-permissions.user').findOne({ + where: {id: data.user.id} }) if (!user) { @@ -35,9 +35,8 @@ module.exports = createCoreController('api::commentaire.commentaire', ({strapi}) data.user = user.id - const parole = await strapi.documents('api::parole.parole').findOne({ - documentId: user.documentId, - fields: ['id'] + const parole = await strapi.db.query('api::parole.parole').findOne({ + where: {id: data.parole} }) if (!parole) { @@ -51,7 +50,7 @@ module.exports = createCoreController('api::commentaire.commentaire', ({strapi}) }) await strapi.documents('api::parole.parole').update({ - documentId: user.documentId, + documentId: parole.documentId, data: { commentaires: [newCommentaire.id] diff --git a/src/api/parole/content-types/parole/__tests__/lifecycles.test.js b/src/api/parole/content-types/parole/__tests__/lifecycles.test.js new file mode 100644 index 0000000..013d28d --- /dev/null +++ b/src/api/parole/content-types/parole/__tests__/lifecycles.test.js @@ -0,0 +1,41 @@ +import {describe, it, expect, vi, afterEach} from 'vitest' + +async function loadLifecycles(strapiMock) { + vi.resetModules() + global.strapi = strapiMock + const mod = await import('../lifecycles.js') + return mod +} + +describe('afterCreate — notification au soumetteur', () => { + afterEach(() => { + delete global.strapi + }) + + it('recherche le user par id (pas par un champ "user" inexistant) et envoie le mail', async () => { + const userFindOne = vi.fn(async ({where}) => { + if (where.id === 5) return {id: 5, username: 'foo', email: 'foo@bar.com'} + return null + }) + const emailSend = vi.fn() + + const strapiMock = { + db: { + query: vi.fn(uid => { + if (uid === 'plugin::users-permissions.user') return {findOne: userFindOne} + return {findOne: vi.fn(async () => null)} + }) + }, + plugins: { + email: {services: {email: {send: emailSend}}} + } + } + + const {afterCreate} = await loadLifecycles(strapiMock) + + await afterCreate({params: {data: {titre: 'Titre', user: {id: 5}}}}) + + expect(userFindOne).toHaveBeenCalledWith({where: {id: 5}}) + expect(emailSend).toHaveBeenCalled() + }) +}) diff --git a/src/api/parole/content-types/parole/lifecycles.js b/src/api/parole/content-types/parole/lifecycles.js index f58a974..a33f7ae 100644 --- a/src/api/parole/content-types/parole/lifecycles.js +++ b/src/api/parole/content-types/parole/lifecycles.js @@ -53,8 +53,8 @@ const jwennUserEpiId = async userId => { } const user = await strapi.db.query('plugin::users-permissions.user').findOne({ - where: {user: userId} - }) + where: {id: userId} + }) if (!user) { throw new ApplicationError('Utilisateur introuvable.') From 36917d79b4caa220a44a12410bd5063e574d4bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 09:47:26 +0400 Subject: [PATCH 05/28] =?UTF-8?q?fix:=20ajouter=20un=20timeout=20DeepL=20e?= =?UTF-8?q?t=20isoler=20les=20=C3=A9checs=20par=20langue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../parole/services/__tests__/parole.test.js | 60 +++++++++++++++++++ src/api/parole/services/parole.js | 11 +++- 2 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/api/parole/services/__tests__/parole.test.js diff --git a/src/api/parole/services/__tests__/parole.test.js b/src/api/parole/services/__tests__/parole.test.js new file mode 100644 index 0000000..6127ac2 --- /dev/null +++ b/src/api/parole/services/__tests__/parole.test.js @@ -0,0 +1,60 @@ +import {describe, it, expect, vi, afterEach} from 'vitest' + +const {default: createService} = await import('../parole.js') + +function fakeDeeplResponse(text) { + return { + ok: true, + json: async () => ({translations: [{text}]}) + } +} + +describe('Translator (DeepL)', () => { + const originalFetch = global.fetch + + afterEach(() => { + global.fetch = originalFetch + vi.restoreAllMocks() + }) + + it('attache un timeout à la requête DeepL', async () => { + global.fetch = vi.fn(async () => fakeDeeplResponse('hello')) + + const strapi = {contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'}))} + const service = createService({strapi}) + await service.translate('FR', 'EN', 'bonjour') + + const [, options] = global.fetch.mock.calls[0] + expect(options.signal).toBeInstanceOf(AbortSignal) + }) +}) + +describe('translateLyrics', () => { + const originalFetch = global.fetch + + afterEach(() => { + global.fetch = originalFetch + vi.restoreAllMocks() + }) + + it('continue les autres langues quand une traduction DeepL échoue', async () => { + global.fetch = vi.fn(async (_url, options) => { + const {target_lang: target} = JSON.parse(options.body) + if (target === 'ES') { + return {ok: false, status: 500, text: async () => 'boom'} + } + + return fakeDeeplResponse(`traduit-${target}`) + }) + + const strapi = { + contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'})), + log: {error: vi.fn()} + } + const service = createService({strapi}) + const result = await service.translateLyrics('Bonjour le monde') + + expect(result.anglais).toContain('traduit-EN') + expect(result.espagnol).toBeUndefined() + }) +}) diff --git a/src/api/parole/services/parole.js b/src/api/parole/services/parole.js index bc78202..536b9e4 100644 --- a/src/api/parole/services/parole.js +++ b/src/api/parole/services/parole.js @@ -64,7 +64,8 @@ class Translator { text: Array.isArray(text) ? text : [text], source_lang: origin, target_lang: target, - }) + }), + signal: AbortSignal.timeout(15_000) }) if (!response.ok) { @@ -90,8 +91,12 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({ for (const lang of ALL_LANGS) { if (lang === 'fr') continue const { field, deeplTarget, suffix } = LANG_MAP[lang] - const translated = await this.translate('FR', deeplTarget, parolesFR) - result[field] = translated + suffix + try { + const translated = await this.translate('FR', deeplTarget, parolesFR) + result[field] = translated + suffix + } catch (err) { + strapi.log.error(`DeepL (${deeplTarget}): ${err.message}`) + } } return result From 4c13f47156521cee55801d5115f2746fd1ca8d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 09:51:10 +0400 Subject: [PATCH 06/28] =?UTF-8?q?fix:=20ne=20pas=20bloquer=20la=20publicat?= =?UTF-8?q?ion=20si=20Telegram/Revolt=20=C3=A9choue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../parole/__tests__/lifecycles.test.js | 78 +++++++++++++++++++ .../parole/content-types/parole/lifecycles.js | 12 ++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/api/parole/content-types/parole/__tests__/lifecycles.test.js b/src/api/parole/content-types/parole/__tests__/lifecycles.test.js index 013d28d..9f0ddb9 100644 --- a/src/api/parole/content-types/parole/__tests__/lifecycles.test.js +++ b/src/api/parole/content-types/parole/__tests__/lifecycles.test.js @@ -39,3 +39,81 @@ describe('afterCreate — notification au soumetteur', () => { expect(emailSend).toHaveBeenCalled() }) }) + +describe('beforeUpdate — notifications Telegram/Revolt', () => { + const originalEnv = {...process.env} + const axios = require('axios') + const originalPost = axios.post + + function buildStrapi(previous) { + const dbQuery = { + findOne: vi.fn(async () => previous), + updateMany: vi.fn() + } + + return { + db: {query: vi.fn(() => dbQuery)}, + plugins: {email: {services: {email: {send: vi.fn()}}}}, + log: {error: vi.fn()} + } + } + + function buildEvent(overrides = {}) { + return { + state: {}, + params: { + data: {documentId: 'doc-1', publishedAt: '2026-07-04T00:00:00.000Z', ...overrides} + } + } + } + + afterEach(() => { + process.env = {...originalEnv} + axios.post = originalPost + delete global.strapi + }) + + it("n'interrompt pas la publication quand Telegram échoue, et encode le message", async () => { + process.env.TELEGRAM_API_TOKEN = 'fake-token' + process.env.TELEGRAM_CHAN_ID = 'fake-chan' + delete process.env.REVOLT_TOKEN + + axios.post = vi.fn(async () => { + throw new Error('boom') + }) + + const previous = {publishedAt: null, slug: 'foo&bar', titre: 'Mon titre', user: null, userAdmin: null, artistes: []} + const strapiMock = buildStrapi(previous) + + const {beforeUpdate} = await loadLifecycles(strapiMock) + + await beforeUpdate(buildEvent()) + + expect(axios.post).toHaveBeenCalledTimes(1) + const [calledUrl] = axios.post.mock.calls[0] + expect(calledUrl).toContain('text=') + expect(calledUrl.split('text=')[1]).not.toContain('&bar') + expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('Telegram')) + }) + + it("n'interrompt pas la publication quand Revolt échoue", async () => { + delete process.env.TELEGRAM_API_TOKEN + process.env.REVOLT_TOKEN = 'fake-token' + process.env.REVOLT_TARGET = 'fake-target' + process.env.REVOLT_BOT_ID = 'fake-bot' + + axios.post = vi.fn(async () => { + throw new Error('boom') + }) + + const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: []} + const strapiMock = buildStrapi(previous) + + const {beforeUpdate} = await loadLifecycles(strapiMock) + + await beforeUpdate(buildEvent()) + + expect(axios.post).toHaveBeenCalledTimes(1) + expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('Revolt')) + }) +}) diff --git a/src/api/parole/content-types/parole/lifecycles.js b/src/api/parole/content-types/parole/lifecycles.js index a33f7ae..89e31ff 100644 --- a/src/api/parole/content-types/parole/lifecycles.js +++ b/src/api/parole/content-types/parole/lifecycles.js @@ -216,7 +216,11 @@ module.exports = { } if (TELEGRAM_API_TOKEN) { - await axios.post(`${MESSAGE_URL}&text=${message}`) + try { + await axios.post(`${MESSAGE_URL}&text=${encodeURIComponent(message)}`) + } catch (err) { + strapi.log.error(`Notification Telegram : ${err.message}`) + } } if (REVOLT_TOKEN && REVOLT_TARGET && REVOLT_BOT_ID) { @@ -234,7 +238,11 @@ module.exports = { } } - await axios.post(url, {content: revoltMessage}, config) + try { + await axios.post(url, {content: revoltMessage}, config) + } catch (err) { + strapi.log.error(`Notification Revolt : ${err.message}`) + } } } } From d87ab299c413d5ee5169502ea4f534baa6a3fcdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 09:52:36 +0400 Subject: [PATCH 07/28] =?UTF-8?q?chore:=20d=C3=A9clarer=20axios=20comme=20?= =?UTF-8?q?d=C3=A9pendance=20directe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 1f682ef..c70e736 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@strapi/plugin-users-permissions": "5.44.0", "@strapi/provider-email-nodemailer": "^4.26.1", "@strapi/strapi": "5.44.0", + "axios": "1.15.1", "better-sqlite3": "^12.9.0", "diff": "^5.1.0", "react": "^18.0.0", From 9fa37c516cc8819fdfb42306d22240fac02bf607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 09:54:39 +0400 Subject: [PATCH 08/28] =?UTF-8?q?chore:=20retirer=20la=20config=20rest-cac?= =?UTF-8?q?he=20(plugin=20non=20install=C3=A9,=20incompatible=20v5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/plugins.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/config/plugins.js b/config/plugins.js index f8e3050..7588585 100644 --- a/config/plugins.js +++ b/config/plugins.js @@ -16,23 +16,4 @@ module.exports = ({env}) => ({ defaultReplyTo: env('SMTP_REPLY_TO'), }, }, - 'rest-cache': { - config: { - provider: { - name: 'memory', - options: { - max: 32767, - maxAge: 3600 - } - }, - strategy: { - contentTypes: [ - 'api::artiste.artiste', - 'api::parole.parole' - ], - debug: true, - hitpass: false - } - } - }, }) From 416032942ae3337e5e2224edb63859e279d9e91e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 09:55:45 +0400 Subject: [PATCH 09/28] fix: restreindre bulk-translate aux appels par token API --- .../policies/__tests__/is-api-token.test.js | 19 +++++++++++++++++++ src/api/parole/policies/is-api-token.js | 5 +++++ src/api/parole/routes/export.js | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/api/parole/policies/__tests__/is-api-token.test.js create mode 100644 src/api/parole/policies/is-api-token.js diff --git a/src/api/parole/policies/__tests__/is-api-token.test.js b/src/api/parole/policies/__tests__/is-api-token.test.js new file mode 100644 index 0000000..0008320 --- /dev/null +++ b/src/api/parole/policies/__tests__/is-api-token.test.js @@ -0,0 +1,19 @@ +import {describe, it, expect} from 'vitest' +import isApiToken from '../is-api-token.js' + +describe('is-api-token policy', () => { + it('autorise une requête authentifiée par token API', () => { + const ctx = {state: {auth: {strategy: {name: 'api-token'}}}} + expect(isApiToken(ctx)).toBe(true) + }) + + it('refuse une requête authentifiée autrement (ex: JWT utilisateur)', () => { + const ctx = {state: {auth: {strategy: {name: 'users-permissions'}}}} + expect(isApiToken(ctx)).toBe(false) + }) + + it('refuse une requête non authentifiée', () => { + const ctx = {state: {}} + expect(isApiToken(ctx)).toBe(false) + }) +}) diff --git a/src/api/parole/policies/is-api-token.js b/src/api/parole/policies/is-api-token.js new file mode 100644 index 0000000..ef061d2 --- /dev/null +++ b/src/api/parole/policies/is-api-token.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = policyContext => { + return policyContext.state?.auth?.strategy?.name === 'api-token' +} diff --git a/src/api/parole/routes/export.js b/src/api/parole/routes/export.js index 3067d5a..18b242b 100644 --- a/src/api/parole/routes/export.js +++ b/src/api/parole/routes/export.js @@ -12,7 +12,7 @@ module.exports = { method: 'POST', path: '/paroles/bulk-translate', handler: 'parole.bulkTranslate', - config: { policies: [], middlewares: [] }, + config: { policies: ['api::parole.is-api-token'], middlewares: [] }, }, ], }; From 1562ecd70681885a84976b928b327f1841fc4e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 09:56:26 +0400 Subject: [PATCH 10/28] =?UTF-8?q?ci:=20ex=C3=A9cuter=20les=20tests=20avant?= =?UTF-8?q?=20le=20build=20sur=20chaque=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/check-pr.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitea/workflows/check-pr.yml b/.gitea/workflows/check-pr.yml index e7f1a44..9f3cc49 100644 --- a/.gitea/workflows/check-pr.yml +++ b/.gitea/workflows/check-pr.yml @@ -22,6 +22,9 @@ jobs: - name: Installer les dépendances run: yarn install --frozen-lockfile + - name: Lancer les tests + run: yarn test + - name: Build Strapi run: yarn build From e515944fb9137cb485fd6cc35c3991db8af1e9b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 10:04:00 +0400 Subject: [PATCH 11/28] =?UTF-8?q?refactor:=20extraire=20la=20v=C3=A9rifica?= =?UTF-8?q?tion=20JWT/payload=20en=20policy=20partag=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/__tests__/artiste.test.js | 28 +------- src/api/artiste/controllers/artiste.js | 14 ---- src/api/artiste/routes/artiste.js | 8 ++- .../controllers/__tests__/commentaire.test.js | 15 +--- .../commentaire/controllers/commentaire.js | 15 +--- src/api/commentaire/routes/commentaire.js | 8 ++- .../controllers/__tests__/parole.test.js | 28 +------- src/api/parole/controllers/parole.js | 14 ---- src/api/parole/routes/parole.js | 8 ++- .../__tests__/is-payload-owner.test.js | 68 +++++++++++++++++++ src/policies/is-payload-owner.js | 23 +++++++ 11 files changed, 122 insertions(+), 107 deletions(-) create mode 100644 src/policies/__tests__/is-payload-owner.test.js create mode 100644 src/policies/is-payload-owner.js diff --git a/src/api/artiste/controllers/__tests__/artiste.test.js b/src/api/artiste/controllers/__tests__/artiste.test.js index c053605..7d4ab90 100644 --- a/src/api/artiste/controllers/__tests__/artiste.test.js +++ b/src/api/artiste/controllers/__tests__/artiste.test.js @@ -2,7 +2,7 @@ import {describe, it, expect, vi} from 'vitest' const {default: createController} = await import('../artiste.js') -function buildStrapi({jwtUserId, dbUser, existingArtiste = null}) { +function buildStrapi({dbUser, existingArtiste = null}) { const dbQuery = { findOne: vi.fn(async () => existingArtiste) } @@ -15,15 +15,6 @@ function buildStrapi({jwtUserId, dbUser, existingArtiste = null}) { const strapi = { contentType: vi.fn(() => ({uid: 'api::artiste.artiste', kind: 'collectionType'})), - plugins: { - 'users-permissions': { - services: { - jwt: { - getToken: vi.fn(async () => ({id: jwtUserId})) - } - } - } - }, db: { query: vi.fn(() => dbQuery) }, @@ -43,7 +34,6 @@ function buildCtx(data) { body: {data}, header: {authorization: 'Bearer faketoken'} }, - unauthorized: vi.fn(), badRequest: vi.fn(), notFound: vi.fn() } @@ -60,25 +50,13 @@ function buildData(overrides = {}) { } describe('artiste.create', () => { - it('refuse et ne crée rien quand le user du JWT ne correspond pas au user du payload, sans planter', async () => { - const {strapi, artisteDocuments} = buildStrapi({jwtUserId: 999, dbUser}) + it('crée l\'artiste quand le user existe et que l\'alias est nouveau', async () => { + const {strapi, artisteDocuments} = buildStrapi({dbUser}) const controller = createController({strapi}) const ctx = buildCtx(buildData()) await controller.create(ctx) - expect(ctx.unauthorized).toHaveBeenCalled() - expect(artisteDocuments.create).not.toHaveBeenCalled() - }) - - it('crée l\'artiste quand le user du JWT correspond au user du payload', async () => { - const {strapi, artisteDocuments} = buildStrapi({jwtUserId: 1, dbUser}) - const controller = createController({strapi}) - const ctx = buildCtx(buildData()) - - await controller.create(ctx) - - expect(ctx.unauthorized).not.toHaveBeenCalled() expect(artisteDocuments.create).toHaveBeenCalled() }) }) diff --git a/src/api/artiste/controllers/artiste.js b/src/api/artiste/controllers/artiste.js index a5e053b..90c0ba7 100644 --- a/src/api/artiste/controllers/artiste.js +++ b/src/api/artiste/controllers/artiste.js @@ -12,20 +12,6 @@ module.exports = createCoreController('api::artiste.artiste', ({strapi}) => ({ const {body} = ctx.request let {data} = body - if (ctx.request && ctx.request.header && ctx.request.header.authorization) { - try { - const {id} = await strapi.plugins[ - 'users-permissions' - ].services.jwt.getToken(ctx) - - if (id !== data.user.id) { - return ctx.unauthorized('Opération non autorisée') - } - } catch (err) { - return ctx.unauthorized('Opération non autorisée') - } - } - const user = await strapi.documents('plugin::users-permissions.user').findOne({ documentId: body.data.user.documentId }) diff --git a/src/api/artiste/routes/artiste.js b/src/api/artiste/routes/artiste.js index 27ff441..7e331e3 100644 --- a/src/api/artiste/routes/artiste.js +++ b/src/api/artiste/routes/artiste.js @@ -2,4 +2,10 @@ const { createCoreRouter } = require('@strapi/strapi').factories; -module.exports = createCoreRouter('api::artiste.artiste') +module.exports = createCoreRouter('api::artiste.artiste', { + config: { + create: { + policies: ['global::is-payload-owner'] + } + } +}) diff --git a/src/api/commentaire/controllers/__tests__/commentaire.test.js b/src/api/commentaire/controllers/__tests__/commentaire.test.js index 53f7b65..960ca94 100644 --- a/src/api/commentaire/controllers/__tests__/commentaire.test.js +++ b/src/api/commentaire/controllers/__tests__/commentaire.test.js @@ -5,7 +5,7 @@ const {default: createController} = await import('../commentaire.js') const dbUser = {id: 1, username: 'foo', email: 'foo@bar.com'} const dbParole = {id: 7, documentId: 'parole-doc-7'} -function buildStrapi({jwtUserId, existingParole = dbParole}) { +function buildStrapi({existingParole = dbParole} = {}) { const commentaireDocuments = { create: vi.fn(async ({data}) => ({id: 99, ...data})) } @@ -21,15 +21,6 @@ function buildStrapi({jwtUserId, existingParole = dbParole}) { const strapi = { contentType: vi.fn(() => ({uid: 'api::commentaire.commentaire', kind: 'collectionType'})), - plugins: { - 'users-permissions': { - services: { - jwt: { - getToken: vi.fn(async () => ({id: jwtUserId})) - } - } - } - }, db: { query: vi.fn(uid => { if (uid === 'plugin::users-permissions.user') return userDbQuery @@ -68,7 +59,7 @@ function buildData(overrides = {}) { describe('commentaire.create', () => { it('retrouve la parole par son id (pas par le documentId du user) et l\'associe correctement', async () => { - const {strapi, commentaireDocuments, paroleDocuments, paroleDbQuery} = buildStrapi({jwtUserId: dbUser.id}) + const {strapi, commentaireDocuments, paroleDocuments, paroleDbQuery} = buildStrapi() const controller = createController({strapi}) const ctx = buildCtx(buildData()) @@ -83,7 +74,7 @@ describe('commentaire.create', () => { }) it('rejette quand la parole ciblée n\'existe pas', async () => { - const {strapi, commentaireDocuments} = buildStrapi({jwtUserId: dbUser.id, existingParole: null}) + const {strapi, commentaireDocuments} = buildStrapi({existingParole: null}) const controller = createController({strapi}) const ctx = buildCtx(buildData()) diff --git a/src/api/commentaire/controllers/commentaire.js b/src/api/commentaire/controllers/commentaire.js index 76d75fa..9e33bd8 100644 --- a/src/api/commentaire/controllers/commentaire.js +++ b/src/api/commentaire/controllers/commentaire.js @@ -1,26 +1,13 @@ 'use strict'; const { createCoreController } = require('@strapi/strapi').factories; -const { ApplicationError, NotFoundError, UnauthorizedError } = require("@strapi/utils").errors +const { ApplicationError, NotFoundError } = require("@strapi/utils").errors module.exports = createCoreController('api::commentaire.commentaire', ({strapi}) => ({ async create(ctx) { const {body} = ctx.request let {data} = body - if (ctx.request && ctx.request.header && ctx.request.header.authorization) { - try { - const {id} = await strapi.plugins[ - 'users-permissions' - ].services.jwt.getToken(ctx) - - if (id !== data.user.id) { - throw new UnauthorizedError('Opération non autorisée') - } - } catch (err) { - throw new UnauthorizedError(ctx, err, 'Opération non autorisée') - } - } const user = await strapi.db.query('plugin::users-permissions.user').findOne({ where: {id: data.user.id} }) diff --git a/src/api/commentaire/routes/commentaire.js b/src/api/commentaire/routes/commentaire.js index 1d1afba..2a5dc0e 100644 --- a/src/api/commentaire/routes/commentaire.js +++ b/src/api/commentaire/routes/commentaire.js @@ -2,4 +2,10 @@ const { createCoreRouter } = require('@strapi/strapi').factories; -module.exports = createCoreRouter('api::commentaire.commentaire') +module.exports = createCoreRouter('api::commentaire.commentaire', { + config: { + create: { + policies: ['global::is-payload-owner'] + } + } +}) diff --git a/src/api/parole/controllers/__tests__/parole.test.js b/src/api/parole/controllers/__tests__/parole.test.js index 0820cf4..0127440 100644 --- a/src/api/parole/controllers/__tests__/parole.test.js +++ b/src/api/parole/controllers/__tests__/parole.test.js @@ -2,7 +2,7 @@ import {describe, it, expect, vi} from 'vitest' const {default: createController} = await import('../parole.js') -function buildStrapi({jwtUserId, dbUser, artiste}) { +function buildStrapi({dbUser, artiste}) { const paroleDocuments = { findMany: vi.fn(async () => []), create: vi.fn(async ({data}) => ({id: 42, ...data})) @@ -21,15 +21,6 @@ function buildStrapi({jwtUserId, dbUser, artiste}) { validateParoles: vi.fn(), translateLyrics: vi.fn() })), - plugins: { - 'users-permissions': { - services: { - jwt: { - getToken: vi.fn(async () => ({id: jwtUserId})) - } - } - } - }, documents: vi.fn(uid => { if (uid === 'plugin::users-permissions.user') return userDocuments if (uid === 'api::artiste.artiste') return artisteDocuments @@ -47,7 +38,6 @@ function buildCtx(data) { body: {data}, header: {authorization: 'Bearer faketoken'} }, - unauthorized: vi.fn(), badRequest: vi.fn(), notFound: vi.fn() } @@ -67,25 +57,13 @@ function buildData(overrides = {}) { } describe('parole.create', () => { - it('refuse et ne crée rien quand le user du JWT ne correspond pas au user du payload', async () => { - const {strapi, paroleDocuments} = buildStrapi({jwtUserId: 999, dbUser, artiste}) + it('crée la parole quand le user et l\'artiste existent', async () => { + const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste}) const controller = createController({strapi}) const ctx = buildCtx(buildData()) await controller.create(ctx) - expect(ctx.unauthorized).toHaveBeenCalled() - expect(paroleDocuments.create).not.toHaveBeenCalled() - }) - - it('crée la parole quand le user du JWT correspond au user du payload', async () => { - const {strapi, paroleDocuments} = buildStrapi({jwtUserId: 1, dbUser, artiste}) - const controller = createController({strapi}) - const ctx = buildCtx(buildData()) - - await controller.create(ctx) - - expect(ctx.unauthorized).not.toHaveBeenCalled() expect(paroleDocuments.create).toHaveBeenCalled() }) }) diff --git a/src/api/parole/controllers/parole.js b/src/api/parole/controllers/parole.js index b275864..37a0ccf 100644 --- a/src/api/parole/controllers/parole.js +++ b/src/api/parole/controllers/parole.js @@ -71,20 +71,6 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ const {data} = body strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription) - if (ctx.request && ctx.request.header && ctx.request.header.authorization) { - try { - const {id} = await strapi.plugins[ - 'users-permissions' - ].services.jwt.getToken(ctx) - - if (id !== data.user.id) { - return ctx.unauthorized('Opération non autorisée') - } - } catch (err) { - return ctx.unauthorized('Opération non autorisée') - } - } - const user = await strapi.documents('plugin::users-permissions.user').findOne({ documentId: body.data.user.documentId }) diff --git a/src/api/parole/routes/parole.js b/src/api/parole/routes/parole.js index 3e80996..58106f4 100644 --- a/src/api/parole/routes/parole.js +++ b/src/api/parole/routes/parole.js @@ -2,4 +2,10 @@ const { createCoreRouter } = require('@strapi/strapi').factories; -module.exports = createCoreRouter('api::parole.parole') +module.exports = createCoreRouter('api::parole.parole', { + config: { + create: { + policies: ['global::is-payload-owner'] + } + } +}) diff --git a/src/policies/__tests__/is-payload-owner.test.js b/src/policies/__tests__/is-payload-owner.test.js new file mode 100644 index 0000000..a13cd84 --- /dev/null +++ b/src/policies/__tests__/is-payload-owner.test.js @@ -0,0 +1,68 @@ +import {describe, it, expect, vi} from 'vitest' + +const {default: isPayloadOwner} = await import('../is-payload-owner.js') + +function buildStrapi(jwtUserId) { + return { + plugins: { + 'users-permissions': { + services: { + jwt: { + getToken: vi.fn(async () => ({id: jwtUserId})) + } + } + } + } + } +} + +function buildPolicyContext({authorization, payloadUserId}) { + return { + request: { + header: authorization ? {authorization} : {}, + body: {data: {user: {id: payloadUserId}}} + } + } +} + +describe('is-payload-owner policy', () => { + it("autorise quand aucun en-tête d'autorisation n'est présent", async () => { + const strapi = buildStrapi(999) + const policyContext = buildPolicyContext({authorization: undefined, payloadUserId: 1}) + + await expect(isPayloadOwner(policyContext, {}, {strapi})).resolves.toBe(true) + }) + + it('autorise quand le user du JWT correspond au user du payload', async () => { + const strapi = buildStrapi(1) + const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1}) + + await expect(isPayloadOwner(policyContext, {}, {strapi})).resolves.toBe(true) + }) + + it('refuse quand le user du JWT ne correspond pas au user du payload', async () => { + const strapi = buildStrapi(999) + const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1}) + + await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée') + }) + + it('refuse quand le token est invalide', async () => { + const strapi = { + plugins: { + 'users-permissions': { + services: { + jwt: { + getToken: vi.fn(async () => { + throw new Error('Invalid token.') + }) + } + } + } + } + } + const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1}) + + await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée') + }) +}) diff --git a/src/policies/is-payload-owner.js b/src/policies/is-payload-owner.js new file mode 100644 index 0000000..76db9e4 --- /dev/null +++ b/src/policies/is-payload-owner.js @@ -0,0 +1,23 @@ +'use strict'; + +const { UnauthorizedError } = require('@strapi/utils').errors + +module.exports = async (policyContext, config, {strapi}) => { + const {request} = policyContext + + if (!request?.header?.authorization) { + return true + } + + try { + const {id} = await strapi.plugins['users-permissions'].services.jwt.getToken(policyContext) + + if (id !== request.body?.data?.user?.id) { + throw new UnauthorizedError('Opération non autorisée') + } + } catch (err) { + throw new UnauthorizedError('Opération non autorisée') + } + + return true +} From bc5d0fb498faab946e35f1f2d7afb3c801ce4a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 11:35:41 +0400 Subject: [PATCH 12/28] =?UTF-8?q?fix:=20refuser=20par=20d=C3=A9faut=20is-p?= =?UTF-8?q?ayload-owner=20sans=20en-t=C3=AAte=20Authorization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/policies/__tests__/is-payload-owner.test.js | 4 ++-- src/policies/is-payload-owner.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/policies/__tests__/is-payload-owner.test.js b/src/policies/__tests__/is-payload-owner.test.js index a13cd84..1fc909e 100644 --- a/src/policies/__tests__/is-payload-owner.test.js +++ b/src/policies/__tests__/is-payload-owner.test.js @@ -26,11 +26,11 @@ function buildPolicyContext({authorization, payloadUserId}) { } describe('is-payload-owner policy', () => { - it("autorise quand aucun en-tête d'autorisation n'est présent", async () => { + it("refuse quand aucun en-tête d'autorisation n'est présent", async () => { const strapi = buildStrapi(999) const policyContext = buildPolicyContext({authorization: undefined, payloadUserId: 1}) - await expect(isPayloadOwner(policyContext, {}, {strapi})).resolves.toBe(true) + await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée') }) it('autorise quand le user du JWT correspond au user du payload', async () => { diff --git a/src/policies/is-payload-owner.js b/src/policies/is-payload-owner.js index 76db9e4..aa5cac5 100644 --- a/src/policies/is-payload-owner.js +++ b/src/policies/is-payload-owner.js @@ -6,7 +6,7 @@ module.exports = async (policyContext, config, {strapi}) => { const {request} = policyContext if (!request?.header?.authorization) { - return true + throw new UnauthorizedError('Opération non autorisée') } try { From 291aa5491231348e6d0d3550b904370b507fbcb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 11:36:20 +0400 Subject: [PATCH 13/28] fix: corriger l'IDOR sur update/delete (parole, artiste, commentaire) --- src/api/artiste/routes/artiste.js | 6 ++ src/api/commentaire/routes/commentaire.js | 6 ++ src/api/parole/routes/parole.js | 6 ++ .../__tests__/is-document-owner.test.js | 73 +++++++++++++++++++ src/policies/is-document-owner.js | 35 +++++++++ 5 files changed, 126 insertions(+) create mode 100644 src/policies/__tests__/is-document-owner.test.js create mode 100644 src/policies/is-document-owner.js diff --git a/src/api/artiste/routes/artiste.js b/src/api/artiste/routes/artiste.js index 7e331e3..55c06de 100644 --- a/src/api/artiste/routes/artiste.js +++ b/src/api/artiste/routes/artiste.js @@ -6,6 +6,12 @@ module.exports = createCoreRouter('api::artiste.artiste', { config: { create: { policies: ['global::is-payload-owner'] + }, + update: { + policies: [{name: 'global::is-document-owner', config: {uid: 'api::artiste.artiste'}}] + }, + delete: { + policies: [{name: 'global::is-document-owner', config: {uid: 'api::artiste.artiste'}}] } } }) diff --git a/src/api/commentaire/routes/commentaire.js b/src/api/commentaire/routes/commentaire.js index 2a5dc0e..1418b6a 100644 --- a/src/api/commentaire/routes/commentaire.js +++ b/src/api/commentaire/routes/commentaire.js @@ -6,6 +6,12 @@ module.exports = createCoreRouter('api::commentaire.commentaire', { config: { create: { policies: ['global::is-payload-owner'] + }, + update: { + policies: [{name: 'global::is-document-owner', config: {uid: 'api::commentaire.commentaire'}}] + }, + delete: { + policies: [{name: 'global::is-document-owner', config: {uid: 'api::commentaire.commentaire'}}] } } }) diff --git a/src/api/parole/routes/parole.js b/src/api/parole/routes/parole.js index 58106f4..69c520c 100644 --- a/src/api/parole/routes/parole.js +++ b/src/api/parole/routes/parole.js @@ -6,6 +6,12 @@ module.exports = createCoreRouter('api::parole.parole', { config: { create: { policies: ['global::is-payload-owner'] + }, + update: { + policies: [{name: 'global::is-document-owner', config: {uid: 'api::parole.parole'}}] + }, + delete: { + policies: [{name: 'global::is-document-owner', config: {uid: 'api::parole.parole'}}] } } }) diff --git a/src/policies/__tests__/is-document-owner.test.js b/src/policies/__tests__/is-document-owner.test.js new file mode 100644 index 0000000..c25f614 --- /dev/null +++ b/src/policies/__tests__/is-document-owner.test.js @@ -0,0 +1,73 @@ +import {describe, it, expect, vi} from 'vitest' + +const {default: isDocumentOwner} = await import('../is-document-owner.js') + +function buildStrapi({jwtUserId, document}) { + const dbQuery = { + findOne: vi.fn(async () => document) + } + + return { + plugins: { + 'users-permissions': { + services: { + jwt: { + getToken: vi.fn(async () => ({id: jwtUserId})) + } + } + } + }, + db: { + query: vi.fn(() => dbQuery) + }, + dbQuery + } +} + +function buildPolicyContext({authorization = 'Bearer faketoken', paramId, bodyDocumentId} = {}) { + return { + params: paramId ? {id: paramId} : {}, + request: { + header: authorization ? {authorization} : {}, + body: {data: {documentId: bodyDocumentId}} + } + } +} + +describe('is-document-owner policy', () => { + it("refuse quand aucun en-tête d'autorisation n'est présent", async () => { + const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}}) + const policyContext = buildPolicyContext({authorization: null, paramId: 'doc-1'}) + + await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Opération non autorisée') + }) + + it('autorise quand le user du JWT est le propriétaire du document (id dans les params)', async () => { + const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}}) + const policyContext = buildPolicyContext({paramId: 'doc-1'}) + + await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).resolves.toBe(true) + expect(strapi.dbQuery.findOne).toHaveBeenCalledWith({where: {documentId: 'doc-1'}, populate: {user: true}}) + }) + + it('autorise quand le documentId vient du corps de la requête (cas du contrôleur parole.update)', async () => { + const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}}) + const policyContext = buildPolicyContext({bodyDocumentId: 'doc-1'}) + + await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).resolves.toBe(true) + }) + + it("refuse quand le user du JWT n'est pas le propriétaire du document", async () => { + const strapi = buildStrapi({jwtUserId: 999, document: {documentId: 'doc-1', user: {id: 1}}}) + const policyContext = buildPolicyContext({paramId: 'doc-1'}) + + await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Opération non autorisée') + }) + + it("refuse quand le document ciblé n'existe pas", async () => { + const strapi = buildStrapi({jwtUserId: 1, document: null}) + const policyContext = buildPolicyContext({paramId: 'doc-inconnu'}) + + await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Ressource introuvable.') + }) +}) diff --git a/src/policies/is-document-owner.js b/src/policies/is-document-owner.js new file mode 100644 index 0000000..f25bed7 --- /dev/null +++ b/src/policies/is-document-owner.js @@ -0,0 +1,35 @@ +'use strict'; + +const { UnauthorizedError, NotFoundError } = require('@strapi/utils').errors + +module.exports = async (policyContext, config, {strapi}) => { + const {request, params} = policyContext + + if (!request?.header?.authorization) { + throw new UnauthorizedError('Opération non autorisée') + } + + let jwtUserId + try { + ({id: jwtUserId} = await strapi.plugins['users-permissions'].services.jwt.getToken(policyContext)) + } catch (err) { + throw new UnauthorizedError('Opération non autorisée') + } + + const documentId = params?.id ?? request.body?.data?.documentId + + const document = await strapi.db.query(config.uid).findOne({ + where: {documentId}, + populate: {user: true} + }) + + if (!document) { + throw new NotFoundError('Ressource introuvable.') + } + + if (document.user?.id !== jwtUserId) { + throw new UnauthorizedError('Opération non autorisée') + } + + return true +} From 85ecdf307228b94fc5bba5c0679b8d05122df8fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 11:36:50 +0400 Subject: [PATCH 14/28] fix: retirer l'override register qui exposait le hash de mot de passe --- .../users-permissions/strapi-server.js | 87 ------------------- 1 file changed, 87 deletions(-) delete mode 100644 src/extensions/users-permissions/strapi-server.js diff --git a/src/extensions/users-permissions/strapi-server.js b/src/extensions/users-permissions/strapi-server.js deleted file mode 100644 index 0234d43..0000000 --- a/src/extensions/users-permissions/strapi-server.js +++ /dev/null @@ -1,87 +0,0 @@ -'use strict'; - -const yup = require('yup'); - -module.exports = (plugin) => { - /** - * Validation custom (équivalent Strapi) - */ - const registerSchema = yup.object({ - username: yup.string().required(), - email: yup.string().email().required(), - password: yup.string().min(6).required(), - }); - - const validateRegisterBody = async (data) => { - try { - return await registerSchema.validate(data, { - abortEarly: false, - stripUnknown: true, - }); - } catch (err) { - const message = err.errors.join(', '); - throw new Error(message); - } - }; - - /** - * Override du controller register - */ - plugin.controllers.auth.register = async (ctx) => { - const { body } = ctx.request; - - // 🔒 1. Validation - let params; - try { - params = await validateRegisterBody(body); - } catch (err) { - return ctx.badRequest(err.message); - } - - const { email, username, password } = params; - - // 🔎 2. Vérifier si user existe déjà - const userService = strapi.service('plugin::users-permissions.user'); - - const existingUser = await userService.fetchAll({ - filters: { - $or: [{ email }, { username }], - }, - }); - - if (existingUser.length > 0) { - return ctx.badRequest('Email or Username already taken'); - } - - // ⚙️ 3. Récupérer rôle "authenticated" - const role = await strapi - .query('plugin::users-permissions.role') - .findOne({ where: { type: 'authenticated' } }); - - if (!role) { - return ctx.badRequest('Default role not found'); - } - - // 👤 4. Création user - const newUser = await userService.add({ - username, - email, - password, - role: role.id, - confirmed: false, - }); - - // 🔑 5. Générer JWT - const jwtService = strapi.service('plugin::users-permissions.jwt'); - - const token = jwtService.issue({ id: newUser.id }); - - // 📤 6. Réponse - ctx.send({ - jwt: token, - user: newUser, - }); - }; - - return plugin; -}; \ No newline at end of file From c66c972a93731d0d89cf49df4a7f698e3b5028a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 11:37:20 +0400 Subject: [PATCH 15/28] =?UTF-8?q?fix:=20r=C3=A9parer=20la=20cascade=20de?= =?UTF-8?q?=20renommage=20des=20slugs=20d'artiste?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../artiste/__tests__/lifecycles.test.js | 66 +++++++++++++++++++ .../content-types/artiste/lifecycles.js | 26 +++++--- 2 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 src/api/artiste/content-types/artiste/__tests__/lifecycles.test.js diff --git a/src/api/artiste/content-types/artiste/__tests__/lifecycles.test.js b/src/api/artiste/content-types/artiste/__tests__/lifecycles.test.js new file mode 100644 index 0000000..5984bed --- /dev/null +++ b/src/api/artiste/content-types/artiste/__tests__/lifecycles.test.js @@ -0,0 +1,66 @@ +import {describe, it, expect, vi, afterEach} from 'vitest' + +async function loadLifecycles(strapiMock) { + vi.resetModules() + global.strapi = strapiMock + const mod = await import('../lifecycles.js') + return mod +} + +describe('artiste afterUpdate — cascade de renommage des slugs', () => { + afterEach(() => { + delete global.strapi + }) + + it("renomme le slug des paroles de l'artiste quand son alias change", async () => { + const artisteFindOne = vi.fn(async () => ({ + id: 5, + paroles: [{id: 10}, {id: 11}] + })) + const paroleFindMany = vi.fn(async () => [ + {id: 10, titre: 'Titre 1', slug: 'ancien-alias-titre-1', artistes: [{alias: 'nouvel-alias'}]}, + {id: 11, titre: 'Titre 2', slug: 'nouvel-alias-titre-2', artistes: [{alias: 'nouvel-alias'}]} + ]) + const paroleUpdate = vi.fn(async () => {}) + + const strapiMock = { + db: { + query: vi.fn(uid => { + if (uid === 'api::artiste.artiste') return {findOne: artisteFindOne} + if (uid === 'api::parole.parole') return {findMany: paroleFindMany, update: paroleUpdate} + throw new Error(`unexpected uid: ${uid}`) + }) + } + } + + const {afterUpdate} = await loadLifecycles(strapiMock) + + await afterUpdate({result: {id: 5}}) + + expect(artisteFindOne).toHaveBeenCalledWith({where: {id: 5}, populate: ['paroles']}) + expect(paroleFindMany).toHaveBeenCalledWith({where: {id: {$in: [10, 11]}}, populate: ['artistes']}) + expect(paroleUpdate).toHaveBeenCalledTimes(1) + expect(paroleUpdate).toHaveBeenCalledWith({where: {id: 10}, data: {slug: 'nouvel-alias-titre-1'}}) + }) + + it("ne fait rien quand l'artiste n'a aucune parole", async () => { + const artisteFindOne = vi.fn(async () => ({id: 5, paroles: []})) + const paroleFindMany = vi.fn() + + const strapiMock = { + db: { + query: vi.fn(uid => { + if (uid === 'api::artiste.artiste') return {findOne: artisteFindOne} + if (uid === 'api::parole.parole') return {findMany: paroleFindMany} + throw new Error(`unexpected uid: ${uid}`) + }) + } + } + + const {afterUpdate} = await loadLifecycles(strapiMock) + + await afterUpdate({result: {id: 5}}) + + expect(paroleFindMany).not.toHaveBeenCalled() + }) +}) diff --git a/src/api/artiste/content-types/artiste/lifecycles.js b/src/api/artiste/content-types/artiste/lifecycles.js index e62905e..5afc926 100644 --- a/src/api/artiste/content-types/artiste/lifecycles.js +++ b/src/api/artiste/content-types/artiste/lifecycles.js @@ -3,13 +3,19 @@ const { ApplicationError } = require("@strapi/utils").errors const slugify = require('slugify') -const jwennTeksEpiId = async data => { - const paroles = await strapi.db.query('api::parole.parole').find({id_in: data}) +const jwennTeksEpiId = async ids => { + const paroles = await strapi.db.query('api::parole.parole').findMany({ + where: {id: {$in: ids}}, + populate: ['artistes'] + }) return paroles } const jwennAwtisEpiId = async id => { - const artiste = await strapi.db.query('api::artiste.artiste').findOne({id}) + const artiste = await strapi.db.query('api::artiste.artiste').findOne({ + where: {id}, + populate: ['paroles'] + }) return artiste } @@ -39,15 +45,15 @@ module.exports = { data.slug = slugify(data.alias, {lower: true, remove: /[*#+~.()'"!:@]/g}) }, afterUpdate: async event => { - let {data} = event.params - const {id} = data - const artiste = await jwennAwtisEpiId(id) + const {result} = event + const artiste = await jwennAwtisEpiId(result.id) if (artiste.paroles && artiste.paroles.length >= 1) { - const paroles = await jwennTeksEpiId(artiste.paroles) - Promise.all(paroles.map(async t => { - const {id, titre, slug, artiste} = t - const alias = artiste.map(a => a.alias).join('-') + const paroleIds = artiste.paroles.map(({id}) => id) + const paroles = await jwennTeksEpiId(paroleIds) + await Promise.all(paroles.map(async t => { + const {id, titre, slug, artistes} = t + const alias = artistes.map(a => a.alias).join('-') const slugUpdated = slugify(`${alias}-${titre}`, {lower: true, remove: /[*#+~.()'"!:@]/g}) if (slug !== slugUpdated) { From c4f45f712a3ed08e9f2967e0d9e280d30546e696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 11:37:40 +0400 Subject: [PATCH 16/28] =?UTF-8?q?fix:=20=C3=A9viter=20un=20crash=20sur=20u?= =?UTF-8?q?pdatedBy=20non=20peupl=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../parole/__tests__/lifecycles.test.js | 34 +++++++++++++++++++ .../parole/content-types/parole/lifecycles.js | 2 +- .../parole/content-types/parole/schema.json | 3 +- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/api/parole/content-types/parole/__tests__/lifecycles.test.js b/src/api/parole/content-types/parole/__tests__/lifecycles.test.js index 9f0ddb9..a93d267 100644 --- a/src/api/parole/content-types/parole/__tests__/lifecycles.test.js +++ b/src/api/parole/content-types/parole/__tests__/lifecycles.test.js @@ -117,3 +117,37 @@ describe('beforeUpdate — notifications Telegram/Revolt', () => { expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('Revolt')) }) }) + +describe('afterUpdate — historique de différence', () => { + afterEach(() => { + delete global.strapi + }) + + it("n'échoue pas quand updatedBy n'est pas peuplé", async () => { + const entityServiceUpdate = vi.fn(async () => {}) + const strapiMock = { + entityService: {update: entityServiceUpdate} + } + + const {afterUpdate} = await loadLifecycles(strapiMock) + + const event = { + result: {id: 1, difference: [], updatedBy: undefined}, + state: {diff: {path: 'transcription', jsonDiff: []}} + } + + await afterUpdate(event) + + expect(entityServiceUpdate).toHaveBeenCalledWith('api::parole.parole', 1, { + data: { + difference: [{ + admin_user: null, + paroles: 'transcription', + jsonDiff: [], + date: expect.any(Date), + sources: 'transcription' + }] + } + }) + }) +}) diff --git a/src/api/parole/content-types/parole/lifecycles.js b/src/api/parole/content-types/parole/lifecycles.js index 89e31ff..fbc8c15 100644 --- a/src/api/parole/content-types/parole/lifecycles.js +++ b/src/api/parole/content-types/parole/lifecycles.js @@ -256,7 +256,7 @@ module.exports = { difference: [ ...result.difference, { - admin_user: result.updatedBy.id, + admin_user: result.updatedBy?.id ?? null, paroles: state.diff.path, jsonDiff: state.diff.jsonDiff, date: new Date(), diff --git a/src/api/parole/content-types/parole/schema.json b/src/api/parole/content-types/parole/schema.json index a1ef029..0f6c54e 100644 --- a/src/api/parole/content-types/parole/schema.json +++ b/src/api/parole/content-types/parole/schema.json @@ -8,7 +8,8 @@ "description": "" }, "options": { - "draftAndPublish": true + "draftAndPublish": true, + "populateCreatorFields": true }, "pluginOptions": {}, "attributes": { From 6f77c170035dfdb351c812361ed98a698a023f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 11:38:02 +0400 Subject: [PATCH 17/28] =?UTF-8?q?fix:=20valider=20data.user/data.artistes?= =?UTF-8?q?=20avant=20d=C3=A9r=C3=A9f=C3=A9rencement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/__tests__/artiste.test.js | 11 ++++++++++ src/api/artiste/controllers/artiste.js | 4 ++++ .../controllers/__tests__/commentaire.test.js | 18 +++++++++++++++ .../commentaire/controllers/commentaire.js | 4 ++++ .../controllers/__tests__/parole.test.js | 22 +++++++++++++++++++ src/api/parole/controllers/parole.js | 5 +++++ 6 files changed, 64 insertions(+) diff --git a/src/api/artiste/controllers/__tests__/artiste.test.js b/src/api/artiste/controllers/__tests__/artiste.test.js index 7d4ab90..707c474 100644 --- a/src/api/artiste/controllers/__tests__/artiste.test.js +++ b/src/api/artiste/controllers/__tests__/artiste.test.js @@ -59,4 +59,15 @@ describe('artiste.create', () => { expect(artisteDocuments.create).toHaveBeenCalled() }) + + it('refuse sans planter quand data.user est absent', async () => { + const {strapi} = buildStrapi({dbUser}) + const controller = createController({strapi}) + const ctx = buildCtx(buildData({user: undefined})) + + await controller.create(ctx) + + expect(ctx.badRequest).toHaveBeenCalled() + expect(strapi.documents).not.toHaveBeenCalled() + }) }) diff --git a/src/api/artiste/controllers/artiste.js b/src/api/artiste/controllers/artiste.js index 90c0ba7..ffa0e5a 100644 --- a/src/api/artiste/controllers/artiste.js +++ b/src/api/artiste/controllers/artiste.js @@ -12,6 +12,10 @@ module.exports = createCoreController('api::artiste.artiste', ({strapi}) => ({ const {body} = ctx.request let {data} = body + if (!data?.user?.documentId) { + return ctx.badRequest('Informations manquantes.') + } + const user = await strapi.documents('plugin::users-permissions.user').findOne({ documentId: body.data.user.documentId }) diff --git a/src/api/commentaire/controllers/__tests__/commentaire.test.js b/src/api/commentaire/controllers/__tests__/commentaire.test.js index 960ca94..35fbd55 100644 --- a/src/api/commentaire/controllers/__tests__/commentaire.test.js +++ b/src/api/commentaire/controllers/__tests__/commentaire.test.js @@ -81,4 +81,22 @@ describe('commentaire.create', () => { await expect(controller.create(ctx)).rejects.toThrow('Texte introuvable.') expect(commentaireDocuments.create).not.toHaveBeenCalled() }) + + it('refuse sans planter quand data.user est absent', async () => { + const {strapi, userDbQuery} = buildStrapi() + const controller = createController({strapi}) + const ctx = buildCtx(buildData({user: undefined})) + + await expect(controller.create(ctx)).rejects.toThrow('Informations manquantes.') + expect(userDbQuery.findOne).not.toHaveBeenCalled() + }) + + it('refuse sans planter quand data.parole est absent', async () => { + const {strapi, paroleDbQuery} = buildStrapi() + const controller = createController({strapi}) + const ctx = buildCtx(buildData({parole: undefined})) + + await expect(controller.create(ctx)).rejects.toThrow('Informations manquantes.') + expect(paroleDbQuery.findOne).not.toHaveBeenCalled() + }) }) diff --git a/src/api/commentaire/controllers/commentaire.js b/src/api/commentaire/controllers/commentaire.js index 9e33bd8..62b8bd6 100644 --- a/src/api/commentaire/controllers/commentaire.js +++ b/src/api/commentaire/controllers/commentaire.js @@ -8,6 +8,10 @@ module.exports = createCoreController('api::commentaire.commentaire', ({strapi}) const {body} = ctx.request let {data} = body + if (!data?.user?.id || !data?.parole) { + throw new ApplicationError('Informations manquantes.') + } + const user = await strapi.db.query('plugin::users-permissions.user').findOne({ where: {id: data.user.id} }) diff --git a/src/api/parole/controllers/__tests__/parole.test.js b/src/api/parole/controllers/__tests__/parole.test.js index 0127440..e333ce1 100644 --- a/src/api/parole/controllers/__tests__/parole.test.js +++ b/src/api/parole/controllers/__tests__/parole.test.js @@ -66,4 +66,26 @@ describe('parole.create', () => { expect(paroleDocuments.create).toHaveBeenCalled() }) + + it('refuse sans planter quand data.user est absent', async () => { + const {strapi, userDocuments} = buildStrapi({dbUser, artiste}) + const controller = createController({strapi}) + const ctx = buildCtx(buildData({user: undefined})) + + await controller.create(ctx) + + expect(ctx.badRequest).toHaveBeenCalled() + expect(userDocuments.findOne).not.toHaveBeenCalled() + }) + + it('refuse sans planter quand data.artistes est vide', async () => { + const {strapi, artisteDocuments} = buildStrapi({dbUser, artiste}) + const controller = createController({strapi}) + const ctx = buildCtx(buildData({artistes: []})) + + await controller.create(ctx) + + expect(ctx.badRequest).toHaveBeenCalled() + expect(artisteDocuments.findOne).not.toHaveBeenCalled() + }) }) diff --git a/src/api/parole/controllers/parole.js b/src/api/parole/controllers/parole.js index 37a0ccf..eb24efb 100644 --- a/src/api/parole/controllers/parole.js +++ b/src/api/parole/controllers/parole.js @@ -69,6 +69,11 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ async create(ctx) { const {body} = ctx.request const {data} = body + + if (!data?.user?.documentId || !data?.artistes?.[0]?.documentId) { + return ctx.badRequest('Informations manquantes.') + } + strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription) const user = await strapi.documents('plugin::users-permissions.user').findOne({ From c0d6834178e192b24c253424e9b9a83b5b7f2baa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 11:38:26 +0400 Subject: [PATCH 18/28] =?UTF-8?q?feat:=20impl=C3=A9menter=20r=C3=A9ellemen?= =?UTF-8?q?t=20la=20sauvegarde=20hebdomadaire=20de=20la=20base?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + config/cron-task.js | 15 ++++- src/utils/__tests__/backup-database.test.js | 62 +++++++++++++++++++++ src/utils/backup-database.js | 31 +++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 src/utils/__tests__/backup-database.test.js create mode 100644 src/utils/backup-database.js diff --git a/.gitignore b/.gitignore index 8e339ee..ebc9f8f 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,7 @@ $RECYCLE.BIN/ *.sql *.sqlite *.sqlite3 +backups/ ############################ diff --git a/config/cron-task.js b/config/cron-task.js index 97d5441..fc910a8 100644 --- a/config/cron-task.js +++ b/config/cron-task.js @@ -1,6 +1,19 @@ +const path = require('path'); +const {backupDatabase} = require('../src/utils/backup-database'); + module.exports = { myJob: { - task: ({ strapi }) => {console.log('TODO > save db')}, + task: ({ strapi }) => { + const dbPath = path.join(__dirname, '..', process.env.DATABASE_FILENAME || '.tmp/data.db'); + const backupsDir = path.join(__dirname, '..', 'backups'); + const backupPath = backupDatabase({dbPath, backupsDir}); + + if (backupPath) { + strapi.log.info(`Sauvegarde de la base effectuée : ${backupPath}`); + } else { + strapi.log.warn(`Sauvegarde de la base ignorée : fichier introuvable (${dbPath})`); + } + }, options: { rule: '0 0 * * SUN', tz: 'Indian/Reunion', diff --git a/src/utils/__tests__/backup-database.test.js b/src/utils/__tests__/backup-database.test.js new file mode 100644 index 0000000..74b43f2 --- /dev/null +++ b/src/utils/__tests__/backup-database.test.js @@ -0,0 +1,62 @@ +import {describe, it, expect, afterEach} from 'vitest' +import fs from 'fs' +import os from 'os' +import path from 'path' +import {backupDatabase} from '../backup-database.js' + +const tmpDirs = [] + +function makeTmpDir() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'backup-database-test-')) + tmpDirs.push(dir) + return dir +} + +describe('backupDatabase', () => { + afterEach(() => { + for (const dir of tmpDirs.splice(0)) { + fs.rmSync(dir, {recursive: true, force: true}) + } + }) + + it("ne fait rien quand le fichier de base n'existe pas", () => { + const root = makeTmpDir() + const result = backupDatabase({ + dbPath: path.join(root, 'inexistant.db'), + backupsDir: path.join(root, 'backups') + }) + + expect(result).toBeNull() + expect(fs.existsSync(path.join(root, 'backups'))).toBe(false) + }) + + it('copie le fichier de base dans le dossier de sauvegardes', () => { + const root = makeTmpDir() + const dbPath = path.join(root, 'data.db') + fs.writeFileSync(dbPath, 'contenu-de-la-base') + const backupsDir = path.join(root, 'backups') + + const result = backupDatabase({dbPath, backupsDir}) + + expect(result).not.toBeNull() + expect(fs.existsSync(result)).toBe(true) + expect(fs.readFileSync(result, 'utf8')).toBe('contenu-de-la-base') + }) + + it('ne conserve que les 8 sauvegardes les plus récentes', () => { + const root = makeTmpDir() + const dbPath = path.join(root, 'data.db') + fs.writeFileSync(dbPath, 'contenu') + const backupsDir = path.join(root, 'backups') + fs.mkdirSync(backupsDir, {recursive: true}) + + for (let i = 0; i < 10; i++) { + fs.writeFileSync(path.join(backupsDir, `data-2020-01-0${i}.db`), 'ancien') + } + + backupDatabase({dbPath, backupsDir}) + + const remaining = fs.readdirSync(backupsDir).filter(name => name.startsWith('data-')) + expect(remaining).toHaveLength(8) + }) +}) diff --git a/src/utils/backup-database.js b/src/utils/backup-database.js new file mode 100644 index 0000000..28fe84a --- /dev/null +++ b/src/utils/backup-database.js @@ -0,0 +1,31 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const RETENTION = 8; + +function backupDatabase({dbPath, backupsDir}) { + if (!fs.existsSync(dbPath)) { + return null; + } + + fs.mkdirSync(backupsDir, {recursive: true}); + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const backupPath = path.join(backupsDir, `data-${timestamp}.db`); + fs.copyFileSync(dbPath, backupPath); + + const backups = fs.readdirSync(backupsDir) + .filter(name => name.startsWith('data-') && name.endsWith('.db')) + .sort(); + + const toDelete = backups.slice(0, Math.max(backups.length - RETENTION, 0)); + for (const name of toDelete) { + fs.unlinkSync(path.join(backupsDir, name)); + } + + return backupPath; +} + +module.exports = {backupDatabase}; From e4187a697a2e04e312d2a262d26ea7f3f2112f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 11:38:56 +0400 Subject: [PATCH 19/28] =?UTF-8?q?fix:=20envoyer=20le=20commentaire=20en=20?= =?UTF-8?q?texte=20brut=20(=C3=A9viter=20l'injection=20HTML)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commentaire/__tests__/lifecycles.test.js | 37 +++++++++++++++++++ .../content-types/commentaire/lifecycles.js | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/api/commentaire/content-types/commentaire/__tests__/lifecycles.test.js diff --git a/src/api/commentaire/content-types/commentaire/__tests__/lifecycles.test.js b/src/api/commentaire/content-types/commentaire/__tests__/lifecycles.test.js new file mode 100644 index 0000000..db02fd5 --- /dev/null +++ b/src/api/commentaire/content-types/commentaire/__tests__/lifecycles.test.js @@ -0,0 +1,37 @@ +import {describe, it, expect, vi, afterEach} from 'vitest' + +async function loadLifecycles(strapiMock) { + vi.resetModules() + global.strapi = strapiMock + const mod = await import('../lifecycles.js') + return mod +} + +describe('commentaire afterCreate — notification email', () => { + afterEach(() => { + delete global.strapi + }) + + it("envoie le contenu en texte brut, jamais comme HTML non échappé", async () => { + const emailSend = vi.fn() + const strapiMock = { + db: { + query: vi.fn(uid => { + if (uid === 'plugin::users-permissions.user') return {findOne: vi.fn(async () => ({id: 1, username: 'foo'}))} + if (uid === 'api::parole.parole') return {findOne: vi.fn(async () => ({id: 7, titre: 'Mon titre'}))} + throw new Error(`unexpected uid: ${uid}`) + }) + }, + plugins: {email: {services: {email: {send: emailSend}}}} + } + + const {afterCreate} = await loadLifecycles(strapiMock) + + await afterCreate({params: {data: {user: 1, parole: 7, contenu: ''}}}) + + expect(emailSend).toHaveBeenCalledTimes(1) + const [payload] = emailSend.mock.calls[0] + expect(payload.text).toBe('') + expect(payload.html).toBeUndefined() + }) +}) diff --git a/src/api/commentaire/content-types/commentaire/lifecycles.js b/src/api/commentaire/content-types/commentaire/lifecycles.js index 61f9ca4..eb2a599 100644 --- a/src/api/commentaire/content-types/commentaire/lifecycles.js +++ b/src/api/commentaire/content-types/commentaire/lifecycles.js @@ -59,7 +59,7 @@ module.exports = { from: process.env.SMTP_FROM, to: process.env.SMTP_SEND_TO, subject: `Commentaire de ${user.username} sur "${parole.titre}"`, - html: data.contenu + text: data.contenu }) } } From b36f53f022dde60417ec7d5a8aa26dcadd6f6a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 11:39:36 +0400 Subject: [PATCH 20/28] chore: supprimer les dossiers legacy extensions/ et components/ --- components/store/store.json | 29 - components/trad/traductions.json | 26 - components/url/liens.json | 28 - extensions/.gitkeep | 0 extensions/documentation/config/settings.json | 43 - .../1.0.0/full_documentation.json | 2340 ----------------- extensions/documentation/public/index.html | 57 - extensions/documentation/public/login.html | 136 - extensions/users-permissions/config/jwt.js | 3 - .../models/User.settings.json | 78 - 10 files changed, 2740 deletions(-) delete mode 100644 components/store/store.json delete mode 100644 components/trad/traductions.json delete mode 100644 components/url/liens.json delete mode 100644 extensions/.gitkeep delete mode 100644 extensions/documentation/config/settings.json delete mode 100644 extensions/documentation/documentation/1.0.0/full_documentation.json delete mode 100644 extensions/documentation/public/index.html delete mode 100644 extensions/documentation/public/login.html delete mode 100644 extensions/users-permissions/config/jwt.js delete mode 100644 extensions/users-permissions/models/User.settings.json diff --git a/components/store/store.json b/components/store/store.json deleted file mode 100644 index 44b5961..0000000 --- a/components/store/store.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "collectionName": "components_store_stores", - "info": { - "name": "Album", - "icon": "music", - "description": "" - }, - "options": {}, - "attributes": { - "url": { - "type": "string", - "required": true - }, - "boutik": { - "type": "enumeration", - "enum": [ - "Tidal", - "Spotify", - "Deezer", - "Qobuz", - "Youtubemusic", - "Applemusic", - "Amazon", - "Soundcloud" - ], - "required": true - } - } -} diff --git a/components/trad/traductions.json b/components/trad/traductions.json deleted file mode 100644 index 608d0bf..0000000 --- a/components/trad/traductions.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "collectionName": "components_trad_traductions", - "info": { - "name": "traductions", - "icon": "spell-check", - "description": "" - }, - "options": {}, - "attributes": { - "francais": { - "type": "richtext" - }, - "english": { - "type": "richtext" - }, - "espagnol": { - "type": "richtext" - }, - "deutsch": { - "type": "richtext" - }, - "italiano": { - "type": "richtext" - } - } -} diff --git a/components/url/liens.json b/components/url/liens.json deleted file mode 100644 index f613dfa..0000000 --- a/components/url/liens.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "collectionName": "components_url_liens", - "info": { - "name": "liens", - "icon": "link", - "description": "" - }, - "options": {}, - "attributes": { - "url": { - "type": "string", - "required": true - }, - "sit": { - "type": "enumeration", - "enum": [ - "Youtube", - "Peertube", - "Dailymotion", - "Vimeo", - "File", - "Lbry", - "Rumble" - ], - "required": true - } - } -} diff --git a/extensions/.gitkeep b/extensions/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/extensions/documentation/config/settings.json b/extensions/documentation/config/settings.json deleted file mode 100644 index 0429639..0000000 --- a/extensions/documentation/config/settings.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "version": "1.0.0", - "title": "#OKi API Dokiman", - "description": "", - "termsOfService": null, - "contact": { - "name": "#OKi", - "email": "kontak@o-k-i.net", - "url": "https://o-k-i.net" - }, - "license": null - }, - "x-strapi-config": { - "path": "/dokiman", - "showGeneratedFiles": true, - "pluginsForWhichToGenerateDoc": [] - }, - "servers": [ - { - "url": "https://api.o-k-i.net", - "description": "Production server" - }, - { - "url": "http://localhost:1337", - "description": "Development server" - }, - { - "url": "http://localhost:1337", - "description": "Staging server" - } - ], - "externalDocs": null, - "security": [ - { - "bearerAuth": [] - } - ], - "paths": {}, - "tags": [], - "components": {} -} diff --git a/extensions/documentation/documentation/1.0.0/full_documentation.json b/extensions/documentation/documentation/1.0.0/full_documentation.json deleted file mode 100644 index 6b86b78..0000000 --- a/extensions/documentation/documentation/1.0.0/full_documentation.json +++ /dev/null @@ -1,2340 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "version": "1.0.0", - "title": "#OKi API Dokiman", - "description": "", - "termsOfService": null, - "contact": { - "name": "#OKi", - "email": "kontak@o-k-i.net", - "url": "https://o-k-i.net" - }, - "license": null, - "x-generation-date": "05/10/2022 9:18:59 PM" - }, - "x-strapi-config": { - "path": "/dokiman", - "showGeneratedFiles": true, - "generateDefaultResponse": true, - "pluginsForWhichToGenerateDoc": [] - }, - "servers": [ - { - "url": "https://api.o-k-i.net", - "description": "Production server" - }, - { - "url": "http://localhost:1337", - "description": "Development server" - }, - { - "url": "http://localhost:1337", - "description": "Staging server" - } - ], - "externalDocs": null, - "security": [ - { - "bearerAuth": [] - } - ], - "paths": { - "/awtis": { - "get": { - "deprecated": false, - "description": "Find all the awtis's records", - "responses": { - "200": { - "description": "Retrieve awtis document(s)", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Awtis" - } - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Awtis" - ], - "parameters": [] - }, - "post": { - "deprecated": true, - "description": "Create a new awtis record", - "responses": { - "200": { - "description": "Retrieve awtis document(s)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Awtis" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Awtis" - ], - "requestBody": { - "description": "", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewAwtis" - } - } - } - } - } - }, - "/awtis/count": { - "get": { - "deprecated": false, - "description": "Retrieve the number of awtis documents", - "responses": { - "200": { - "description": "Retrieve awtis document(s)", - "content": { - "application/json": { - "schema": { - "properties": { - "count": { - "type": "integer" - } - } - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Awtis" - ], - "parameters": [] - } - }, - "/awtis/{id}": { - "get": { - "deprecated": false, - "description": "Find one awtis record", - "responses": { - "200": { - "description": "Retrieve awtis document(s)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Awtis" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Awtis" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "description": "", - "deprecated": false, - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "deprecated": true, - "description": "Update a single awtis record", - "responses": { - "200": { - "description": "Retrieve awtis document(s)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Awtis" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Awtis" - ], - "requestBody": { - "description": "", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewAwtis" - } - } - } - }, - "parameters": [ - { - "name": "id", - "in": "path", - "description": "", - "deprecated": false, - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "delete": { - "deprecated": true, - "description": "Delete a single awtis record", - "responses": { - "200": { - "description": "deletes a single awtis based on the ID supplied", - "content": { - "application/json": { - "schema": { - "type": "integer", - "format": "int64" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Awtis" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "description": "", - "deprecated": false, - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/komante": { - "get": { - "deprecated": false, - "description": "Find all the komante's records", - "responses": { - "200": { - "description": "Retrieve komante document(s)", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Komante" - } - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Komante" - ], - "parameters": [ - { - "name": "_limit", - "in": "query", - "required": false, - "description": "Maximum number of results possible", - "schema": { - "type": "integer" - }, - "deprecated": false - }, - { - "name": "_sort", - "in": "query", - "required": false, - "description": "Sort according to a specific field.", - "schema": { - "type": "string" - }, - "deprecated": false - }, - { - "name": "_start", - "in": "query", - "required": false, - "description": "Skip a specific number of entries (especially useful for pagination)", - "schema": { - "type": "integer" - }, - "deprecated": false - }, - { - "name": "=", - "in": "query", - "required": false, - "description": "Get entries that matches exactly your input", - "schema": { - "type": "string" - }, - "deprecated": false - }, - { - "name": "_ne", - "in": "query", - "required": false, - "description": "Get records that are not equals to something", - "schema": { - "type": "string" - }, - "deprecated": false - }, - { - "name": "_lt", - "in": "query", - "required": false, - "description": "Get record that are lower than a value", - "schema": { - "type": "string" - }, - "deprecated": false - }, - { - "name": "_lte", - "in": "query", - "required": false, - "description": "Get records that are lower than or equal to a value", - "schema": { - "type": "string" - }, - "deprecated": false - }, - { - "name": "_gt", - "in": "query", - "required": false, - "description": "Get records that are greater than a value", - "schema": { - "type": "string" - }, - "deprecated": false - }, - { - "name": "_gte", - "in": "query", - "required": false, - "description": "Get records that are greater than or equal a value", - "schema": { - "type": "string" - }, - "deprecated": false - }, - { - "name": "_contains", - "in": "query", - "required": false, - "description": "Get records that contains a value", - "schema": { - "type": "string" - }, - "deprecated": false - }, - { - "name": "_containss", - "in": "query", - "required": false, - "description": "Get records that contains (case sensitive) a value", - "schema": { - "type": "string" - }, - "deprecated": false - }, - { - "name": "_in", - "in": "query", - "required": false, - "description": "Get records that matches any value in the array of values", - "schema": { - "type": "array", - "items": { - "type": "string" - } - }, - "deprecated": false - }, - { - "name": "_nin", - "in": "query", - "required": false, - "description": "Get records that doesn't match any value in the array of values", - "schema": { - "type": "array", - "items": { - "type": "string" - } - }, - "deprecated": false - } - ] - }, - "post": { - "deprecated": false, - "description": "Create a new komante record", - "responses": { - "200": { - "description": "Retrieve komante document(s)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Komante" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Komante" - ], - "requestBody": { - "description": "", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewKomante" - } - } - } - } - } - }, - "/komante/count": { - "get": { - "deprecated": false, - "description": "Retrieve the number of komante documents", - "responses": { - "200": { - "description": "Retrieve komante document(s)", - "content": { - "application/json": { - "schema": { - "properties": { - "count": { - "type": "integer" - } - } - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Komante" - ], - "parameters": [] - } - }, - "/komante/{id}": { - "get": { - "deprecated": false, - "description": "Find one komante record", - "responses": { - "200": { - "description": "Retrieve komante document(s)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Komante" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Komante" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "description": "", - "deprecated": false, - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "deprecated": false, - "description": "Update a single komante record", - "responses": { - "200": { - "description": "Retrieve komante document(s)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Komante" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Komante" - ], - "requestBody": { - "description": "", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewKomante" - } - } - } - }, - "parameters": [ - { - "name": "id", - "in": "path", - "description": "", - "deprecated": false, - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "delete": { - "deprecated": false, - "description": "Delete a single komante record", - "responses": { - "200": { - "description": "deletes a single komante based on the ID supplied", - "content": { - "application/json": { - "schema": { - "type": "integer", - "format": "int64" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Komante" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "description": "", - "deprecated": false, - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/slugs": { - "get": { - "deprecated": false, - "description": "", - "responses": { - "200": { - "description": "response", - "content": { - "application/json": { - "schema": { - "properties": { - "foo": { - "type": "string" - } - } - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Slugs" - ], - "parameters": [] - } - }, - "/teks": { - "get": { - "deprecated": false, - "description": "Find all the teks's records", - "responses": { - "200": { - "description": "Retrieve teks document(s)", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Teks" - } - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Teks" - ], - "parameters": [] - }, - "post": { - "deprecated": false, - "description": "Create a new teks record", - "responses": { - "200": { - "description": "Retrieve teks document(s)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Teks" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Teks" - ], - "requestBody": { - "description": "", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewTeks" - } - } - } - } - } - }, - "/teks/count": { - "get": { - "deprecated": false, - "description": "Retrieve the number of teks documents", - "responses": { - "200": { - "description": "Retrieve teks document(s)", - "content": { - "application/json": { - "schema": { - "properties": { - "count": { - "type": "integer" - } - } - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Teks" - ], - "parameters": [] - } - }, - "/teks/{id}": { - "get": { - "deprecated": false, - "description": "Find one teks record", - "responses": { - "200": { - "description": "Retrieve teks document(s)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Teks" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Teks" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "description": "", - "deprecated": false, - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "deprecated": false, - "description": "Update a single teks record", - "responses": { - "200": { - "description": "Retrieve teks document(s)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Teks" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Teks" - ], - "requestBody": { - "description": "", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewTeks" - } - } - } - }, - "parameters": [ - { - "name": "id", - "in": "path", - "description": "", - "deprecated": false, - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "delete": { - "deprecated": true, - "description": "Delete a single teks record", - "responses": { - "200": { - "description": "deletes a single teks based on the ID supplied", - "content": { - "application/json": { - "schema": { - "type": "integer", - "format": "int64" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "default": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - }, - "summary": "", - "tags": [ - "Teks" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "description": "", - "deprecated": false, - "required": true, - "schema": { - "type": "string" - } - } - ] - } - } - }, - "components": { - "schemas": { - "Awtis": { - "required": [ - "id", - "alias" - ], - "properties": { - "id": { - "type": "string" - }, - "alias": { - "type": "string" - }, - "prenon": { - "type": "string" - }, - "non": { - "type": "string" - }, - "biyografi": { - "type": "string" - }, - "nesans": { - "type": "string", - "format": "date" - }, - "foto": { - "type": "array", - "items": { - "required": [ - "id", - "name", - "hash", - "mime", - "size", - "url", - "provider" - ], - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "alternativeText": { - "type": "string" - }, - "caption": { - "type": "string" - }, - "width": { - "type": "integer" - }, - "height": { - "type": "integer" - }, - "formats": { - "type": "object" - }, - "hash": { - "type": "string" - }, - "ext": { - "type": "string" - }, - "mime": { - "type": "string" - }, - "size": { - "type": "number" - }, - "url": { - "type": "string" - }, - "previewUrl": { - "type": "string" - }, - "provider": { - "type": "string" - }, - "provider_metadata": { - "type": "object" - }, - "related": { - "type": "string" - }, - "created_by": { - "type": "string" - }, - "updated_by": { - "type": "string" - } - } - } - }, - "teks": { - "type": "array", - "items": { - "required": [ - "id", - "tit", - "transkripsyon" - ], - "properties": { - "id": { - "type": "string" - }, - "tit": { - "type": "string" - }, - "transkripsyon": { - "type": "string" - }, - "tradiksyon": { - "type": "component" - }, - "lanne": { - "type": "integer" - }, - "lyen": { - "type": "component" - }, - "awtis": { - "type": "array", - "items": { - "type": "string" - } - }, - "kouteyAchtey": { - "type": "component" - }, - "slug": { - "type": "string" - }, - "kouveti": { - "type": "string" - }, - "okiMizikID": { - "type": "integer" - }, - "user": { - "type": "string" - }, - "eksplisit": { - "type": "boolean" - }, - "komante": { - "type": "array", - "items": { - "type": "string" - } - }, - "forceSlug": { - "type": "boolean" - }, - "tradiksyonOtomatik": { - "type": "boolean" - }, - "userAdmin": { - "type": "string" - }, - "published_at": { - "type": "string" - }, - "created_by": { - "type": "string" - }, - "updated_by": { - "type": "string" - } - } - } - }, - "user": { - "required": [ - "id", - "username", - "email" - ], - "properties": { - "id": { - "type": "string" - }, - "username": { - "type": "string" - }, - "email": { - "type": "string" - }, - "provider": { - "type": "string" - }, - "password": { - "type": "string" - }, - "resetPasswordToken": { - "type": "string" - }, - "confirmationToken": { - "type": "string" - }, - "confirmed": { - "type": "boolean" - }, - "blocked": { - "type": "boolean" - }, - "role": { - "type": "string" - }, - "canAutoTranslate": { - "type": "boolean" - }, - "created_by": { - "type": "string" - }, - "updated_by": { - "type": "string" - } - } - }, - "slug": { - "type": "string" - }, - "published_at": { - "type": "string", - "format": "date-time" - } - } - }, - "NewAwtis": { - "required": [ - "alias" - ], - "properties": { - "alias": { - "type": "string" - }, - "prenon": { - "type": "string" - }, - "non": { - "type": "string" - }, - "biyografi": { - "type": "string" - }, - "nesans": { - "type": "string", - "format": "date" - }, - "teks": { - "type": "array", - "items": { - "type": "string" - } - }, - "user": { - "type": "string" - }, - "slug": { - "type": "string" - }, - "published_at": { - "type": "string", - "format": "date-time" - }, - "created_by": { - "type": "string" - }, - "updated_by": { - "type": "string" - } - } - }, - "Komante": { - "required": [ - "id", - "kontni", - "sentAt" - ], - "properties": { - "id": { - "type": "string" - }, - "kontni": { - "type": "string" - }, - "user": { - "required": [ - "id", - "username", - "email" - ], - "properties": { - "id": { - "type": "string" - }, - "username": { - "type": "string" - }, - "email": { - "type": "string" - }, - "provider": { - "type": "string" - }, - "password": { - "type": "string" - }, - "resetPasswordToken": { - "type": "string" - }, - "confirmationToken": { - "type": "string" - }, - "confirmed": { - "type": "boolean" - }, - "blocked": { - "type": "boolean" - }, - "role": { - "type": "string" - }, - "canAutoTranslate": { - "type": "boolean" - }, - "created_by": { - "type": "string" - }, - "updated_by": { - "type": "string" - } - } - }, - "teks": { - "required": [ - "id", - "tit", - "transkripsyon" - ], - "properties": { - "id": { - "type": "string" - }, - "tit": { - "type": "string" - }, - "transkripsyon": { - "type": "string" - }, - "tradiksyon": { - "type": "component" - }, - "lanne": { - "type": "integer" - }, - "lyen": { - "type": "component" - }, - "awtis": { - "type": "array", - "items": { - "type": "string" - } - }, - "kouteyAchtey": { - "type": "component" - }, - "slug": { - "type": "string" - }, - "kouveti": { - "type": "string" - }, - "okiMizikID": { - "type": "integer" - }, - "user": { - "type": "string" - }, - "eksplisit": { - "type": "boolean" - }, - "komante": { - "type": "array", - "items": { - "type": "string" - } - }, - "forceSlug": { - "type": "boolean" - }, - "tradiksyonOtomatik": { - "type": "boolean" - }, - "userAdmin": { - "type": "string" - }, - "published_at": { - "type": "string" - }, - "created_by": { - "type": "string" - }, - "updated_by": { - "type": "string" - } - } - }, - "sentAt": { - "type": "string", - "format": "date-time" - }, - "published_at": { - "type": "string", - "format": "date-time" - } - } - }, - "NewKomante": { - "required": [ - "kontni", - "sentAt" - ], - "properties": { - "kontni": { - "type": "string" - }, - "user": { - "type": "string" - }, - "teks": { - "type": "string" - }, - "sentAt": { - "type": "string", - "format": "date-time" - }, - "published_at": { - "type": "string", - "format": "date-time" - }, - "created_by": { - "type": "string" - }, - "updated_by": { - "type": "string" - } - } - }, - "Teks": { - "required": [ - "id", - "tit", - "transkripsyon" - ], - "properties": { - "id": { - "type": "string" - }, - "tit": { - "type": "string" - }, - "transkripsyon": { - "type": "string" - }, - "tradiksyon": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "string" - }, - "francais": { - "type": "string" - }, - "english": { - "type": "string" - }, - "espagnol": { - "type": "string" - }, - "deutsch": { - "type": "string" - }, - "italiano": { - "type": "string" - } - } - }, - "lanne": { - "type": "integer" - }, - "lyen": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "url", - "sit" - ], - "properties": { - "id": { - "type": "string" - }, - "url": { - "type": "string" - }, - "sit": { - "type": "string", - "enum": [ - "Youtube", - "Peertube", - "Dailymotion", - "Vimeo", - "File", - "Lbry", - "Rumble" - ] - } - } - } - }, - "awtis": { - "type": "array", - "items": { - "required": [ - "id", - "alias" - ], - "properties": { - "id": { - "type": "string" - }, - "alias": { - "type": "string" - }, - "prenon": { - "type": "string" - }, - "non": { - "type": "string" - }, - "biyografi": { - "type": "string" - }, - "nesans": { - "type": "string" - }, - "foto": { - "type": "array", - "items": { - "type": "string" - } - }, - "teks": { - "type": "array", - "items": { - "type": "string" - } - }, - "user": { - "type": "string" - }, - "slug": { - "type": "string" - }, - "published_at": { - "type": "string" - }, - "created_by": { - "type": "string" - }, - "updated_by": { - "type": "string" - } - } - } - }, - "kouteyAchtey": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "url", - "boutik" - ], - "properties": { - "id": { - "type": "string" - }, - "url": { - "type": "string" - }, - "boutik": { - "type": "string", - "enum": [ - "Tidal", - "Spotify", - "Deezer", - "Qobuz", - "Youtubemusic", - "Applemusic", - "Amazon", - "Soundcloud" - ] - } - } - } - }, - "slug": { - "type": "string" - }, - "kouveti": { - "required": [ - "id", - "name", - "hash", - "mime", - "size", - "url", - "provider" - ], - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "alternativeText": { - "type": "string" - }, - "caption": { - "type": "string" - }, - "width": { - "type": "integer" - }, - "height": { - "type": "integer" - }, - "formats": { - "type": "object" - }, - "hash": { - "type": "string" - }, - "ext": { - "type": "string" - }, - "mime": { - "type": "string" - }, - "size": { - "type": "number" - }, - "url": { - "type": "string" - }, - "previewUrl": { - "type": "string" - }, - "provider": { - "type": "string" - }, - "provider_metadata": { - "type": "object" - }, - "related": { - "type": "string" - }, - "created_by": { - "type": "string" - }, - "updated_by": { - "type": "string" - } - } - }, - "okiMizikID": { - "type": "integer" - }, - "user": { - "required": [ - "id", - "username", - "email" - ], - "properties": { - "id": { - "type": "string" - }, - "username": { - "type": "string" - }, - "email": { - "type": "string" - }, - "provider": { - "type": "string" - }, - "password": { - "type": "string" - }, - "resetPasswordToken": { - "type": "string" - }, - "confirmationToken": { - "type": "string" - }, - "confirmed": { - "type": "boolean" - }, - "blocked": { - "type": "boolean" - }, - "role": { - "type": "string" - }, - "canAutoTranslate": { - "type": "boolean" - }, - "created_by": { - "type": "string" - }, - "updated_by": { - "type": "string" - } - } - }, - "eksplisit": { - "type": "boolean" - }, - "komante": { - "type": "array", - "items": { - "required": [ - "id", - "kontni", - "sentAt" - ], - "properties": { - "id": { - "type": "string" - }, - "kontni": { - "type": "string" - }, - "user": { - "type": "string" - }, - "teks": { - "type": "string" - }, - "sentAt": { - "type": "string" - }, - "published_at": { - "type": "string" - }, - "created_by": { - "type": "string" - }, - "updated_by": { - "type": "string" - } - } - } - }, - "forceSlug": { - "type": "boolean" - }, - "tradiksyonOtomatik": { - "type": "boolean" - }, - "userAdmin": { - "type": "string" - }, - "published_at": { - "type": "string", - "format": "date-time" - } - } - }, - "NewTeks": { - "required": [ - "tit", - "transkripsyon" - ], - "properties": { - "tit": { - "type": "string" - }, - "transkripsyon": { - "type": "string" - }, - "tradiksyon": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "string" - }, - "francais": { - "type": "string" - }, - "english": { - "type": "string" - }, - "espagnol": { - "type": "string" - }, - "deutsch": { - "type": "string" - }, - "italiano": { - "type": "string" - } - } - }, - "lanne": { - "type": "integer" - }, - "lyen": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "url", - "sit" - ], - "properties": { - "id": { - "type": "string" - }, - "url": { - "type": "string" - }, - "sit": { - "type": "string", - "enum": [ - "Youtube", - "Peertube", - "Dailymotion", - "Vimeo", - "File", - "Lbry", - "Rumble" - ] - } - } - } - }, - "awtis": { - "type": "array", - "items": { - "type": "string" - } - }, - "kouteyAchtey": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "url", - "boutik" - ], - "properties": { - "id": { - "type": "string" - }, - "url": { - "type": "string" - }, - "boutik": { - "type": "string", - "enum": [ - "Tidal", - "Spotify", - "Deezer", - "Qobuz", - "Youtubemusic", - "Applemusic", - "Amazon", - "Soundcloud" - ] - } - } - } - }, - "slug": { - "type": "string" - }, - "okiMizikID": { - "type": "integer" - }, - "user": { - "type": "string" - }, - "eksplisit": { - "type": "boolean" - }, - "komante": { - "type": "array", - "items": { - "type": "string" - } - }, - "forceSlug": { - "type": "boolean" - }, - "tradiksyonOtomatik": { - "type": "boolean" - }, - "userAdmin": { - "type": "string" - }, - "published_at": { - "type": "string", - "format": "date-time" - }, - "created_by": { - "type": "string" - }, - "updated_by": { - "type": "string" - } - } - }, - "Error": { - "required": [ - "code", - "message" - ], - "properties": { - "code": { - "type": "integer", - "format": "int32" - }, - "message": { - "type": "string" - } - } - } - }, - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT" - } - } - }, - "tags": [ - { - "name": "Awtis" - }, - { - "name": "Komante" - }, - { - "name": "Slugs" - }, - { - "name": "Teks" - } - ] -} \ No newline at end of file diff --git a/extensions/documentation/public/index.html b/extensions/documentation/public/index.html deleted file mode 100644 index 0876abb..0000000 --- a/extensions/documentation/public/index.html +++ /dev/null @@ -1,57 +0,0 @@ - - - Swagger UI - - - - - - - -
- - - - - - diff --git a/extensions/documentation/public/login.html b/extensions/documentation/public/login.html deleted file mode 100644 index df88290..0000000 --- a/extensions/documentation/public/login.html +++ /dev/null @@ -1,136 +0,0 @@ - - Login - Documentation - - - - - - - - \ No newline at end of file diff --git a/extensions/users-permissions/config/jwt.js b/extensions/users-permissions/config/jwt.js deleted file mode 100644 index ad4f11a..0000000 --- a/extensions/users-permissions/config/jwt.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - jwtSecret: process.env.JWT_SECRET || '3527426b-d513-44a5-b4c8-ee16494bab0d' -}; \ No newline at end of file diff --git a/extensions/users-permissions/models/User.settings.json b/extensions/users-permissions/models/User.settings.json deleted file mode 100644 index 96b6f7c..0000000 --- a/extensions/users-permissions/models/User.settings.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "kind": "collectionType", - "collectionName": "users-permissions_user", - "info": { - "name": "user", - "description": "" - }, - "options": { - "draftAndPublish": false, - "timestamps": true, - "privateAttributes": [ - "createdAt", - "updatedAt", - "created_at", - "updated_at" - ] - }, - "attributes": { - "username": { - "type": "string", - "minLength": 3, - "unique": true, - "configurable": false, - "required": true - }, - "email": { - "type": "email", - "minLength": 6, - "configurable": false, - "required": true, - "private": true - }, - "provider": { - "type": "string", - "configurable": false, - "private": true - }, - "password": { - "type": "password", - "minLength": 6, - "configurable": false, - "private": true - }, - "resetPasswordToken": { - "type": "string", - "configurable": false, - "private": true - }, - "confirmationToken": { - "type": "string", - "configurable": false, - "private": true - }, - "confirmed": { - "type": "boolean", - "default": false, - "configurable": false, - "private": true - }, - "blocked": { - "type": "boolean", - "default": false, - "configurable": false, - "private": true - }, - "role": { - "model": "role", - "via": "users", - "plugin": "users-permissions", - "configurable": false, - "private": true - }, - "canAutoTranslate": { - "type": "boolean", - "private": false - } - } -} From 250ae822eafaea15f308b003fb14e19acc96008e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 11:39:45 +0400 Subject: [PATCH 21/28] =?UTF-8?q?chore:=20retirer=20les=20secrets=20de=20f?= =?UTF-8?q?allback=20hardcod=C3=A9s=20dans=20admin.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/admin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/admin.js b/config/admin.js index 7f2a388..4c5b029 100644 --- a/config/admin.js +++ b/config/admin.js @@ -4,10 +4,10 @@ const forgotPasswordTemplate = require('./email-templates/forgot-password'); module.exports = ({ env }) => ({ url: env('STRAPI_ADMIN_URL', '/admin'), apiToken: { - salt: env('API_TOKEN_SALT', 'd9b0df66ff97a666027e665707b4e3e7'), + salt: env('API_TOKEN_SALT'), }, auth: { - secret: env('ADMIN_JWT_SECRET', '77b2c87dbab4e1697bec244226fbd1b3'), + secret: env('ADMIN_JWT_SECRET'), }, forgotPassword: { from: env('SMTP_FROM'), From 0afa9251d61a0b099e03c6e318fd7d18a825b2c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 11:39:53 +0400 Subject: [PATCH 22/28] =?UTF-8?q?ci:=20ex=C3=A9cuter=20les=20tests=20dans?= =?UTF-8?q?=20deploy-beta=20et=20deploy-prod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/deploy-beta.yml | 3 +++ .gitea/workflows/deploy-prod.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.gitea/workflows/deploy-beta.yml b/.gitea/workflows/deploy-beta.yml index 154b863..e8eebe9 100644 --- a/.gitea/workflows/deploy-beta.yml +++ b/.gitea/workflows/deploy-beta.yml @@ -20,6 +20,9 @@ jobs: - name: Installer les dépendances run: yarn install --frozen-lockfile + - name: Lancer les tests + run: yarn test + - name: Build Strapi run: yarn build diff --git a/.gitea/workflows/deploy-prod.yml b/.gitea/workflows/deploy-prod.yml index bbcf9d1..adfacae 100644 --- a/.gitea/workflows/deploy-prod.yml +++ b/.gitea/workflows/deploy-prod.yml @@ -22,6 +22,9 @@ jobs: - name: Installer les dépendances run: yarn install --frozen-lockfile + - name: Lancer les tests + run: yarn test + - name: Build Strapi run: yarn build From 4ee7ed3e5e0f88befbbd4d469c99b5d4cb8f8026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 14:58:43 +0400 Subject: [PATCH 23/28] fix: corriger la signature de parole.findOne (ctx au lieu de documentId) --- .../controllers/__tests__/parole.test.js | 25 +++++++++++++++++++ src/api/parole/controllers/parole.js | 3 ++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/api/parole/controllers/__tests__/parole.test.js b/src/api/parole/controllers/__tests__/parole.test.js index e333ce1..75b7f09 100644 --- a/src/api/parole/controllers/__tests__/parole.test.js +++ b/src/api/parole/controllers/__tests__/parole.test.js @@ -56,6 +56,31 @@ function buildData(overrides = {}) { } } +describe('parole.findOne', () => { + it('interroge avec le documentId venant de ctx.params.id, pas avec ctx lui-même', async () => { + const paroleDocuments = { + findOne: vi.fn(async ({documentId}) => ({id: 1, documentId, titre: 'Test'})) + } + const strapi = { + contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'})), + documents: vi.fn(uid => { + if (uid === 'api::parole.parole') return paroleDocuments + throw new Error(`unexpected uid: ${uid}`) + }) + } + const controller = createController({strapi}) + const ctx = {params: {id: 'doc-123'}} + + const result = await controller.findOne(ctx) + + expect(paroleDocuments.findOne).toHaveBeenCalledWith({ + documentId: 'doc-123', + populate: ['artistes'] + }) + expect(result).toEqual({id: 1, documentId: 'doc-123', titre: 'Test'}) + }) +}) + describe('parole.create', () => { it('crée la parole quand le user et l\'artiste existent', async () => { const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste}) diff --git a/src/api/parole/controllers/parole.js b/src/api/parole/controllers/parole.js index eb24efb..bc3a1cd 100644 --- a/src/api/parole/controllers/parole.js +++ b/src/api/parole/controllers/parole.js @@ -44,7 +44,8 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ return ctx.send(result) }, - async findOne(documentId) { + async findOne(ctx) { + const {id: documentId} = ctx.params const parole = await strapi.documents('api::parole.parole').findOne({ documentId, populate: ['artistes'] From 3b9b28d3267b2f1bdcfba8c8ef4515ffe80093ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 14:59:58 +0400 Subject: [PATCH 24/28] =?UTF-8?q?fix:=20ne=20plus=20=C3=A9craser=20les=20c?= =?UTF-8?q?ommentaires=20existants=20d'une=20parole?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/commentaire/controllers/__tests__/commentaire.test.js | 2 +- src/api/commentaire/controllers/commentaire.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/commentaire/controllers/__tests__/commentaire.test.js b/src/api/commentaire/controllers/__tests__/commentaire.test.js index 35fbd55..bc11da4 100644 --- a/src/api/commentaire/controllers/__tests__/commentaire.test.js +++ b/src/api/commentaire/controllers/__tests__/commentaire.test.js @@ -69,7 +69,7 @@ describe('commentaire.create', () => { expect(commentaireDocuments.create).toHaveBeenCalled() expect(paroleDocuments.update).toHaveBeenCalledWith({ documentId: dbParole.documentId, - data: {commentaires: [99]} + data: {commentaires: {connect: [99]}} }) }) diff --git a/src/api/commentaire/controllers/commentaire.js b/src/api/commentaire/controllers/commentaire.js index 62b8bd6..8ab2755 100644 --- a/src/api/commentaire/controllers/commentaire.js +++ b/src/api/commentaire/controllers/commentaire.js @@ -44,7 +44,7 @@ module.exports = createCoreController('api::commentaire.commentaire', ({strapi}) documentId: parole.documentId, data: { - commentaires: [newCommentaire.id] + commentaires: {connect: [newCommentaire.id]} } }) From 1d54d287e1fd7b2f7e5c39badb04eaf7a868edb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 15:04:18 +0400 Subject: [PATCH 25/28] fix: retirer createdBy/updatedBy du payload dans beforeUpdate --- .../parole/__tests__/lifecycles.test.js | 25 +++++++++++++++++++ .../parole/content-types/parole/lifecycles.js | 4 +++ types/generated/contentTypes.d.ts | 7 +++--- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/api/parole/content-types/parole/__tests__/lifecycles.test.js b/src/api/parole/content-types/parole/__tests__/lifecycles.test.js index a93d267..5460350 100644 --- a/src/api/parole/content-types/parole/__tests__/lifecycles.test.js +++ b/src/api/parole/content-types/parole/__tests__/lifecycles.test.js @@ -118,6 +118,31 @@ describe('beforeUpdate — notifications Telegram/Revolt', () => { }) }) +describe('beforeUpdate — createdBy/updatedBy', () => { + afterEach(() => { + delete global.strapi + }) + + it('retire createdBy/updatedBy du payload avant la mise à jour', async () => { + const dbQuery = {findOne: vi.fn(async () => ({publishedAt: null, artistes: []}))} + const strapiMock = {db: {query: vi.fn(() => dbQuery)}} + + const {beforeUpdate} = await loadLifecycles(strapiMock) + + const event = { + state: {}, + params: { + data: {documentId: 'doc-1', createdBy: 999, updatedBy: 999} + } + } + + await beforeUpdate(event) + + expect(event.params.data.createdBy).toBeUndefined() + expect(event.params.data.updatedBy).toBeUndefined() + }) +}) + describe('afterUpdate — historique de différence', () => { afterEach(() => { delete global.strapi diff --git a/src/api/parole/content-types/parole/lifecycles.js b/src/api/parole/content-types/parole/lifecycles.js index fbc8c15..e1b7494 100644 --- a/src/api/parole/content-types/parole/lifecycles.js +++ b/src/api/parole/content-types/parole/lifecycles.js @@ -142,6 +142,10 @@ module.exports = { beforeUpdate: async event => { const {state} = event let {data} = event.params + + delete data.createdBy + delete data.updatedBy + const {documentId} = data if (data.isNewRelease === true) { diff --git a/types/generated/contentTypes.d.ts b/types/generated/contentTypes.d.ts index 89f6933..527838e 100644 --- a/types/generated/contentTypes.d.ts +++ b/types/generated/contentTypes.d.ts @@ -521,6 +521,7 @@ export interface ApiParoleParole extends Struct.CollectionTypeSchema { }; options: { draftAndPublish: true; + populateCreatorFields: true; }; attributes: { annee: Schema.Attribute.Integer; @@ -531,8 +532,7 @@ export interface ApiParoleParole extends Struct.CollectionTypeSchema { >; couverture: Schema.Attribute.Media<'images'>; createdAt: Schema.Attribute.DateTime; - createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & - Schema.Attribute.Private; + createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'>; creativeCommons: Schema.Attribute.Enumeration< ['BY', 'BY-SA', 'BY-ND', 'BY-NC', 'BY-NC-SA', 'BY-NC-ND'] >; @@ -576,8 +576,7 @@ export interface ApiParoleParole extends Struct.CollectionTypeSchema { traductions: Schema.Attribute.Component<'trad.traductions', false>; transcription: Schema.Attribute.RichText & Schema.Attribute.Required; updatedAt: Schema.Attribute.DateTime; - updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & - Schema.Attribute.Private; + updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'>; user: Schema.Attribute.Relation< 'oneToOne', 'plugin::users-permissions.user' From 90c048a2827323988fb8daee2b467329efe077d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 15:08:19 +0400 Subject: [PATCH 26/28] =?UTF-8?q?fix:=20whitelister=20les=20champs=20autor?= =?UTF-8?q?is=C3=A9s=20sur=20create/update=20(mass=20assignment)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/__tests__/artiste.test.js | 18 ++++++ src/api/artiste/controllers/artiste.js | 3 +- .../controllers/__tests__/commentaire.test.js | 17 ++++++ .../commentaire/controllers/commentaire.js | 7 ++- .../controllers/__tests__/parole.test.js | 58 ++++++++++++++++++- src/api/parole/controllers/parole.js | 13 ++++- 6 files changed, 108 insertions(+), 8 deletions(-) diff --git a/src/api/artiste/controllers/__tests__/artiste.test.js b/src/api/artiste/controllers/__tests__/artiste.test.js index 707c474..dbb6ff8 100644 --- a/src/api/artiste/controllers/__tests__/artiste.test.js +++ b/src/api/artiste/controllers/__tests__/artiste.test.js @@ -70,4 +70,22 @@ describe('artiste.create', () => { expect(ctx.badRequest).toHaveBeenCalled() expect(strapi.documents).not.toHaveBeenCalled() }) + + it('ignore les champs non autorisés du payload (mass assignment)', async () => { + const {strapi, artisteDocuments} = buildStrapi({dbUser}) + const controller = createController({strapi}) + const ctx = buildCtx(buildData({ + isExclusiveArtist: true, + userAdmin: {id: 999} + })) + + await controller.create(ctx) + + expect(artisteDocuments.create).toHaveBeenCalledWith({ + data: { + alias: 'Test Artist', + user: dbUser.id + } + }) + }) }) diff --git a/src/api/artiste/controllers/artiste.js b/src/api/artiste/controllers/artiste.js index ffa0e5a..bbbaf73 100644 --- a/src/api/artiste/controllers/artiste.js +++ b/src/api/artiste/controllers/artiste.js @@ -37,7 +37,8 @@ module.exports = createCoreController('api::artiste.artiste', ({strapi}) => ({ } else { const newArtiste = await strapi.documents('api::artiste.artiste').create({ data: { - ...data + alias: data.alias, + user: user.id } }) return newArtiste diff --git a/src/api/commentaire/controllers/__tests__/commentaire.test.js b/src/api/commentaire/controllers/__tests__/commentaire.test.js index bc11da4..41a99a3 100644 --- a/src/api/commentaire/controllers/__tests__/commentaire.test.js +++ b/src/api/commentaire/controllers/__tests__/commentaire.test.js @@ -99,4 +99,21 @@ describe('commentaire.create', () => { await expect(controller.create(ctx)).rejects.toThrow('Informations manquantes.') expect(paroleDbQuery.findOne).not.toHaveBeenCalled() }) + + it('ignore les champs non autorisés du payload (mass assignment)', async () => { + const {strapi, commentaireDocuments} = buildStrapi() + const controller = createController({strapi}) + const ctx = buildCtx(buildData({publishedAt: '2020-01-01'})) + + await controller.create(ctx) + + expect(commentaireDocuments.create).toHaveBeenCalledWith({ + data: { + contenu: 'Un commentaire', + datePublication: '2026-07-04', + user: dbUser.id, + parole: dbParole.id + } + }) + }) }) diff --git a/src/api/commentaire/controllers/commentaire.js b/src/api/commentaire/controllers/commentaire.js index 8ab2755..b856285 100644 --- a/src/api/commentaire/controllers/commentaire.js +++ b/src/api/commentaire/controllers/commentaire.js @@ -24,8 +24,6 @@ module.exports = createCoreController('api::commentaire.commentaire', ({strapi}) throw new ApplicationError('Informations non valides.') } - data.user = user.id - const parole = await strapi.db.query('api::parole.parole').findOne({ where: {id: data.parole} }) @@ -36,7 +34,10 @@ module.exports = createCoreController('api::commentaire.commentaire', ({strapi}) const newCommentaire = await strapi.documents('api::commentaire.commentaire').create({ data: { - ...data + contenu: data.contenu, + datePublication: data.datePublication, + user: user.id, + parole: parole.id } }) diff --git a/src/api/parole/controllers/__tests__/parole.test.js b/src/api/parole/controllers/__tests__/parole.test.js index 75b7f09..a789456 100644 --- a/src/api/parole/controllers/__tests__/parole.test.js +++ b/src/api/parole/controllers/__tests__/parole.test.js @@ -5,7 +5,8 @@ const {default: createController} = await import('../parole.js') function buildStrapi({dbUser, artiste}) { const paroleDocuments = { findMany: vi.fn(async () => []), - create: vi.fn(async ({data}) => ({id: 42, ...data})) + create: vi.fn(async ({data}) => ({id: 42, ...data})), + update: vi.fn(async ({data}) => ({id: 42, ...data})) } const userDocuments = { findOne: vi.fn(async () => dbUser), @@ -44,7 +45,7 @@ function buildCtx(data) { } const dbUser = {id: 1, documentId: 'user-doc-1', username: 'foo', email: 'foo@bar.com'} -const artiste = {documentId: 'artiste-doc-1'} +const artiste = {id: 9, documentId: 'artiste-doc-1'} function buildData(overrides = {}) { return { @@ -113,4 +114,57 @@ describe('parole.create', () => { expect(ctx.badRequest).toHaveBeenCalled() expect(artisteDocuments.findOne).not.toHaveBeenCalled() }) + + it('ignore les champs non autorisés du payload (mass assignment)', async () => { + const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste}) + const controller = createController({strapi}) + const ctx = buildCtx(buildData({ + userAdmin: {id: 999}, + isNewRelease: true, + difference: [{fake: true}] + })) + + await controller.create(ctx) + + expect(paroleDocuments.create).toHaveBeenCalledWith({ + data: { + titre: 'Test', + transcription: 'Paroles...', + traductions: undefined, + traductionAuto: undefined, + artistes: [artiste.id], + user: dbUser.id + } + }) + }) +}) + +describe('parole.update', () => { + it('ignore les champs non autorisés du payload (mass assignment)', async () => { + const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste}) + const controller = createController({strapi}) + const ctx = buildCtx({ + documentId: 'doc-1', + titre: 'Nouveau titre', + transcription: 'Nouveau texte', + traductions: {francais: 'salut'}, + traductionAuto: true, + artistes: [9], + userAdmin: {id: 999}, + user: {id: 999} + }) + + await controller.update(ctx) + + expect(paroleDocuments.update).toHaveBeenCalledWith({ + documentId: 'doc-1', + data: { + titre: 'Nouveau titre', + transcription: 'Nouveau texte', + traductions: {francais: 'salut'}, + traductionAuto: true, + artistes: [9] + } + }) + }) }) diff --git a/src/api/parole/controllers/parole.js b/src/api/parole/controllers/parole.js index bc3a1cd..264cd5b 100644 --- a/src/api/parole/controllers/parole.js +++ b/src/api/parole/controllers/parole.js @@ -61,7 +61,11 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ documentId: data.documentId, data: { - ...data + titre: data.titre, + transcription: data.transcription, + traductions: data.traductions, + traductionAuto: data.traductionAuto, + artistes: data.artistes } }) @@ -119,7 +123,12 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ const newParole = await strapi.documents('api::parole.parole').create({ data: { - ...data + titre: data.titre, + transcription: data.transcription, + traductions: data.traductions, + traductionAuto: data.traductionAuto, + artistes: [artiste.id], + user: user.id } }) From 2224c8f3bb81d9e23ba0f6cf8faf5908ec224c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 20:00:51 +0400 Subject: [PATCH 27/28] chore: activer ESLint sur le backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Installe eslint, ajoute le script lint, modernise le parser (retrait de babel-eslint obsolète) et applique l'autofix (points-virgules manquants sur l'ensemble du code, conformément à la règle "semi" déjà présente dans .eslintrc mais jamais appliquée faute d'ESLint installé et d'un script pour l'exécuter). --- .eslintrc | 8 +- config/email-templates/forgot-password.js | 2 +- config/plugins.js | 2 +- config/server.js | 2 +- package.json | 4 +- .../artiste/__tests__/lifecycles.test.js | 68 ++-- .../content-types/artiste/lifecycles.js | 50 +-- .../controllers/__tests__/artiste.test.js | 66 ++-- src/api/artiste/controllers/artiste.js | 28 +- src/api/artiste/routes/artiste.js | 2 +- .../commentaire/__tests__/lifecycles.test.js | 42 +-- .../controllers/__tests__/commentaire.test.js | 102 +++--- .../commentaire/controllers/commentaire.js | 24 +- src/api/commentaire/routes/commentaire.js | 2 +- .../parole/__tests__/lifecycles.test.js | 166 ++++----- .../parole/content-types/parole/lifecycles.js | 192 +++++----- .../controllers/__tests__/parole.test.js | 120 +++---- src/api/parole/controllers/parole.js | 78 ++-- .../policies/__tests__/is-api-token.test.js | 24 +- src/api/parole/policies/is-api-token.js | 4 +- src/api/parole/routes/parole.js | 2 +- .../parole/services/__tests__/parole.test.js | 60 ++-- src/api/stats/routes/stats.js | 2 +- .../__tests__/is-document-owner.test.js | 60 ++-- .../__tests__/is-payload-owner.test.js | 46 +-- src/policies/is-document-owner.js | 24 +- src/policies/is-payload-owner.js | 16 +- src/utils/__tests__/backup-database.test.js | 74 ++-- yarn.lock | 338 +++++++++++++++++- 29 files changed, 962 insertions(+), 646 deletions(-) diff --git a/.eslintrc b/.eslintrc index b2ca93b..88cf713 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,17 +1,13 @@ { - "parser": "babel-eslint", "extends": "eslint:recommended", "env": { "commonjs": true, - "es6": true, + "es2022": true, "node": true, "browser": false }, "parserOptions": { - "ecmaFeatures": { - "experimentalObjectRestSpread": true, - "jsx": false - }, + "ecmaVersion": 2022, "sourceType": "module" }, "globals": { diff --git a/config/email-templates/forgot-password.js b/config/email-templates/forgot-password.js index aee34d0..0152205 100644 --- a/config/email-templates/forgot-password.js +++ b/config/email-templates/forgot-password.js @@ -1,4 +1,4 @@ -const subject = `Réinitialiser le mot de passe`; +const subject = 'Réinitialiser le mot de passe'; const html = `

Bèl bonjou <%= user.firstname %>

Nous avons appris que tu a perdu ton mot de passe. Nous en sommes désolés !

diff --git a/config/plugins.js b/config/plugins.js index 7588585..d72157f 100644 --- a/config/plugins.js +++ b/config/plugins.js @@ -16,4 +16,4 @@ module.exports = ({env}) => ({ defaultReplyTo: env('SMTP_REPLY_TO'), }, }, -}) +}); diff --git a/config/server.js b/config/server.js index c4fb476..d7e50a9 100644 --- a/config/server.js +++ b/config/server.js @@ -1,4 +1,4 @@ -const cronTasks = require("./cron-task"); +const cronTasks = require('./cron-task'); module.exports = ({ env }) => ({ host: env('HOST', '0.0.0.0'), diff --git a/package.json b/package.json index c70e736..d0b4dc1 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,11 @@ "start": "strapi start", "build": "strapi build", "strapi": "strapi", - "test": "vitest run" + "test": "vitest run", + "lint": "eslint ." }, "devDependencies": { + "eslint": "^8.7.0", "vitest": "^4.1.9" }, "dependencies": { diff --git a/src/api/artiste/content-types/artiste/__tests__/lifecycles.test.js b/src/api/artiste/content-types/artiste/__tests__/lifecycles.test.js index 5984bed..31316a8 100644 --- a/src/api/artiste/content-types/artiste/__tests__/lifecycles.test.js +++ b/src/api/artiste/content-types/artiste/__tests__/lifecycles.test.js @@ -1,66 +1,66 @@ -import {describe, it, expect, vi, afterEach} from 'vitest' +import {describe, it, expect, vi, afterEach} from 'vitest'; async function loadLifecycles(strapiMock) { - vi.resetModules() - global.strapi = strapiMock - const mod = await import('../lifecycles.js') - return mod + vi.resetModules(); + global.strapi = strapiMock; + const mod = await import('../lifecycles.js'); + return mod; } describe('artiste afterUpdate — cascade de renommage des slugs', () => { afterEach(() => { - delete global.strapi - }) + delete global.strapi; + }); - it("renomme le slug des paroles de l'artiste quand son alias change", async () => { + it('renomme le slug des paroles de l\'artiste quand son alias change', async () => { const artisteFindOne = vi.fn(async () => ({ id: 5, paroles: [{id: 10}, {id: 11}] - })) + })); const paroleFindMany = vi.fn(async () => [ {id: 10, titre: 'Titre 1', slug: 'ancien-alias-titre-1', artistes: [{alias: 'nouvel-alias'}]}, {id: 11, titre: 'Titre 2', slug: 'nouvel-alias-titre-2', artistes: [{alias: 'nouvel-alias'}]} - ]) - const paroleUpdate = vi.fn(async () => {}) + ]); + const paroleUpdate = vi.fn(async () => {}); const strapiMock = { db: { query: vi.fn(uid => { - if (uid === 'api::artiste.artiste') return {findOne: artisteFindOne} - if (uid === 'api::parole.parole') return {findMany: paroleFindMany, update: paroleUpdate} - throw new Error(`unexpected uid: ${uid}`) + if (uid === 'api::artiste.artiste') return {findOne: artisteFindOne}; + if (uid === 'api::parole.parole') return {findMany: paroleFindMany, update: paroleUpdate}; + throw new Error(`unexpected uid: ${uid}`); }) } - } + }; - const {afterUpdate} = await loadLifecycles(strapiMock) + const {afterUpdate} = await loadLifecycles(strapiMock); - await afterUpdate({result: {id: 5}}) + await afterUpdate({result: {id: 5}}); - expect(artisteFindOne).toHaveBeenCalledWith({where: {id: 5}, populate: ['paroles']}) - expect(paroleFindMany).toHaveBeenCalledWith({where: {id: {$in: [10, 11]}}, populate: ['artistes']}) - expect(paroleUpdate).toHaveBeenCalledTimes(1) - expect(paroleUpdate).toHaveBeenCalledWith({where: {id: 10}, data: {slug: 'nouvel-alias-titre-1'}}) - }) + expect(artisteFindOne).toHaveBeenCalledWith({where: {id: 5}, populate: ['paroles']}); + expect(paroleFindMany).toHaveBeenCalledWith({where: {id: {$in: [10, 11]}}, populate: ['artistes']}); + expect(paroleUpdate).toHaveBeenCalledTimes(1); + expect(paroleUpdate).toHaveBeenCalledWith({where: {id: 10}, data: {slug: 'nouvel-alias-titre-1'}}); + }); - it("ne fait rien quand l'artiste n'a aucune parole", async () => { - const artisteFindOne = vi.fn(async () => ({id: 5, paroles: []})) - const paroleFindMany = vi.fn() + it('ne fait rien quand l\'artiste n\'a aucune parole', async () => { + const artisteFindOne = vi.fn(async () => ({id: 5, paroles: []})); + const paroleFindMany = vi.fn(); const strapiMock = { db: { query: vi.fn(uid => { - if (uid === 'api::artiste.artiste') return {findOne: artisteFindOne} - if (uid === 'api::parole.parole') return {findMany: paroleFindMany} - throw new Error(`unexpected uid: ${uid}`) + if (uid === 'api::artiste.artiste') return {findOne: artisteFindOne}; + if (uid === 'api::parole.parole') return {findMany: paroleFindMany}; + throw new Error(`unexpected uid: ${uid}`); }) } - } + }; - const {afterUpdate} = await loadLifecycles(strapiMock) + const {afterUpdate} = await loadLifecycles(strapiMock); - await afterUpdate({result: {id: 5}}) + await afterUpdate({result: {id: 5}}); - expect(paroleFindMany).not.toHaveBeenCalled() - }) -}) + expect(paroleFindMany).not.toHaveBeenCalled(); + }); +}); diff --git a/src/api/artiste/content-types/artiste/lifecycles.js b/src/api/artiste/content-types/artiste/lifecycles.js index 5afc926..c4278ca 100644 --- a/src/api/artiste/content-types/artiste/lifecycles.js +++ b/src/api/artiste/content-types/artiste/lifecycles.js @@ -1,60 +1,60 @@ 'use strict'; -const { ApplicationError } = require("@strapi/utils").errors -const slugify = require('slugify') +const { ApplicationError } = require('@strapi/utils').errors; +const slugify = require('slugify'); const jwennTeksEpiId = async ids => { const paroles = await strapi.db.query('api::parole.parole').findMany({ where: {id: {$in: ids}}, populate: ['artistes'] - }) - return paroles -} + }); + return paroles; +}; const jwennAwtisEpiId = async id => { const artiste = await strapi.db.query('api::artiste.artiste').findOne({ where: {id}, populate: ['paroles'] - }) - return artiste -} + }); + return artiste; +}; const validateArtiste = alias => { if (!alias || alias.trim().length === 0) { throw new ApplicationError('Champ obligatoire. Veuillez choisir un alias.'); } -} +}; module.exports = { beforeUpdate: async event => { - let {data} = event.params + let {data} = event.params; if(!data.publishedAt) { - validateArtiste(data.alias) + validateArtiste(data.alias); if (!data.slug || data.slug !== slugify(data.alias, {lower: true, remove: /[*#+~.()'"!:@]/g})) { - data.slug = slugify(data.alias, {lower: true, remove: /[*#+~.()'"!:@]/g}) + data.slug = slugify(data.alias, {lower: true, remove: /[*#+~.()'"!:@]/g}); } } }, beforeCreate: async event => { - let {data} = event.params + let {data} = event.params; - validateArtiste(data.alias) + validateArtiste(data.alias); - data.slug = slugify(data.alias, {lower: true, remove: /[*#+~.()'"!:@]/g}) + data.slug = slugify(data.alias, {lower: true, remove: /[*#+~.()'"!:@]/g}); }, afterUpdate: async event => { - const {result} = event - const artiste = await jwennAwtisEpiId(result.id) + const {result} = event; + const artiste = await jwennAwtisEpiId(result.id); if (artiste.paroles && artiste.paroles.length >= 1) { - const paroleIds = artiste.paroles.map(({id}) => id) - const paroles = await jwennTeksEpiId(paroleIds) + const paroleIds = artiste.paroles.map(({id}) => id); + const paroles = await jwennTeksEpiId(paroleIds); await Promise.all(paroles.map(async t => { - const {id, titre, slug, artistes} = t - const alias = artistes.map(a => a.alias).join('-') - const slugUpdated = slugify(`${alias}-${titre}`, {lower: true, remove: /[*#+~.()'"!:@]/g}) + const {id, titre, slug, artistes} = t; + const alias = artistes.map(a => a.alias).join('-'); + const slugUpdated = slugify(`${alias}-${titre}`, {lower: true, remove: /[*#+~.()'"!:@]/g}); if (slug !== slugUpdated) { await strapi.db.query('api::parole.parole').update({ @@ -62,9 +62,9 @@ module.exports = { data: { slug: slugUpdated } - }) + }); } - })) + })); } } -} +}; diff --git a/src/api/artiste/controllers/__tests__/artiste.test.js b/src/api/artiste/controllers/__tests__/artiste.test.js index dbb6ff8..693d770 100644 --- a/src/api/artiste/controllers/__tests__/artiste.test.js +++ b/src/api/artiste/controllers/__tests__/artiste.test.js @@ -1,17 +1,17 @@ -import {describe, it, expect, vi} from 'vitest' +import {describe, it, expect, vi} from 'vitest'; -const {default: createController} = await import('../artiste.js') +const {default: createController} = await import('../artiste.js'); function buildStrapi({dbUser, existingArtiste = null}) { const dbQuery = { findOne: vi.fn(async () => existingArtiste) - } + }; const artisteDocuments = { create: vi.fn(async ({data}) => ({id: 42, ...data})) - } + }; const userDocuments = { findOne: vi.fn(async () => dbUser) - } + }; const strapi = { contentType: vi.fn(() => ({uid: 'api::artiste.artiste', kind: 'collectionType'})), @@ -19,13 +19,13 @@ function buildStrapi({dbUser, existingArtiste = null}) { query: vi.fn(() => dbQuery) }, documents: vi.fn(uid => { - if (uid === 'plugin::users-permissions.user') return userDocuments - if (uid === 'api::artiste.artiste') return artisteDocuments - throw new Error(`unexpected uid: ${uid}`) + if (uid === 'plugin::users-permissions.user') return userDocuments; + if (uid === 'api::artiste.artiste') return artisteDocuments; + throw new Error(`unexpected uid: ${uid}`); }) - } + }; - return {strapi, artisteDocuments} + return {strapi, artisteDocuments}; } function buildCtx(data) { @@ -36,56 +36,56 @@ function buildCtx(data) { }, badRequest: vi.fn(), notFound: vi.fn() - } + }; } -const dbUser = {id: 1, documentId: 'user-doc-1', username: 'foo', email: 'foo@bar.com'} +const dbUser = {id: 1, documentId: 'user-doc-1', username: 'foo', email: 'foo@bar.com'}; function buildData(overrides = {}) { return { alias: 'Test Artist', user: {...dbUser}, ...overrides - } + }; } describe('artiste.create', () => { it('crée l\'artiste quand le user existe et que l\'alias est nouveau', async () => { - const {strapi, artisteDocuments} = buildStrapi({dbUser}) - const controller = createController({strapi}) - const ctx = buildCtx(buildData()) + const {strapi, artisteDocuments} = buildStrapi({dbUser}); + const controller = createController({strapi}); + const ctx = buildCtx(buildData()); - await controller.create(ctx) + await controller.create(ctx); - expect(artisteDocuments.create).toHaveBeenCalled() - }) + expect(artisteDocuments.create).toHaveBeenCalled(); + }); it('refuse sans planter quand data.user est absent', async () => { - const {strapi} = buildStrapi({dbUser}) - const controller = createController({strapi}) - const ctx = buildCtx(buildData({user: undefined})) + const {strapi} = buildStrapi({dbUser}); + const controller = createController({strapi}); + const ctx = buildCtx(buildData({user: undefined})); - await controller.create(ctx) + await controller.create(ctx); - expect(ctx.badRequest).toHaveBeenCalled() - expect(strapi.documents).not.toHaveBeenCalled() - }) + expect(ctx.badRequest).toHaveBeenCalled(); + expect(strapi.documents).not.toHaveBeenCalled(); + }); it('ignore les champs non autorisés du payload (mass assignment)', async () => { - const {strapi, artisteDocuments} = buildStrapi({dbUser}) - const controller = createController({strapi}) + const {strapi, artisteDocuments} = buildStrapi({dbUser}); + const controller = createController({strapi}); const ctx = buildCtx(buildData({ isExclusiveArtist: true, userAdmin: {id: 999} - })) + })); - await controller.create(ctx) + await controller.create(ctx); expect(artisteDocuments.create).toHaveBeenCalledWith({ data: { alias: 'Test Artist', user: dbUser.id } - }) - }) -}) + }); + }); +}); diff --git a/src/api/artiste/controllers/artiste.js b/src/api/artiste/controllers/artiste.js index bbbaf73..64e7136 100644 --- a/src/api/artiste/controllers/artiste.js +++ b/src/api/artiste/controllers/artiste.js @@ -1,48 +1,48 @@ 'use strict'; const { createCoreController } = require('@strapi/strapi').factories; -const slugify = require('slugify') +const slugify = require('slugify'); const getSlug = text => { - return slugify(text, {lower: true, remove: /[*#+~.()'"!:@]/g}) -} + return slugify(text, {lower: true, remove: /[*#+~.()'"!:@]/g}); +}; module.exports = createCoreController('api::artiste.artiste', ({strapi}) => ({ async create(ctx) { - const {body} = ctx.request - let {data} = body + const {body} = ctx.request; + let {data} = body; if (!data?.user?.documentId) { - return ctx.badRequest('Informations manquantes.') + return ctx.badRequest('Informations manquantes.'); } const user = await strapi.documents('plugin::users-permissions.user').findOne({ documentId: body.data.user.documentId - }) + }); if (!user) { - return ctx.notFound('Utilisateur introuvable.') + return ctx.notFound('Utilisateur introuvable.'); } if (user.id !== data.user.id || user.username !== data.user.username || user.email !== data.user.email) { - return ctx.badRequest('Informations non valides.') + return ctx.badRequest('Informations non valides.'); } const artiste = await strapi.db.query('api::artiste.artiste').findOne({ where: {slug: getSlug(data.alias)} - }) + }); if (artiste) { - return artiste + return artiste; } else { const newArtiste = await strapi.documents('api::artiste.artiste').create({ data: { alias: data.alias, user: user.id } - }) - return newArtiste + }); + return newArtiste; } } -})) +})); diff --git a/src/api/artiste/routes/artiste.js b/src/api/artiste/routes/artiste.js index 55c06de..94015cf 100644 --- a/src/api/artiste/routes/artiste.js +++ b/src/api/artiste/routes/artiste.js @@ -14,4 +14,4 @@ module.exports = createCoreRouter('api::artiste.artiste', { policies: [{name: 'global::is-document-owner', config: {uid: 'api::artiste.artiste'}}] } } -}) +}); diff --git a/src/api/commentaire/content-types/commentaire/__tests__/lifecycles.test.js b/src/api/commentaire/content-types/commentaire/__tests__/lifecycles.test.js index db02fd5..790abb3 100644 --- a/src/api/commentaire/content-types/commentaire/__tests__/lifecycles.test.js +++ b/src/api/commentaire/content-types/commentaire/__tests__/lifecycles.test.js @@ -1,37 +1,37 @@ -import {describe, it, expect, vi, afterEach} from 'vitest' +import {describe, it, expect, vi, afterEach} from 'vitest'; async function loadLifecycles(strapiMock) { - vi.resetModules() - global.strapi = strapiMock - const mod = await import('../lifecycles.js') - return mod + vi.resetModules(); + global.strapi = strapiMock; + const mod = await import('../lifecycles.js'); + return mod; } describe('commentaire afterCreate — notification email', () => { afterEach(() => { - delete global.strapi - }) + delete global.strapi; + }); - it("envoie le contenu en texte brut, jamais comme HTML non échappé", async () => { - const emailSend = vi.fn() + it('envoie le contenu en texte brut, jamais comme HTML non échappé', async () => { + const emailSend = vi.fn(); const strapiMock = { db: { query: vi.fn(uid => { - if (uid === 'plugin::users-permissions.user') return {findOne: vi.fn(async () => ({id: 1, username: 'foo'}))} - if (uid === 'api::parole.parole') return {findOne: vi.fn(async () => ({id: 7, titre: 'Mon titre'}))} - throw new Error(`unexpected uid: ${uid}`) + if (uid === 'plugin::users-permissions.user') return {findOne: vi.fn(async () => ({id: 1, username: 'foo'}))}; + if (uid === 'api::parole.parole') return {findOne: vi.fn(async () => ({id: 7, titre: 'Mon titre'}))}; + throw new Error(`unexpected uid: ${uid}`); }) }, plugins: {email: {services: {email: {send: emailSend}}}} - } + }; - const {afterCreate} = await loadLifecycles(strapiMock) + const {afterCreate} = await loadLifecycles(strapiMock); - await afterCreate({params: {data: {user: 1, parole: 7, contenu: ''}}}) + await afterCreate({params: {data: {user: 1, parole: 7, contenu: ''}}}); - expect(emailSend).toHaveBeenCalledTimes(1) - const [payload] = emailSend.mock.calls[0] - expect(payload.text).toBe('') - expect(payload.html).toBeUndefined() - }) -}) + expect(emailSend).toHaveBeenCalledTimes(1); + const [payload] = emailSend.mock.calls[0]; + expect(payload.text).toBe(''); + expect(payload.html).toBeUndefined(); + }); +}); diff --git a/src/api/commentaire/controllers/__tests__/commentaire.test.js b/src/api/commentaire/controllers/__tests__/commentaire.test.js index 41a99a3..050745f 100644 --- a/src/api/commentaire/controllers/__tests__/commentaire.test.js +++ b/src/api/commentaire/controllers/__tests__/commentaire.test.js @@ -1,41 +1,41 @@ -import {describe, it, expect, vi} from 'vitest' +import {describe, it, expect, vi} from 'vitest'; -const {default: createController} = await import('../commentaire.js') +const {default: createController} = await import('../commentaire.js'); -const dbUser = {id: 1, username: 'foo', email: 'foo@bar.com'} -const dbParole = {id: 7, documentId: 'parole-doc-7'} +const dbUser = {id: 1, username: 'foo', email: 'foo@bar.com'}; +const dbParole = {id: 7, documentId: 'parole-doc-7'}; function buildStrapi({existingParole = dbParole} = {}) { const commentaireDocuments = { create: vi.fn(async ({data}) => ({id: 99, ...data})) - } + }; const paroleDocuments = { update: vi.fn(async () => {}) - } + }; const userDbQuery = { findOne: vi.fn(async ({where}) => (where.id === dbUser.id ? dbUser : null)) - } + }; const paroleDbQuery = { findOne: vi.fn(async ({where}) => (where.id === existingParole?.id ? existingParole : null)) - } + }; const strapi = { contentType: vi.fn(() => ({uid: 'api::commentaire.commentaire', kind: 'collectionType'})), db: { query: vi.fn(uid => { - if (uid === 'plugin::users-permissions.user') return userDbQuery - if (uid === 'api::parole.parole') return paroleDbQuery - throw new Error(`unexpected uid: ${uid}`) + if (uid === 'plugin::users-permissions.user') return userDbQuery; + if (uid === 'api::parole.parole') return paroleDbQuery; + throw new Error(`unexpected uid: ${uid}`); }) }, documents: vi.fn(uid => { - if (uid === 'api::commentaire.commentaire') return commentaireDocuments - if (uid === 'api::parole.parole') return paroleDocuments - throw new Error(`unexpected uid: ${uid}`) + if (uid === 'api::commentaire.commentaire') return commentaireDocuments; + if (uid === 'api::parole.parole') return paroleDocuments; + throw new Error(`unexpected uid: ${uid}`); }) - } + }; - return {strapi, commentaireDocuments, paroleDocuments, userDbQuery, paroleDbQuery} + return {strapi, commentaireDocuments, paroleDocuments, userDbQuery, paroleDbQuery}; } function buildCtx(data) { @@ -44,7 +44,7 @@ function buildCtx(data) { body: {data}, header: {authorization: 'Bearer faketoken'} } - } + }; } function buildData(overrides = {}) { @@ -54,58 +54,58 @@ function buildData(overrides = {}) { parole: dbParole.id, user: {...dbUser}, ...overrides - } + }; } describe('commentaire.create', () => { it('retrouve la parole par son id (pas par le documentId du user) et l\'associe correctement', async () => { - const {strapi, commentaireDocuments, paroleDocuments, paroleDbQuery} = buildStrapi() - const controller = createController({strapi}) - const ctx = buildCtx(buildData()) + const {strapi, commentaireDocuments, paroleDocuments, paroleDbQuery} = buildStrapi(); + const controller = createController({strapi}); + const ctx = buildCtx(buildData()); - await controller.create(ctx) + await controller.create(ctx); - expect(paroleDbQuery.findOne).toHaveBeenCalledWith({where: {id: dbParole.id}}) - expect(commentaireDocuments.create).toHaveBeenCalled() + expect(paroleDbQuery.findOne).toHaveBeenCalledWith({where: {id: dbParole.id}}); + expect(commentaireDocuments.create).toHaveBeenCalled(); expect(paroleDocuments.update).toHaveBeenCalledWith({ documentId: dbParole.documentId, data: {commentaires: {connect: [99]}} - }) - }) + }); + }); it('rejette quand la parole ciblée n\'existe pas', async () => { - const {strapi, commentaireDocuments} = buildStrapi({existingParole: null}) - const controller = createController({strapi}) - const ctx = buildCtx(buildData()) + const {strapi, commentaireDocuments} = buildStrapi({existingParole: null}); + const controller = createController({strapi}); + const ctx = buildCtx(buildData()); - await expect(controller.create(ctx)).rejects.toThrow('Texte introuvable.') - expect(commentaireDocuments.create).not.toHaveBeenCalled() - }) + await expect(controller.create(ctx)).rejects.toThrow('Texte introuvable.'); + expect(commentaireDocuments.create).not.toHaveBeenCalled(); + }); it('refuse sans planter quand data.user est absent', async () => { - const {strapi, userDbQuery} = buildStrapi() - const controller = createController({strapi}) - const ctx = buildCtx(buildData({user: undefined})) + const {strapi, userDbQuery} = buildStrapi(); + const controller = createController({strapi}); + const ctx = buildCtx(buildData({user: undefined})); - await expect(controller.create(ctx)).rejects.toThrow('Informations manquantes.') - expect(userDbQuery.findOne).not.toHaveBeenCalled() - }) + await expect(controller.create(ctx)).rejects.toThrow('Informations manquantes.'); + expect(userDbQuery.findOne).not.toHaveBeenCalled(); + }); it('refuse sans planter quand data.parole est absent', async () => { - const {strapi, paroleDbQuery} = buildStrapi() - const controller = createController({strapi}) - const ctx = buildCtx(buildData({parole: undefined})) + const {strapi, paroleDbQuery} = buildStrapi(); + const controller = createController({strapi}); + const ctx = buildCtx(buildData({parole: undefined})); - await expect(controller.create(ctx)).rejects.toThrow('Informations manquantes.') - expect(paroleDbQuery.findOne).not.toHaveBeenCalled() - }) + await expect(controller.create(ctx)).rejects.toThrow('Informations manquantes.'); + expect(paroleDbQuery.findOne).not.toHaveBeenCalled(); + }); it('ignore les champs non autorisés du payload (mass assignment)', async () => { - const {strapi, commentaireDocuments} = buildStrapi() - const controller = createController({strapi}) - const ctx = buildCtx(buildData({publishedAt: '2020-01-01'})) + const {strapi, commentaireDocuments} = buildStrapi(); + const controller = createController({strapi}); + const ctx = buildCtx(buildData({publishedAt: '2020-01-01'})); - await controller.create(ctx) + await controller.create(ctx); expect(commentaireDocuments.create).toHaveBeenCalledWith({ data: { @@ -114,6 +114,6 @@ describe('commentaire.create', () => { user: dbUser.id, parole: dbParole.id } - }) - }) -}) + }); + }); +}); diff --git a/src/api/commentaire/controllers/commentaire.js b/src/api/commentaire/controllers/commentaire.js index b856285..08db007 100644 --- a/src/api/commentaire/controllers/commentaire.js +++ b/src/api/commentaire/controllers/commentaire.js @@ -1,35 +1,35 @@ 'use strict'; const { createCoreController } = require('@strapi/strapi').factories; -const { ApplicationError, NotFoundError } = require("@strapi/utils").errors +const { ApplicationError, NotFoundError } = require('@strapi/utils').errors; module.exports = createCoreController('api::commentaire.commentaire', ({strapi}) => ({ async create(ctx) { - const {body} = ctx.request - let {data} = body + const {body} = ctx.request; + let {data} = body; if (!data?.user?.id || !data?.parole) { - throw new ApplicationError('Informations manquantes.') + throw new ApplicationError('Informations manquantes.'); } const user = await strapi.db.query('plugin::users-permissions.user').findOne({ where: {id: data.user.id} - }) + }); if (!user) { - throw new NotFoundError('Utilisateur introuvable.') + throw new NotFoundError('Utilisateur introuvable.'); } if (user.id !== data.user.id || user.username !== data.user.username || user.email !== data.user.email) { - throw new ApplicationError('Informations non valides.') + throw new ApplicationError('Informations non valides.'); } const parole = await strapi.db.query('api::parole.parole').findOne({ where: {id: data.parole} - }) + }); if (!parole) { - throw new NotFoundError('Texte introuvable.') + throw new NotFoundError('Texte introuvable.'); } const newCommentaire = await strapi.documents('api::commentaire.commentaire').create({ @@ -39,7 +39,7 @@ module.exports = createCoreController('api::commentaire.commentaire', ({strapi}) user: user.id, parole: parole.id } - }) + }); await strapi.documents('api::parole.parole').update({ documentId: parole.documentId, @@ -47,8 +47,8 @@ module.exports = createCoreController('api::commentaire.commentaire', ({strapi}) data: { commentaires: {connect: [newCommentaire.id]} } - }) + }); return newCommentaire; } -})) +})); diff --git a/src/api/commentaire/routes/commentaire.js b/src/api/commentaire/routes/commentaire.js index 1418b6a..9b6f317 100644 --- a/src/api/commentaire/routes/commentaire.js +++ b/src/api/commentaire/routes/commentaire.js @@ -14,4 +14,4 @@ module.exports = createCoreRouter('api::commentaire.commentaire', { policies: [{name: 'global::is-document-owner', config: {uid: 'api::commentaire.commentaire'}}] } } -}) +}); diff --git a/src/api/parole/content-types/parole/__tests__/lifecycles.test.js b/src/api/parole/content-types/parole/__tests__/lifecycles.test.js index 5460350..4d394e3 100644 --- a/src/api/parole/content-types/parole/__tests__/lifecycles.test.js +++ b/src/api/parole/content-types/parole/__tests__/lifecycles.test.js @@ -1,61 +1,61 @@ -import {describe, it, expect, vi, afterEach} from 'vitest' +import {describe, it, expect, vi, afterEach} from 'vitest'; async function loadLifecycles(strapiMock) { - vi.resetModules() - global.strapi = strapiMock - const mod = await import('../lifecycles.js') - return mod + vi.resetModules(); + global.strapi = strapiMock; + const mod = await import('../lifecycles.js'); + return mod; } describe('afterCreate — notification au soumetteur', () => { afterEach(() => { - delete global.strapi - }) + delete global.strapi; + }); it('recherche le user par id (pas par un champ "user" inexistant) et envoie le mail', async () => { const userFindOne = vi.fn(async ({where}) => { - if (where.id === 5) return {id: 5, username: 'foo', email: 'foo@bar.com'} - return null - }) - const emailSend = vi.fn() + if (where.id === 5) return {id: 5, username: 'foo', email: 'foo@bar.com'}; + return null; + }); + const emailSend = vi.fn(); const strapiMock = { db: { query: vi.fn(uid => { - if (uid === 'plugin::users-permissions.user') return {findOne: userFindOne} - return {findOne: vi.fn(async () => null)} + if (uid === 'plugin::users-permissions.user') return {findOne: userFindOne}; + return {findOne: vi.fn(async () => null)}; }) }, plugins: { email: {services: {email: {send: emailSend}}} } - } + }; - const {afterCreate} = await loadLifecycles(strapiMock) + const {afterCreate} = await loadLifecycles(strapiMock); - await afterCreate({params: {data: {titre: 'Titre', user: {id: 5}}}}) + await afterCreate({params: {data: {titre: 'Titre', user: {id: 5}}}}); - expect(userFindOne).toHaveBeenCalledWith({where: {id: 5}}) - expect(emailSend).toHaveBeenCalled() - }) -}) + expect(userFindOne).toHaveBeenCalledWith({where: {id: 5}}); + expect(emailSend).toHaveBeenCalled(); + }); +}); describe('beforeUpdate — notifications Telegram/Revolt', () => { - const originalEnv = {...process.env} - const axios = require('axios') - const originalPost = axios.post + const originalEnv = {...process.env}; + const axios = require('axios'); + const originalPost = axios.post; function buildStrapi(previous) { const dbQuery = { findOne: vi.fn(async () => previous), updateMany: vi.fn() - } + }; return { db: {query: vi.fn(() => dbQuery)}, plugins: {email: {services: {email: {send: vi.fn()}}}}, log: {error: vi.fn()} - } + }; } function buildEvent(overrides = {}) { @@ -64,104 +64,104 @@ describe('beforeUpdate — notifications Telegram/Revolt', () => { params: { data: {documentId: 'doc-1', publishedAt: '2026-07-04T00:00:00.000Z', ...overrides} } - } + }; } afterEach(() => { - process.env = {...originalEnv} - axios.post = originalPost - delete global.strapi - }) + process.env = {...originalEnv}; + axios.post = originalPost; + delete global.strapi; + }); - it("n'interrompt pas la publication quand Telegram échoue, et encode le message", async () => { - process.env.TELEGRAM_API_TOKEN = 'fake-token' - process.env.TELEGRAM_CHAN_ID = 'fake-chan' - delete process.env.REVOLT_TOKEN + it('n\'interrompt pas la publication quand Telegram échoue, et encode le message', async () => { + process.env.TELEGRAM_API_TOKEN = 'fake-token'; + process.env.TELEGRAM_CHAN_ID = 'fake-chan'; + delete process.env.REVOLT_TOKEN; axios.post = vi.fn(async () => { - throw new Error('boom') - }) + throw new Error('boom'); + }); - const previous = {publishedAt: null, slug: 'foo&bar', titre: 'Mon titre', user: null, userAdmin: null, artistes: []} - const strapiMock = buildStrapi(previous) + const previous = {publishedAt: null, slug: 'foo&bar', titre: 'Mon titre', user: null, userAdmin: null, artistes: []}; + const strapiMock = buildStrapi(previous); - const {beforeUpdate} = await loadLifecycles(strapiMock) + const {beforeUpdate} = await loadLifecycles(strapiMock); - await beforeUpdate(buildEvent()) + await beforeUpdate(buildEvent()); - expect(axios.post).toHaveBeenCalledTimes(1) - const [calledUrl] = axios.post.mock.calls[0] - expect(calledUrl).toContain('text=') - expect(calledUrl.split('text=')[1]).not.toContain('&bar') - expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('Telegram')) - }) + expect(axios.post).toHaveBeenCalledTimes(1); + const [calledUrl] = axios.post.mock.calls[0]; + expect(calledUrl).toContain('text='); + expect(calledUrl.split('text=')[1]).not.toContain('&bar'); + expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('Telegram')); + }); - it("n'interrompt pas la publication quand Revolt échoue", async () => { - delete process.env.TELEGRAM_API_TOKEN - process.env.REVOLT_TOKEN = 'fake-token' - process.env.REVOLT_TARGET = 'fake-target' - process.env.REVOLT_BOT_ID = 'fake-bot' + it('n\'interrompt pas la publication quand Revolt échoue', async () => { + delete process.env.TELEGRAM_API_TOKEN; + process.env.REVOLT_TOKEN = 'fake-token'; + process.env.REVOLT_TARGET = 'fake-target'; + process.env.REVOLT_BOT_ID = 'fake-bot'; axios.post = vi.fn(async () => { - throw new Error('boom') - }) + throw new Error('boom'); + }); - const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: []} - const strapiMock = buildStrapi(previous) + const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: []}; + const strapiMock = buildStrapi(previous); - const {beforeUpdate} = await loadLifecycles(strapiMock) + const {beforeUpdate} = await loadLifecycles(strapiMock); - await beforeUpdate(buildEvent()) + await beforeUpdate(buildEvent()); - expect(axios.post).toHaveBeenCalledTimes(1) - expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('Revolt')) - }) -}) + expect(axios.post).toHaveBeenCalledTimes(1); + expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('Revolt')); + }); +}); describe('beforeUpdate — createdBy/updatedBy', () => { afterEach(() => { - delete global.strapi - }) + delete global.strapi; + }); it('retire createdBy/updatedBy du payload avant la mise à jour', async () => { - const dbQuery = {findOne: vi.fn(async () => ({publishedAt: null, artistes: []}))} - const strapiMock = {db: {query: vi.fn(() => dbQuery)}} + const dbQuery = {findOne: vi.fn(async () => ({publishedAt: null, artistes: []}))}; + const strapiMock = {db: {query: vi.fn(() => dbQuery)}}; - const {beforeUpdate} = await loadLifecycles(strapiMock) + const {beforeUpdate} = await loadLifecycles(strapiMock); const event = { state: {}, params: { data: {documentId: 'doc-1', createdBy: 999, updatedBy: 999} } - } + }; - await beforeUpdate(event) + await beforeUpdate(event); - expect(event.params.data.createdBy).toBeUndefined() - expect(event.params.data.updatedBy).toBeUndefined() - }) -}) + expect(event.params.data.createdBy).toBeUndefined(); + expect(event.params.data.updatedBy).toBeUndefined(); + }); +}); describe('afterUpdate — historique de différence', () => { afterEach(() => { - delete global.strapi - }) + delete global.strapi; + }); - it("n'échoue pas quand updatedBy n'est pas peuplé", async () => { - const entityServiceUpdate = vi.fn(async () => {}) + it('n\'échoue pas quand updatedBy n\'est pas peuplé', async () => { + const entityServiceUpdate = vi.fn(async () => {}); const strapiMock = { entityService: {update: entityServiceUpdate} - } + }; - const {afterUpdate} = await loadLifecycles(strapiMock) + const {afterUpdate} = await loadLifecycles(strapiMock); const event = { result: {id: 1, difference: [], updatedBy: undefined}, state: {diff: {path: 'transcription', jsonDiff: []}} - } + }; - await afterUpdate(event) + await afterUpdate(event); expect(entityServiceUpdate).toHaveBeenCalledWith('api::parole.parole', 1, { data: { @@ -173,6 +173,6 @@ describe('afterUpdate — historique de différence', () => { sources: 'transcription' }] } - }) - }) -}) + }); + }); +}); diff --git a/src/api/parole/content-types/parole/lifecycles.js b/src/api/parole/content-types/parole/lifecycles.js index e1b7494..b0ce667 100644 --- a/src/api/parole/content-types/parole/lifecycles.js +++ b/src/api/parole/content-types/parole/lifecycles.js @@ -1,22 +1,22 @@ 'use strict'; -const slugify = require('slugify') -const axios = require('axios') +const slugify = require('slugify'); +const axios = require('axios'); -const utils = require('@strapi/utils') -const { ApplicationError } = utils.errors +const utils = require('@strapi/utils'); +const { ApplicationError } = utils.errors; -const TELEGRAM_API_URL = 'https://api.telegram.org' -const TELEGRAM_CHAN_ID = process.env.TELEGRAM_CHAN_ID || null -const TELEGRAM_API_TOKEN = process.env.TELEGRAM_API_TOKEN || null -const MESSAGE_URL = `${TELEGRAM_API_URL}/bot${TELEGRAM_API_TOKEN}/sendMessage?chat_id=${TELEGRAM_CHAN_ID}&parse_mode=html` -const REVOLT_BOT_ID = process.env.REVOLT_BOT_ID || null -const REVOLT_TARGET = process.env.REVOLT_TARGET || null -const REVOLT_TOKEN = process.env.REVOLT_TOKEN || null +const TELEGRAM_API_URL = 'https://api.telegram.org'; +const TELEGRAM_CHAN_ID = process.env.TELEGRAM_CHAN_ID || null; +const TELEGRAM_API_TOKEN = process.env.TELEGRAM_API_TOKEN || null; +const MESSAGE_URL = `${TELEGRAM_API_URL}/bot${TELEGRAM_API_TOKEN}/sendMessage?chat_id=${TELEGRAM_CHAN_ID}&parse_mode=html`; +const REVOLT_BOT_ID = process.env.REVOLT_BOT_ID || null; +const REVOLT_TARGET = process.env.REVOLT_TARGET || null; +const REVOLT_TOKEN = process.env.REVOLT_TOKEN || null; const getSlug = (artiste, parole) => { - return slugify(`${artiste}-${parole}`, {lower: true, remove: /[*#+~.()'"!:@]/g}) -} + return slugify(`${artiste}-${parole}`, {lower: true, remove: /[*#+~.()'"!:@]/g}); +}; const isSlugExists = async existingSlug => { const slugs = await strapi.db.query('api::parole.parole').count({ @@ -25,10 +25,10 @@ const isSlugExists = async existingSlug => { $eq: existingSlug } } - }) + }); - return Boolean(slugs) -} + return Boolean(slugs); +}; const jwennAwtisEpiId = async artistesIds => { if (!artistesIds || artistesIds.length === 0) { @@ -42,30 +42,30 @@ const jwennAwtisEpiId = async artistesIds => { $in: artistesIds.map(id => id) } } - }) + }); - return artistes.map(a => a.alias).join('-') -} + return artistes.map(a => a.alias).join('-'); +}; const jwennUserEpiId = async userId => { if (!userId) { - return null + return null; } const user = await strapi.db.query('plugin::users-permissions.user').findOne({ where: {id: userId} - }) + }); if (!user) { - throw new ApplicationError('Utilisateur introuvable.') + throw new ApplicationError('Utilisateur introuvable.'); } - return user -} + return user; +}; const jwennUserAdminEpiId = async userAdminId => { if (!userAdminId) { - return null + return null; } const userAdmin = await strapi.db.query('admin::user').findOne({ @@ -83,14 +83,14 @@ const jwennUserAdminEpiId = async userAdminId => { } ] } - }) + }); - return userAdmin -} + return userAdmin; +}; const jwennSuperAdminEpiId = async userAdminId => { if (!userAdminId) { - return null + return null; } const userAdmin = await strapi.db.query('admin::user').findOne({ @@ -108,91 +108,91 @@ const jwennSuperAdminEpiId = async userAdminId => { } ] } - }) + }); - return userAdmin -} + return userAdmin; +}; module.exports = { beforeCreate: async event => { - let {data} = event.params + let {data} = event.params; - delete data.createdBy - delete data.updatedBy + delete data.createdBy; + delete data.updatedBy; - strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription) + strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription); - let artistesIds = [] + let artistesIds = []; if (data?.artistes?.connect?.length) { - artistesIds = data.artistes.connect.map(a => a.id) + artistesIds = data.artistes.connect.map(a => a.id); if (data.titre && !data.forceSlug) { - const artiste = await jwennAwtisEpiId(artistesIds) - data.slug = getSlug(artiste, data.titre) + const artiste = await jwennAwtisEpiId(artistesIds); + data.slug = getSlug(artiste, data.titre); } - const getSlugExistance = await isSlugExists(data.slug) + const getSlugExistance = await isSlugExists(data.slug); if (getSlugExistance) { - throw new ApplicationError('Un morceau du même artiste existe déjà.') + throw new ApplicationError('Un morceau du même artiste existe déjà.'); } } }, beforeUpdate: async event => { - const {state} = event - let {data} = event.params + const {state} = event; + let {data} = event.params; - delete data.createdBy - delete data.updatedBy + delete data.createdBy; + delete data.updatedBy; - const {documentId} = data + const {documentId} = data; if (data.isNewRelease === true) { await strapi.db.query('api::parole.parole').updateMany({ where: { isNewRelease: true }, data: { isNewRelease: false }, - }) + }); } const previousParoles = await strapi.db.query('api::parole.parole').findOne({ where: {documentId}, populate: {difference: true, artistes: true} - }) + }); - if (data.transcription && previousParoles.publishedAt) { - const difference = strapi.service('api::parole.parole').parolesDiff(data.titre, previousParoles.transcription, data.transcription) + if (data.transcription && previousParoles.publishedAt) { + const difference = strapi.service('api::parole.parole').parolesDiff(data.titre, previousParoles.transcription, data.transcription); - state.diff = difference - } + state.diff = difference; + } if(!data.publishedAt && data.titre && data.transcription) { - strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription) + strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription); if (data.titre && !data.forceSlug) { - let artistes + let artistes; if (data.artistes.connect.length === 0) { - artistes = previousParoles.artistes.map(a => a.alias).join('-') + artistes = previousParoles.artistes.map(a => a.alias).join('-'); } else { - let artistesIds = [] + let artistesIds = []; - artistesIds = data.artistes.connect.map(a => a.id) - artistes = await jwennAwtisEpiId(artistesIds) + artistesIds = data.artistes.connect.map(a => a.id); + artistes = await jwennAwtisEpiId(artistesIds); } - data.slug = getSlug(artistes, data.titre) + data.slug = getSlug(artistes, data.titre); } } if (data.publishedAt != null) { const previousData = await strapi.db.query('api::parole.parole').findOne({ where: {documentId} - }) + }); - const previousPublishedAt = previousData.publishedAt - const currentPublished_at = data.publishedAt + const previousPublishedAt = previousData.publishedAt; + const currentPublished_at = data.publishedAt; if (currentPublished_at != previousPublishedAt) { const message = `Nouvelle publication ❤️ -\n${process.env.WEBSITE_URL}/paroles/${previousData.slug}` +\n${process.env.WEBSITE_URL}/paroles/${previousData.slug}`; if (previousData.user) { strapi.plugins['email'].services.email.send({ from: process.env.SMTP_FROM, @@ -203,7 +203,7 @@ module.exports = { Merci pour votre contribution ❤️`, html: `

Le titre que vous avez soumis, "${previousData.titre}" a été publié sur le site.

Vous pouvez le trouver à l'adresse ${process.env.WEBSITE_URL}/paroles/${previousData.slug}.

Merci pour votre contribution ❤️

` - }) + }); } if (previousData.userAdmin) { @@ -216,66 +216,66 @@ module.exports = { Merci pour votre contribution ❤️`, html: `

Le titre que vous avez soumis, "${previousData.titre}" a été publié sur le site.

Vous pouvez le trouver à l'adresse ${process.env.WEBSITE_URL}/paroles/${previousData.slug}.

Merci pour votre contribution ❤️

` - }) + }); } if (TELEGRAM_API_TOKEN) { try { - await axios.post(`${MESSAGE_URL}&text=${encodeURIComponent(message)}`) + await axios.post(`${MESSAGE_URL}&text=${encodeURIComponent(message)}`); } catch (err) { - strapi.log.error(`Notification Telegram : ${err.message}`) + strapi.log.error(`Notification Telegram : ${err.message}`); } } if (REVOLT_TOKEN && REVOLT_TARGET && REVOLT_BOT_ID) { const revoltMessage = `Nouvelle publication - \n${process.env.WEBSITE_URL}/paroles/${previousData.slug}` + \n${process.env.WEBSITE_URL}/paroles/${previousData.slug}`; - const targetChannel = REVOLT_TARGET - const botToken = REVOLT_TOKEN - const url = `https://api.revolt.chat/channels/${targetChannel}/messages` + const targetChannel = REVOLT_TARGET; + const botToken = REVOLT_TOKEN; + const url = `https://api.revolt.chat/channels/${targetChannel}/messages`; const config = { headers: { 'X-Bot-Token': botToken, 'Content-Type': 'application/json' } - } + }; try { - await axios.post(url, {content: revoltMessage}, config) + await axios.post(url, {content: revoltMessage}, config); } catch (err) { - strapi.log.error(`Notification Revolt : ${err.message}`) + strapi.log.error(`Notification Revolt : ${err.message}`); } } } } }, afterUpdate: async event => { - const {result, state} = event + const {result, state} = event; if (state.diff) { await strapi.entityService.update('api::parole.parole', result.id, { data: { difference: [ - ...result.difference, - { - admin_user: result.updatedBy?.id ?? null, - paroles: state.diff.path, - jsonDiff: state.diff.jsonDiff, - date: new Date(), - sources: 'transcription' - }] + ...result.difference, + { + admin_user: result.updatedBy?.id ?? null, + paroles: state.diff.path, + jsonDiff: state.diff.jsonDiff, + date: new Date(), + sources: 'transcription' + }] } - }) + }); } }, afterCreate: async event => { - const {data} = event.params - const user = await jwennUserEpiId(data?.user?.id) - const userAdmin = await jwennUserAdminEpiId(data?.createdBy) - const superAdmin = await jwennSuperAdminEpiId(data?.createdBy) - const traductionsId = data?.traductions?.id + const {data} = event.params; + const user = await jwennUserEpiId(data?.user?.id); + const userAdmin = await jwennUserAdminEpiId(data?.createdBy); + const superAdmin = await jwennSuperAdminEpiId(data?.createdBy); + const traductionsId = data?.traductions?.id; if (traductionsId) { const result = await strapi.db.query('api::parole.parole').findOne({ @@ -287,15 +287,15 @@ module.exports = { } }, populate: {traductions: true, artistes: true} - }) + }); if (superAdmin && data.traductionAuto && result.traductions.francais && (!result.traductions.anglais || !result.traductions.espagnol || !result.traductions.allemand || !result.traductions.italien)) { - const traductions = await strapi.service('api::parole.parole').translateLyrics(result.traductions.francais) + const traductions = await strapi.service('api::parole.parole').translateLyrics(result.traductions.francais); await strapi.entityService.update('api::parole.parole', result.id, { data: { traductions } - }) + }); } } @@ -306,7 +306,7 @@ module.exports = { subject: `Nouveau texte de ${user.username} : "${data.titre}" (site)`, text: `Le titre "${data.titre}" a été soumis depuis le site.`, html: `Le titre "${data.titre}" a été soumis depuis le site.` - }) + }); } if (userAdmin) { @@ -316,7 +316,7 @@ module.exports = { subject: `Nouveau texte de ${userAdmin.firstname} : "${data.titre}" (site)`, text: `Le titre "${data.titre}" a été soumis depuis le site.`, html: `Le titre "${data.titre}" a été soumis depuis le site.` - }) + }); } } -} +}; diff --git a/src/api/parole/controllers/__tests__/parole.test.js b/src/api/parole/controllers/__tests__/parole.test.js index a789456..5ff67ce 100644 --- a/src/api/parole/controllers/__tests__/parole.test.js +++ b/src/api/parole/controllers/__tests__/parole.test.js @@ -1,20 +1,20 @@ -import {describe, it, expect, vi} from 'vitest' +import {describe, it, expect, vi} from 'vitest'; -const {default: createController} = await import('../parole.js') +const {default: createController} = await import('../parole.js'); function buildStrapi({dbUser, artiste}) { const paroleDocuments = { findMany: vi.fn(async () => []), create: vi.fn(async ({data}) => ({id: 42, ...data})), update: vi.fn(async ({data}) => ({id: 42, ...data})) - } + }; const userDocuments = { findOne: vi.fn(async () => dbUser), update: vi.fn(async () => {}) - } + }; const artisteDocuments = { findOne: vi.fn(async () => artiste) - } + }; const strapi = { contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'})), @@ -23,14 +23,14 @@ function buildStrapi({dbUser, artiste}) { translateLyrics: vi.fn() })), documents: vi.fn(uid => { - if (uid === 'plugin::users-permissions.user') return userDocuments - if (uid === 'api::artiste.artiste') return artisteDocuments - if (uid === 'api::parole.parole') return paroleDocuments - throw new Error(`unexpected uid: ${uid}`) + if (uid === 'plugin::users-permissions.user') return userDocuments; + if (uid === 'api::artiste.artiste') return artisteDocuments; + if (uid === 'api::parole.parole') return paroleDocuments; + throw new Error(`unexpected uid: ${uid}`); }) - } + }; - return {strapi, paroleDocuments, userDocuments, artisteDocuments} + return {strapi, paroleDocuments, userDocuments, artisteDocuments}; } function buildCtx(data) { @@ -41,11 +41,11 @@ function buildCtx(data) { }, badRequest: vi.fn(), notFound: vi.fn() - } + }; } -const dbUser = {id: 1, documentId: 'user-doc-1', username: 'foo', email: 'foo@bar.com'} -const artiste = {id: 9, documentId: 'artiste-doc-1'} +const dbUser = {id: 1, documentId: 'user-doc-1', username: 'foo', email: 'foo@bar.com'}; +const artiste = {id: 9, documentId: 'artiste-doc-1'}; function buildData(overrides = {}) { return { @@ -54,77 +54,77 @@ function buildData(overrides = {}) { user: {...dbUser}, artistes: [{documentId: 'artiste-doc-1'}], ...overrides - } + }; } describe('parole.findOne', () => { it('interroge avec le documentId venant de ctx.params.id, pas avec ctx lui-même', async () => { const paroleDocuments = { findOne: vi.fn(async ({documentId}) => ({id: 1, documentId, titre: 'Test'})) - } + }; const strapi = { contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'})), documents: vi.fn(uid => { - if (uid === 'api::parole.parole') return paroleDocuments - throw new Error(`unexpected uid: ${uid}`) + if (uid === 'api::parole.parole') return paroleDocuments; + throw new Error(`unexpected uid: ${uid}`); }) - } - const controller = createController({strapi}) - const ctx = {params: {id: 'doc-123'}} + }; + const controller = createController({strapi}); + const ctx = {params: {id: 'doc-123'}}; - const result = await controller.findOne(ctx) + const result = await controller.findOne(ctx); expect(paroleDocuments.findOne).toHaveBeenCalledWith({ documentId: 'doc-123', populate: ['artistes'] - }) - expect(result).toEqual({id: 1, documentId: 'doc-123', titre: 'Test'}) - }) -}) + }); + expect(result).toEqual({id: 1, documentId: 'doc-123', titre: 'Test'}); + }); +}); describe('parole.create', () => { it('crée la parole quand le user et l\'artiste existent', async () => { - const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste}) - const controller = createController({strapi}) - const ctx = buildCtx(buildData()) + const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste}); + const controller = createController({strapi}); + const ctx = buildCtx(buildData()); - await controller.create(ctx) + await controller.create(ctx); - expect(paroleDocuments.create).toHaveBeenCalled() - }) + expect(paroleDocuments.create).toHaveBeenCalled(); + }); it('refuse sans planter quand data.user est absent', async () => { - const {strapi, userDocuments} = buildStrapi({dbUser, artiste}) - const controller = createController({strapi}) - const ctx = buildCtx(buildData({user: undefined})) + const {strapi, userDocuments} = buildStrapi({dbUser, artiste}); + const controller = createController({strapi}); + const ctx = buildCtx(buildData({user: undefined})); - await controller.create(ctx) + await controller.create(ctx); - expect(ctx.badRequest).toHaveBeenCalled() - expect(userDocuments.findOne).not.toHaveBeenCalled() - }) + expect(ctx.badRequest).toHaveBeenCalled(); + expect(userDocuments.findOne).not.toHaveBeenCalled(); + }); it('refuse sans planter quand data.artistes est vide', async () => { - const {strapi, artisteDocuments} = buildStrapi({dbUser, artiste}) - const controller = createController({strapi}) - const ctx = buildCtx(buildData({artistes: []})) + const {strapi, artisteDocuments} = buildStrapi({dbUser, artiste}); + const controller = createController({strapi}); + const ctx = buildCtx(buildData({artistes: []})); - await controller.create(ctx) + await controller.create(ctx); - expect(ctx.badRequest).toHaveBeenCalled() - expect(artisteDocuments.findOne).not.toHaveBeenCalled() - }) + expect(ctx.badRequest).toHaveBeenCalled(); + expect(artisteDocuments.findOne).not.toHaveBeenCalled(); + }); it('ignore les champs non autorisés du payload (mass assignment)', async () => { - const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste}) - const controller = createController({strapi}) + const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste}); + const controller = createController({strapi}); const ctx = buildCtx(buildData({ userAdmin: {id: 999}, isNewRelease: true, difference: [{fake: true}] - })) + })); - await controller.create(ctx) + await controller.create(ctx); expect(paroleDocuments.create).toHaveBeenCalledWith({ data: { @@ -135,14 +135,14 @@ describe('parole.create', () => { artistes: [artiste.id], user: dbUser.id } - }) - }) -}) + }); + }); +}); describe('parole.update', () => { it('ignore les champs non autorisés du payload (mass assignment)', async () => { - const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste}) - const controller = createController({strapi}) + const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste}); + const controller = createController({strapi}); const ctx = buildCtx({ documentId: 'doc-1', titre: 'Nouveau titre', @@ -152,9 +152,9 @@ describe('parole.update', () => { artistes: [9], userAdmin: {id: 999}, user: {id: 999} - }) + }); - await controller.update(ctx) + await controller.update(ctx); expect(paroleDocuments.update).toHaveBeenCalledWith({ documentId: 'doc-1', @@ -165,6 +165,6 @@ describe('parole.update', () => { traductionAuto: true, artistes: [9] } - }) - }) -}) + }); + }); +}); diff --git a/src/api/parole/controllers/parole.js b/src/api/parole/controllers/parole.js index 264cd5b..f3287c6 100644 --- a/src/api/parole/controllers/parole.js +++ b/src/api/parole/controllers/parole.js @@ -2,29 +2,29 @@ const { createCoreController } = require('@strapi/strapi').factories; -const VALID_LANGS = new Set(['fr', 'en', 'es', 'de', 'it']) +const VALID_LANGS = new Set(['fr', 'en', 'es', 'de', 'it']); module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ async export(ctx) { - const { type = 'pairs', lang, format = 'jsonl' } = ctx.query + const { type = 'pairs', lang, format = 'jsonl' } = ctx.query; const langs = lang ? lang.split(',').map(l => l.trim()).filter(l => VALID_LANGS.has(l)) - : null + : null; if (lang && (!langs || langs.length === 0)) { - return ctx.badRequest('Langue(s) invalide(s). Valeurs acceptées : fr, en, es, de, it.') + return ctx.badRequest('Langue(s) invalide(s). Valeurs acceptées : fr, en, es, de, it.'); } if (!['pairs', 'instruct'].includes(type)) { - return ctx.badRequest('type invalide. Valeurs acceptées : pairs, instruct.') + return ctx.badRequest('type invalide. Valeurs acceptées : pairs, instruct.'); } - const paroles = await strapi.service('api::parole.parole').fetchAllParoles() - const { metadata, pairs } = strapi.service('api::parole.parole').buildExport(paroles, type, langs) + const paroles = await strapi.service('api::parole.parole').fetchAllParoles(); + const { metadata, pairs } = strapi.service('api::parole.parole').buildExport(paroles, type, langs); if (format === 'json') { - return ctx.send({ metadata, data: pairs }) + return ctx.send({ metadata, data: pairs }); } // JSONL : première ligne = métadonnées, suivies des exemples d'entraînement. @@ -32,30 +32,30 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ const lines = [ JSON.stringify({ _metadata: true, ...metadata }), ...pairs.map(p => JSON.stringify(p)), - ] + ]; - ctx.set('Content-Type', 'application/x-ndjson') - ctx.set('Content-Disposition', `attachment; filename="pawol-nu-export-${Date.now()}.jsonl"`) - ctx.body = lines.join('\n') + ctx.set('Content-Type', 'application/x-ndjson'); + ctx.set('Content-Disposition', `attachment; filename="pawol-nu-export-${Date.now()}.jsonl"`); + ctx.body = lines.join('\n'); }, async bulkTranslate(ctx) { - const result = await strapi.service('api::parole.parole').bulkTranslateMissing() - return ctx.send(result) + const result = await strapi.service('api::parole.parole').bulkTranslateMissing(); + return ctx.send(result); }, async findOne(ctx) { - const {id: documentId} = ctx.params + const {id: documentId} = ctx.params; const parole = await strapi.documents('api::parole.parole').findOne({ documentId, populate: ['artistes'] - }) + }); - return parole + return parole; }, async update(ctx) { - const {body} = ctx.request - const {data} = body + const {body} = ctx.request; + const {data} = body; const updatedParole = await strapi.documents('api::parole.parole').update({ documentId: data.documentId, @@ -67,38 +67,38 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ traductionAuto: data.traductionAuto, artistes: data.artistes } - }) + }); - return updatedParole + return updatedParole; }, async create(ctx) { - const {body} = ctx.request - const {data} = body + const {body} = ctx.request; + const {data} = body; if (!data?.user?.documentId || !data?.artistes?.[0]?.documentId) { - return ctx.badRequest('Informations manquantes.') + return ctx.badRequest('Informations manquantes.'); } - strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription) + strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription); const user = await strapi.documents('plugin::users-permissions.user').findOne({ documentId: body.data.user.documentId - }) + }); if (!user) { - return ctx.notFound('Utilisateur introuvable.') + return ctx.notFound('Utilisateur introuvable.'); } if (user.id !== data.user.id || user.username !== data.user.username || user.email !== data.user.email) { - return ctx.badRequest('Informations non valides.') + return ctx.badRequest('Informations non valides.'); } const artiste = await strapi.documents('api::artiste.artiste').findOne({ documentId: data.artistes[0].documentId - }) + }); if (!artiste) { - return ctx.notFound('Artiste introuvable.') + return ctx.notFound('Artiste introuvable.'); } const currentUserParole = await strapi.documents('api::parole.parole').findMany({ @@ -113,12 +113,12 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ $eq: null } } - }) + }); if (user && user.canAutoTranslate && data.traductionAuto && data.traductions.francais && (!data.traductions.anglais || !data.traductions.espagnol || !data.traductions.allemand || !data.traductions.italien)) { - const translated = await strapi.service('api::parole.parole').translateLyrics(data.traductions.francais) - data.traductions = translated + const translated = await strapi.service('api::parole.parole').translateLyrics(data.traductions.francais); + data.traductions = translated; } const newParole = await strapi.documents('api::parole.parole').create({ @@ -130,10 +130,10 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ artistes: [artiste.id], user: user.id } - }) + }); - const parolesIds = currentUserParole.map(({id}) => id) - parolesIds.push(newParole.id) + const parolesIds = currentUserParole.map(({id}) => id); + parolesIds.push(newParole.id); await strapi.documents('plugin::users-permissions.user').update({ documentId: user.documentId, @@ -141,8 +141,8 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ data: { paroles: parolesIds } - }) + }); - return newParole + return newParole; } -})) +})); diff --git a/src/api/parole/policies/__tests__/is-api-token.test.js b/src/api/parole/policies/__tests__/is-api-token.test.js index 0008320..321de3f 100644 --- a/src/api/parole/policies/__tests__/is-api-token.test.js +++ b/src/api/parole/policies/__tests__/is-api-token.test.js @@ -1,19 +1,19 @@ -import {describe, it, expect} from 'vitest' -import isApiToken from '../is-api-token.js' +import {describe, it, expect} from 'vitest'; +import isApiToken from '../is-api-token.js'; describe('is-api-token policy', () => { it('autorise une requête authentifiée par token API', () => { - const ctx = {state: {auth: {strategy: {name: 'api-token'}}}} - expect(isApiToken(ctx)).toBe(true) - }) + const ctx = {state: {auth: {strategy: {name: 'api-token'}}}}; + expect(isApiToken(ctx)).toBe(true); + }); it('refuse une requête authentifiée autrement (ex: JWT utilisateur)', () => { - const ctx = {state: {auth: {strategy: {name: 'users-permissions'}}}} - expect(isApiToken(ctx)).toBe(false) - }) + const ctx = {state: {auth: {strategy: {name: 'users-permissions'}}}}; + expect(isApiToken(ctx)).toBe(false); + }); it('refuse une requête non authentifiée', () => { - const ctx = {state: {}} - expect(isApiToken(ctx)).toBe(false) - }) -}) + const ctx = {state: {}}; + expect(isApiToken(ctx)).toBe(false); + }); +}); diff --git a/src/api/parole/policies/is-api-token.js b/src/api/parole/policies/is-api-token.js index ef061d2..244ec54 100644 --- a/src/api/parole/policies/is-api-token.js +++ b/src/api/parole/policies/is-api-token.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = policyContext => { - return policyContext.state?.auth?.strategy?.name === 'api-token' -} + return policyContext.state?.auth?.strategy?.name === 'api-token'; +}; diff --git a/src/api/parole/routes/parole.js b/src/api/parole/routes/parole.js index 69c520c..a08c8b6 100644 --- a/src/api/parole/routes/parole.js +++ b/src/api/parole/routes/parole.js @@ -14,4 +14,4 @@ module.exports = createCoreRouter('api::parole.parole', { policies: [{name: 'global::is-document-owner', config: {uid: 'api::parole.parole'}}] } } -}) +}); diff --git a/src/api/parole/services/__tests__/parole.test.js b/src/api/parole/services/__tests__/parole.test.js index 6127ac2..a435348 100644 --- a/src/api/parole/services/__tests__/parole.test.js +++ b/src/api/parole/services/__tests__/parole.test.js @@ -1,60 +1,60 @@ -import {describe, it, expect, vi, afterEach} from 'vitest' +import {describe, it, expect, vi, afterEach} from 'vitest'; -const {default: createService} = await import('../parole.js') +const {default: createService} = await import('../parole.js'); function fakeDeeplResponse(text) { return { ok: true, json: async () => ({translations: [{text}]}) - } + }; } describe('Translator (DeepL)', () => { - const originalFetch = global.fetch + const originalFetch = global.fetch; afterEach(() => { - global.fetch = originalFetch - vi.restoreAllMocks() - }) + global.fetch = originalFetch; + vi.restoreAllMocks(); + }); it('attache un timeout à la requête DeepL', async () => { - global.fetch = vi.fn(async () => fakeDeeplResponse('hello')) + global.fetch = vi.fn(async () => fakeDeeplResponse('hello')); - const strapi = {contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'}))} - const service = createService({strapi}) - await service.translate('FR', 'EN', 'bonjour') + const strapi = {contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'}))}; + const service = createService({strapi}); + await service.translate('FR', 'EN', 'bonjour'); - const [, options] = global.fetch.mock.calls[0] - expect(options.signal).toBeInstanceOf(AbortSignal) - }) -}) + const [, options] = global.fetch.mock.calls[0]; + expect(options.signal).toBeInstanceOf(AbortSignal); + }); +}); describe('translateLyrics', () => { - const originalFetch = global.fetch + const originalFetch = global.fetch; afterEach(() => { - global.fetch = originalFetch - vi.restoreAllMocks() - }) + global.fetch = originalFetch; + vi.restoreAllMocks(); + }); it('continue les autres langues quand une traduction DeepL échoue', async () => { global.fetch = vi.fn(async (_url, options) => { - const {target_lang: target} = JSON.parse(options.body) + const {target_lang: target} = JSON.parse(options.body); if (target === 'ES') { - return {ok: false, status: 500, text: async () => 'boom'} + return {ok: false, status: 500, text: async () => 'boom'}; } - return fakeDeeplResponse(`traduit-${target}`) - }) + return fakeDeeplResponse(`traduit-${target}`); + }); const strapi = { contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'})), log: {error: vi.fn()} - } - const service = createService({strapi}) - const result = await service.translateLyrics('Bonjour le monde') + }; + const service = createService({strapi}); + const result = await service.translateLyrics('Bonjour le monde'); - expect(result.anglais).toContain('traduit-EN') - expect(result.espagnol).toBeUndefined() - }) -}) + expect(result.anglais).toContain('traduit-EN'); + expect(result.espagnol).toBeUndefined(); + }); +}); diff --git a/src/api/stats/routes/stats.js b/src/api/stats/routes/stats.js index 61cfa56..d998c5e 100644 --- a/src/api/stats/routes/stats.js +++ b/src/api/stats/routes/stats.js @@ -9,4 +9,4 @@ module.exports = { } } ] -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/policies/__tests__/is-document-owner.test.js b/src/policies/__tests__/is-document-owner.test.js index c25f614..0b063ba 100644 --- a/src/policies/__tests__/is-document-owner.test.js +++ b/src/policies/__tests__/is-document-owner.test.js @@ -1,11 +1,11 @@ -import {describe, it, expect, vi} from 'vitest' +import {describe, it, expect, vi} from 'vitest'; -const {default: isDocumentOwner} = await import('../is-document-owner.js') +const {default: isDocumentOwner} = await import('../is-document-owner.js'); function buildStrapi({jwtUserId, document}) { const dbQuery = { findOne: vi.fn(async () => document) - } + }; return { plugins: { @@ -21,7 +21,7 @@ function buildStrapi({jwtUserId, document}) { query: vi.fn(() => dbQuery) }, dbQuery - } + }; } function buildPolicyContext({authorization = 'Bearer faketoken', paramId, bodyDocumentId} = {}) { @@ -31,43 +31,43 @@ function buildPolicyContext({authorization = 'Bearer faketoken', paramId, bodyDo header: authorization ? {authorization} : {}, body: {data: {documentId: bodyDocumentId}} } - } + }; } describe('is-document-owner policy', () => { - it("refuse quand aucun en-tête d'autorisation n'est présent", async () => { - const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}}) - const policyContext = buildPolicyContext({authorization: null, paramId: 'doc-1'}) + it('refuse quand aucun en-tête d\'autorisation n\'est présent', async () => { + const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}}); + const policyContext = buildPolicyContext({authorization: null, paramId: 'doc-1'}); - await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Opération non autorisée') - }) + await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Opération non autorisée'); + }); it('autorise quand le user du JWT est le propriétaire du document (id dans les params)', async () => { - const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}}) - const policyContext = buildPolicyContext({paramId: 'doc-1'}) + const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}}); + const policyContext = buildPolicyContext({paramId: 'doc-1'}); - await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).resolves.toBe(true) - expect(strapi.dbQuery.findOne).toHaveBeenCalledWith({where: {documentId: 'doc-1'}, populate: {user: true}}) - }) + await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).resolves.toBe(true); + expect(strapi.dbQuery.findOne).toHaveBeenCalledWith({where: {documentId: 'doc-1'}, populate: {user: true}}); + }); it('autorise quand le documentId vient du corps de la requête (cas du contrôleur parole.update)', async () => { - const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}}) - const policyContext = buildPolicyContext({bodyDocumentId: 'doc-1'}) + const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}}); + const policyContext = buildPolicyContext({bodyDocumentId: 'doc-1'}); - await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).resolves.toBe(true) - }) + await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).resolves.toBe(true); + }); - it("refuse quand le user du JWT n'est pas le propriétaire du document", async () => { - const strapi = buildStrapi({jwtUserId: 999, document: {documentId: 'doc-1', user: {id: 1}}}) - const policyContext = buildPolicyContext({paramId: 'doc-1'}) + it('refuse quand le user du JWT n\'est pas le propriétaire du document', async () => { + const strapi = buildStrapi({jwtUserId: 999, document: {documentId: 'doc-1', user: {id: 1}}}); + const policyContext = buildPolicyContext({paramId: 'doc-1'}); - await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Opération non autorisée') - }) + await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Opération non autorisée'); + }); - it("refuse quand le document ciblé n'existe pas", async () => { - const strapi = buildStrapi({jwtUserId: 1, document: null}) - const policyContext = buildPolicyContext({paramId: 'doc-inconnu'}) + it('refuse quand le document ciblé n\'existe pas', async () => { + const strapi = buildStrapi({jwtUserId: 1, document: null}); + const policyContext = buildPolicyContext({paramId: 'doc-inconnu'}); - await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Ressource introuvable.') - }) -}) + await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Ressource introuvable.'); + }); +}); diff --git a/src/policies/__tests__/is-payload-owner.test.js b/src/policies/__tests__/is-payload-owner.test.js index 1fc909e..c13cb70 100644 --- a/src/policies/__tests__/is-payload-owner.test.js +++ b/src/policies/__tests__/is-payload-owner.test.js @@ -1,6 +1,6 @@ -import {describe, it, expect, vi} from 'vitest' +import {describe, it, expect, vi} from 'vitest'; -const {default: isPayloadOwner} = await import('../is-payload-owner.js') +const {default: isPayloadOwner} = await import('../is-payload-owner.js'); function buildStrapi(jwtUserId) { return { @@ -13,7 +13,7 @@ function buildStrapi(jwtUserId) { } } } - } + }; } function buildPolicyContext({authorization, payloadUserId}) { @@ -22,30 +22,30 @@ function buildPolicyContext({authorization, payloadUserId}) { header: authorization ? {authorization} : {}, body: {data: {user: {id: payloadUserId}}} } - } + }; } describe('is-payload-owner policy', () => { - it("refuse quand aucun en-tête d'autorisation n'est présent", async () => { - const strapi = buildStrapi(999) - const policyContext = buildPolicyContext({authorization: undefined, payloadUserId: 1}) + it('refuse quand aucun en-tête d\'autorisation n\'est présent', async () => { + const strapi = buildStrapi(999); + const policyContext = buildPolicyContext({authorization: undefined, payloadUserId: 1}); - await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée') - }) + await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée'); + }); it('autorise quand le user du JWT correspond au user du payload', async () => { - const strapi = buildStrapi(1) - const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1}) + const strapi = buildStrapi(1); + const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1}); - await expect(isPayloadOwner(policyContext, {}, {strapi})).resolves.toBe(true) - }) + await expect(isPayloadOwner(policyContext, {}, {strapi})).resolves.toBe(true); + }); it('refuse quand le user du JWT ne correspond pas au user du payload', async () => { - const strapi = buildStrapi(999) - const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1}) + const strapi = buildStrapi(999); + const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1}); - await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée') - }) + await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée'); + }); it('refuse quand le token est invalide', async () => { const strapi = { @@ -54,15 +54,15 @@ describe('is-payload-owner policy', () => { services: { jwt: { getToken: vi.fn(async () => { - throw new Error('Invalid token.') + throw new Error('Invalid token.'); }) } } } } - } - const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1}) + }; + const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1}); - await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée') - }) -}) + await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée'); + }); +}); diff --git a/src/policies/is-document-owner.js b/src/policies/is-document-owner.js index f25bed7..628a327 100644 --- a/src/policies/is-document-owner.js +++ b/src/policies/is-document-owner.js @@ -1,35 +1,35 @@ 'use strict'; -const { UnauthorizedError, NotFoundError } = require('@strapi/utils').errors +const { UnauthorizedError, NotFoundError } = require('@strapi/utils').errors; module.exports = async (policyContext, config, {strapi}) => { - const {request, params} = policyContext + const {request, params} = policyContext; if (!request?.header?.authorization) { - throw new UnauthorizedError('Opération non autorisée') + throw new UnauthorizedError('Opération non autorisée'); } - let jwtUserId + let jwtUserId; try { - ({id: jwtUserId} = await strapi.plugins['users-permissions'].services.jwt.getToken(policyContext)) + ({id: jwtUserId} = await strapi.plugins['users-permissions'].services.jwt.getToken(policyContext)); } catch (err) { - throw new UnauthorizedError('Opération non autorisée') + throw new UnauthorizedError('Opération non autorisée'); } - const documentId = params?.id ?? request.body?.data?.documentId + const documentId = params?.id ?? request.body?.data?.documentId; const document = await strapi.db.query(config.uid).findOne({ where: {documentId}, populate: {user: true} - }) + }); if (!document) { - throw new NotFoundError('Ressource introuvable.') + throw new NotFoundError('Ressource introuvable.'); } if (document.user?.id !== jwtUserId) { - throw new UnauthorizedError('Opération non autorisée') + throw new UnauthorizedError('Opération non autorisée'); } - return true -} + return true; +}; diff --git a/src/policies/is-payload-owner.js b/src/policies/is-payload-owner.js index aa5cac5..7dd3242 100644 --- a/src/policies/is-payload-owner.js +++ b/src/policies/is-payload-owner.js @@ -1,23 +1,23 @@ 'use strict'; -const { UnauthorizedError } = require('@strapi/utils').errors +const { UnauthorizedError } = require('@strapi/utils').errors; module.exports = async (policyContext, config, {strapi}) => { - const {request} = policyContext + const {request} = policyContext; if (!request?.header?.authorization) { - throw new UnauthorizedError('Opération non autorisée') + throw new UnauthorizedError('Opération non autorisée'); } try { - const {id} = await strapi.plugins['users-permissions'].services.jwt.getToken(policyContext) + const {id} = await strapi.plugins['users-permissions'].services.jwt.getToken(policyContext); if (id !== request.body?.data?.user?.id) { - throw new UnauthorizedError('Opération non autorisée') + throw new UnauthorizedError('Opération non autorisée'); } } catch (err) { - throw new UnauthorizedError('Opération non autorisée') + throw new UnauthorizedError('Opération non autorisée'); } - return true -} + return true; +}; diff --git a/src/utils/__tests__/backup-database.test.js b/src/utils/__tests__/backup-database.test.js index 74b43f2..cbae91f 100644 --- a/src/utils/__tests__/backup-database.test.js +++ b/src/utils/__tests__/backup-database.test.js @@ -1,62 +1,62 @@ -import {describe, it, expect, afterEach} from 'vitest' -import fs from 'fs' -import os from 'os' -import path from 'path' -import {backupDatabase} from '../backup-database.js' +import {describe, it, expect, afterEach} from 'vitest'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import {backupDatabase} from '../backup-database.js'; -const tmpDirs = [] +const tmpDirs = []; function makeTmpDir() { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'backup-database-test-')) - tmpDirs.push(dir) - return dir + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'backup-database-test-')); + tmpDirs.push(dir); + return dir; } describe('backupDatabase', () => { afterEach(() => { for (const dir of tmpDirs.splice(0)) { - fs.rmSync(dir, {recursive: true, force: true}) + fs.rmSync(dir, {recursive: true, force: true}); } - }) + }); - it("ne fait rien quand le fichier de base n'existe pas", () => { - const root = makeTmpDir() + it('ne fait rien quand le fichier de base n\'existe pas', () => { + const root = makeTmpDir(); const result = backupDatabase({ dbPath: path.join(root, 'inexistant.db'), backupsDir: path.join(root, 'backups') - }) + }); - expect(result).toBeNull() - expect(fs.existsSync(path.join(root, 'backups'))).toBe(false) - }) + expect(result).toBeNull(); + expect(fs.existsSync(path.join(root, 'backups'))).toBe(false); + }); it('copie le fichier de base dans le dossier de sauvegardes', () => { - const root = makeTmpDir() - const dbPath = path.join(root, 'data.db') - fs.writeFileSync(dbPath, 'contenu-de-la-base') - const backupsDir = path.join(root, 'backups') + const root = makeTmpDir(); + const dbPath = path.join(root, 'data.db'); + fs.writeFileSync(dbPath, 'contenu-de-la-base'); + const backupsDir = path.join(root, 'backups'); - const result = backupDatabase({dbPath, backupsDir}) + const result = backupDatabase({dbPath, backupsDir}); - expect(result).not.toBeNull() - expect(fs.existsSync(result)).toBe(true) - expect(fs.readFileSync(result, 'utf8')).toBe('contenu-de-la-base') - }) + expect(result).not.toBeNull(); + expect(fs.existsSync(result)).toBe(true); + expect(fs.readFileSync(result, 'utf8')).toBe('contenu-de-la-base'); + }); it('ne conserve que les 8 sauvegardes les plus récentes', () => { - const root = makeTmpDir() - const dbPath = path.join(root, 'data.db') - fs.writeFileSync(dbPath, 'contenu') - const backupsDir = path.join(root, 'backups') - fs.mkdirSync(backupsDir, {recursive: true}) + const root = makeTmpDir(); + const dbPath = path.join(root, 'data.db'); + fs.writeFileSync(dbPath, 'contenu'); + const backupsDir = path.join(root, 'backups'); + fs.mkdirSync(backupsDir, {recursive: true}); for (let i = 0; i < 10; i++) { - fs.writeFileSync(path.join(backupsDir, `data-2020-01-0${i}.db`), 'ancien') + fs.writeFileSync(path.join(backupsDir, `data-2020-01-0${i}.db`), 'ancien'); } - backupDatabase({dbPath, backupsDir}) + backupDatabase({dbPath, backupsDir}); - const remaining = fs.readdirSync(backupsDir).filter(name => name.startsWith('data-')) - expect(remaining).toHaveLength(8) - }) -}) + const remaining = fs.readdirSync(backupsDir).filter(name => name.startsWith('data-')); + expect(remaining).toHaveLength(8); + }); +}); diff --git a/yarn.lock b/yarn.lock index 520f2f2..b0d6dfb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1036,6 +1036,38 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz#8fe30b3088b89b4873c3a6cc87597ae3920c0a8b" integrity sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg== +"@eslint-community/eslint-utils@^4.2.0": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" + integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.6.1": + version "4.12.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.57.1": + version "8.57.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" + integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== + "@floating-ui/core@^1.0.5": version "1.1.0" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.1.0.tgz#0a1dee4bbce87ff71602625d33f711cafd8afc08" @@ -1169,6 +1201,25 @@ resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-3.0.0.tgz#f11fdf7dda62fe8e336fa7c6642d9041f30356d7" integrity sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w== +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== + dependencies: + "@humanwhocodes/object-schema" "^2.0.3" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + "@img/sharp-darwin-arm64@0.33.5": version "0.33.5" resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08" @@ -1507,7 +1558,7 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3": +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -3818,6 +3869,11 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== +"@ungap/structured-clone@^1.2.0": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.2.tgz#a03ad82cd5676414d068ba86f880c5681194aadf" + integrity sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA== + "@vercel/oidc@3.0.5": version "3.0.5" resolved "https://registry.yarnpkg.com/@vercel/oidc/-/oidc-3.0.5.tgz#bd8db7ee777255c686443413492db4d98ef49657" @@ -4044,6 +4100,11 @@ acorn-import-phases@^1.0.3: resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7" integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ== +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + acorn-walk@^8.0.0: version "8.3.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f" @@ -4064,6 +4125,11 @@ acorn@^8.5.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== +acorn@^8.9.0: + version "8.17.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe" + integrity sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg== + addressparser@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/addressparser/-/addressparser-1.0.1.tgz#47afbe1a2a9262191db6838e4fd1d39b40821746" @@ -4126,6 +4192,16 @@ ajv@8.18.0, ajv@^8.9.0: json-schema-traverse "^1.0.0" require-from-string "^2.0.2" +ajv@^6.12.4: + version "6.15.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.15.0.tgz#07e982c74626167aa7a2495c53817892d7139492" + integrity sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -4659,7 +4735,7 @@ chai@^6.2.2: resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e" integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg== -chalk@4.1.2, chalk@^4.1.0, chalk@^4.1.2: +chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -5154,6 +5230,15 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +cross-spawn@^7.0.2: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + crypto-random-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" @@ -5304,6 +5389,11 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + deepmerge@^2.1.1: version "2.2.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" @@ -5450,6 +5540,13 @@ dnd-core@^16.0.1: "@react-dnd/invariant" "^4.0.1" redux "^4.2.0" +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + dom-accessibility-api@^0.5.9: version "0.5.16" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" @@ -5851,16 +5948,89 @@ eslint-scope@5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.7.0: + version "8.57.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" + integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.1" + "@humanwhocodes/config-array" "^0.13.0" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + esm@^3.2.25: version "3.2.25" resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + esprima@^4.0.0, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== +esquery@^1.4.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d" + integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" @@ -5873,6 +6043,11 @@ estraverse@^4.1.1: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== +estraverse@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + estraverse@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" @@ -5890,6 +6065,11 @@ estree-walker@^3.0.3: dependencies: "@types/estree" "^1.0.0" +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -5963,6 +6143,11 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + fast-safe-stringify@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" @@ -5990,6 +6175,13 @@ fecha@^4.2.0: resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + file-selector@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-2.1.2.tgz#fe7c7ee9e550952dfbc863d73b14dc740d7de8b4" @@ -6097,6 +6289,20 @@ flagged-respawn@^2.0.0: resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-2.0.0.tgz#abf39719dcfe1ac06c86c9466081c541c682987b" integrity sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA== +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flatted@^3.2.9: + version "3.4.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" + integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== + flow-parser@0.*: version "0.309.0" resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.309.0.tgz#ca2eae0b1a604cafbba99863785a92f7164671ee" @@ -6406,6 +6612,13 @@ glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + glob-to-regexp@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" @@ -6462,6 +6675,13 @@ global-prefix@^1.0.1: is-windows "^1.0.1" which "^1.2.14" +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + globalthis@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" @@ -6521,6 +6741,11 @@ grant@5.4.24: jwk-to-pem "^2.0.7" jws "^4.0.0" +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + gzip-size@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" @@ -6868,6 +7093,11 @@ ignore-by-default@^1.0.1: resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -7088,6 +7318,13 @@ is-generator-function@^1.0.7: resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.8.tgz#dfb5c2b120e02b0a8d9d2c6806cd5621aa922f7b" integrity sha512-2Omr/twNtufVZFr1GhxjOMFPAj2sjc/dKaIqBhvo4qciXfJmITGH6ZGd8eZYNHza8t1y0e01AuqRhJwfWp26WQ== +is-glob@^4.0.0, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" @@ -7095,13 +7332,6 @@ is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-glob@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - is-hexadecimal@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" @@ -7132,6 +7362,11 @@ is-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-plain-obj@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" @@ -7268,6 +7503,13 @@ js-yaml@^3.13.0: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.0.tgz#d1900572a7f7cf0b5f540c83673e60bad3436592" + integrity sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q== + dependencies: + argparse "^2.0.1" + jscodeshift@17.3.0: version "17.3.0" resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-17.3.0.tgz#b9ea1d8d1c9255103bfc4cb42ddb46e18cb2415c" @@ -7327,6 +7569,11 @@ json-schema@^0.4.0: resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + json5@^2.1.2: version "2.1.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" @@ -7443,6 +7690,13 @@ keyv@^4.0.0: compress-brotli "^1.3.8" json-buffer "3.0.1" +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + kind-of@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -7611,6 +7865,14 @@ kuler@^2.0.0: resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + libbase64@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/libbase64/-/libbase64-0.1.0.tgz#62351a839563ac5ff5bd26f12f60e9830bb751e6" @@ -7816,6 +8078,11 @@ lodash.isplainobject@4.0.6: resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -8526,6 +8793,13 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@^3.0.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== + dependencies: + brace-expansion "^1.1.7" + minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -8673,6 +8947,11 @@ napi-build-utils@^1.0.1: resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -8916,6 +9195,18 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + ora@5.4.1, ora@^5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" @@ -9401,6 +9692,11 @@ preferred-pm@3.1.3: path-exists "^4.0.0" which-pm "2.0.0" +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + prettier@3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" @@ -10133,7 +10429,7 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@3.0.2: +rimraf@3.0.2, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -10826,6 +11122,11 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -11009,6 +11310,11 @@ text-hex@1.0.x: resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" @@ -11214,6 +11520,13 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -11751,6 +12064,11 @@ winston@3.10.0: triple-beam "^1.3.0" winston-transport "^4.5.0" +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" From 2c84ea0f25d59411bdb11b275d7f62e8da6d0887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 20:01:16 +0400 Subject: [PATCH 28/28] =?UTF-8?q?fix:=20corriger=20les=20erreurs=20ESLint?= =?UTF-8?q?=20restantes=20apr=C3=A8s=20activation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - retirer les paramètres/variables inutilisés (result, ctx/next, qs) - corriger l'indentation tabs/espaces mélangés dans src/admin/app.js - retirer les échappements regex inutiles dans stripMarkdown - remplacer while(true) par for(;;) (faux positif no-constant-condition) --- src/admin/app.js | 42 ++-- .../content-types/commentaire/lifecycles.js | 40 ++-- src/api/parole/services/parole.js | 202 +++++++++--------- src/api/stats/controllers/stats.js | 10 +- 4 files changed, 147 insertions(+), 147 deletions(-) diff --git a/src/admin/app.js b/src/admin/app.js index 69d3340..950041d 100644 --- a/src/admin/app.js +++ b/src/admin/app.js @@ -15,14 +15,14 @@ export default { fr: { 'Auth.form.welcome.subtitle': `Connectez-vous à votre compte ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API`, 'Auth.form.welcome.title': `Bienvenue sur ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API !`, - 'LeftMenu.navbrand.title': 'Tableau de bord', - 'LeftMenu.navbrand.workplace': 'Menu', + 'LeftMenu.navbrand.title': 'Tableau de bord', + 'LeftMenu.navbrand.workplace': 'Menu', }, en: { - 'Auth.form.welcome.subtitle': `Log in to your ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API account`, - 'Auth.form.welcome.title': `Welcome to ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API !`, - 'LeftMenu.navbrand.title': 'Dashboard', - 'LeftMenu.navbrand.workplace': 'Workplace', + 'Auth.form.welcome.subtitle': `Log in to your ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API account`, + 'Auth.form.welcome.title': `Welcome to ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API !`, + 'LeftMenu.navbrand.title': 'Dashboard', + 'LeftMenu.navbrand.workplace': 'Workplace', } }, menu: { @@ -30,9 +30,9 @@ export default { }, theme: { colors: { - buttonNeutral0: '#ffffff', - buttonPrimary500: '#4caf50', - buttonPrimary600: '#43a047', + buttonNeutral0: '#ffffff', + buttonPrimary500: '#4caf50', + buttonPrimary600: '#43a047', primary100: '#c8e6c9', primary200: '#a5d6a7', primary500: '#4caf50', @@ -54,18 +54,18 @@ export default { succes600: '#7cb342', succes700: '#689f38', danger700: '#d32f2f', - neutral0: '#ffffff', - neutral100: '#f6f6f9', - neutral1000: '#181826', - neutral150: '#eaeaef', - neutral200: '#dcdce4', - neutral300: '#c0c0cf', - neutral400: '#a5a5ba', - neutral500: '#8e8ea9', - neutral600: '#666687', - neutral700: '#4a4a6a', - neutral800: '#32324d', - neutral900: '#212134' + neutral0: '#ffffff', + neutral100: '#f6f6f9', + neutral1000: '#181826', + neutral150: '#eaeaef', + neutral200: '#dcdce4', + neutral300: '#c0c0cf', + neutral400: '#a5a5ba', + neutral500: '#8e8ea9', + neutral600: '#666687', + neutral700: '#4a4a6a', + neutral800: '#32324d', + neutral900: '#212134' }, }, tutorials: false, diff --git a/src/api/commentaire/content-types/commentaire/lifecycles.js b/src/api/commentaire/content-types/commentaire/lifecycles.js index eb2a599..2ee4b10 100644 --- a/src/api/commentaire/content-types/commentaire/lifecycles.js +++ b/src/api/commentaire/content-types/commentaire/lifecycles.js @@ -1,57 +1,57 @@ 'use strict'; -const { ApplicationError, NotFoundError } = require("@strapi/utils").errors +const { ApplicationError, NotFoundError } = require('@strapi/utils').errors; const jwennUserEpiId = async userId => { if (!userId) { - return null + return null; } const user = await strapi.db.query('plugin::users-permissions.user').findOne({ where: {id: userId} - }) + }); - return user -} + return user; +}; const jwennParoleEpiId = async paroleId => { if (!paroleId) { - return null + return null; } const parole = await strapi.db.query('api::parole.parole').findOne({ where: {id: paroleId} - }) + }); - return parole -} + return parole; +}; const validateCommentaire = data => { if (!data.contenu && !data.datePublication) { - throw new ApplicationError('Mauvaise requête, contenu et datePublication sont obligatoires') + throw new ApplicationError('Mauvaise requête, contenu et datePublication sont obligatoires'); } if (!data.contenu || data.contenu.trim().length === 0) { - throw new ApplicationError('Champ obligatoire. Veuillez renseigner le contenu du commentaire.') + throw new ApplicationError('Champ obligatoire. Veuillez renseigner le contenu du commentaire.'); } if (data.contenu.trim().length > 500) { - throw new ApplicationError('Le commentaire doit contenir 500 caractères maximum.') + throw new ApplicationError('Le commentaire doit contenir 500 caractères maximum.'); } -} +}; module.exports = { beforeCreate: async event => { - const {data} = event.params - validateCommentaire(data) + const {data} = event.params; + validateCommentaire(data); }, afterCreate: async event => { - const {data, result} = event.params - const user = await jwennUserEpiId(data.user) - const parole = await jwennParoleEpiId(data.parole) + const {data} = event.params; + const user = await jwennUserEpiId(data.user); + const parole = await jwennParoleEpiId(data.parole); if (!parole) { - throw new NotFoundError('Texte introuvable.') + throw new NotFoundError('Texte introuvable.'); } if (user) { @@ -60,7 +60,7 @@ module.exports = { to: process.env.SMTP_SEND_TO, subject: `Commentaire de ${user.username} sur "${parole.titre}"`, text: data.contenu - }) + }); } } }; diff --git a/src/api/parole/services/parole.js b/src/api/parole/services/parole.js index 536b9e4..d329e32 100644 --- a/src/api/parole/services/parole.js +++ b/src/api/parole/services/parole.js @@ -1,9 +1,8 @@ 'use strict'; -const qs = require('qs') -const Diff = require('diff') +const Diff = require('diff'); const { createCoreService } = require('@strapi/strapi').factories; -const { ApplicationError } = require("@strapi/utils").errors +const { ApplicationError } = require('@strapi/utils').errors; const LANG_MAP = { fr: { field: 'francais', targetLang: 'fr', userPrompt: 'Tradui an fransé' }, @@ -14,43 +13,43 @@ const LANG_MAP = { pt: { field: 'portugais', targetLang: 'pt', userPrompt: 'Traduza para o português', deeplTarget: 'PT-BR', suffix: '\n\n (Traduzido pela DeepL)' }, ja: { field: 'japonais', targetLang: 'ja', userPrompt: '日本語に翻訳して', deeplTarget: 'JA', suffix: '\n\n (DeepLによる翻訳)' }, ko: { field: 'coreen', targetLang: 'ko', userPrompt: '한국어로 번역해줘', deeplTarget: 'KO', suffix: '\n\n (DeepL 번역)' }, -} +}; -const ALL_LANGS = Object.keys(LANG_MAP) +const ALL_LANGS = Object.keys(LANG_MAP); function stripMarkdown(text) { - if (!text) return '' + if (!text) return ''; return text .replace(/#{1,6}\s+/g, '') .replace(/\*\*(.*?)\*\*/gs, '$1') .replace(/\*(.*?)\*/gs, '$1') .replace(/__(.*?)__/gs, '$1') .replace(/_(.*?)_/gs, '$1') - .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') - .replace(/^[>\-\*\+]\s+/gm, '') + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + .replace(/^[>\-*+]\s+/gm, '') .replace(/\n{3,}/g, '\n\n') - .trim() + .trim(); } // Détecte si une transcription est probablement en français plutôt qu'en KA. // Heuristique : si les pronoms personnels français représentent > 4 % des mots. -const FR_PRONOUNS = new Set(['je', 'tu', 'il', 'elle', 'nous', 'vous', 'ils', 'elles']) +const FR_PRONOUNS = new Set(['je', 'tu', 'il', 'elle', 'nous', 'vous', 'ils', 'elles']); function suspectFrench(text) { - if (!text) return false - const words = text.toLowerCase().match(/\b[a-zàâäéèêëîïôöùûüç]+\b/g) || [] - if (words.length < 10) return false - const frCount = words.filter(w => FR_PRONOUNS.has(w)).length - return frCount / words.length > 0.04 + if (!text) return false; + const words = text.toLowerCase().match(/\b[a-zàâäéèêëîïôöùûüç]+\b/g) || []; + if (words.length < 10) return false; + const frCount = words.filter(w => FR_PRONOUNS.has(w)).length; + return frCount / words.length > 0.04; } -const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); class Translator { constructor() { - this.deeplApi = process.env.DEEPL_URL || 'api-free.deepl.com' - this.deeplKey = process.env.DEEPL_KEY - this.urlRequest = `https://${this.deeplApi}/v2/translate` + this.deeplApi = process.env.DEEPL_URL || 'api-free.deepl.com'; + this.deeplKey = process.env.DEEPL_KEY; + this.urlRequest = `https://${this.deeplApi}/v2/translate`; } async get(origin, target, text) { @@ -66,40 +65,40 @@ class Translator { target_lang: target, }), signal: AbortSignal.timeout(15_000) - }) + }); if (!response.ok) { - const body = await response.text() - console.error('DeepL error:', body) - throw new Error(`DeepL ${response.status}: ${body}`) + const body = await response.text(); + console.error('DeepL error:', body); + throw new Error(`DeepL ${response.status}: ${body}`); } - return await response.json() + return await response.json(); } } -const translator = new Translator() +const translator = new Translator(); module.exports = createCoreService('api::parole.parole', ({strapi}) => ({ async translate(origin, target, text) { - const data = await translator.get(origin, target, text) - return data.translations[0].text + const data = await translator.get(origin, target, text); + return data.translations[0].text; }, async translateLyrics(parolesFR) { - const result = { francais: parolesFR } + const result = { francais: parolesFR }; for (const lang of ALL_LANGS) { - if (lang === 'fr') continue - const { field, deeplTarget, suffix } = LANG_MAP[lang] + if (lang === 'fr') continue; + const { field, deeplTarget, suffix } = LANG_MAP[lang]; try { - const translated = await this.translate('FR', deeplTarget, parolesFR) - result[field] = translated + suffix + const translated = await this.translate('FR', deeplTarget, parolesFR); + result[field] = translated + suffix; } catch (err) { - strapi.log.error(`DeepL (${deeplTarget}): ${err.message}`) + strapi.log.error(`DeepL (${deeplTarget}): ${err.message}`); } } - return result + return result; }, validateParoles(titre, transcription) { if (!titre || titre.trim().length === 0) { @@ -107,77 +106,77 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({ } if (!transcription || transcription.trim().length === 0) { - throw new ApplicationError('Champ obligatoire. Veuillez renseigner la transcription.') + throw new ApplicationError('Champ obligatoire. Veuillez renseigner la transcription.'); } if (transcription.trim().length < 10) { - throw new ApplicationError('La transcription doit contenir au moins 10 caractères.') + throw new ApplicationError('La transcription doit contenir au moins 10 caractères.'); } }, async fetchAllParoles() { - const pageSize = 100 - let start = 0 - const all = [] + const pageSize = 100; + let start = 0; + const all = []; - while (true) { + for (;;) { const batch = await strapi.documents('api::parole.parole').findMany({ status: 'published', populate: ['artistes', 'traductions'], fields: ['documentId', 'titre', 'slug', 'transcription', 'annee', 'langueSource'], limit: pageSize, start, - }) - all.push(...batch) - if (batch.length < pageSize) break - start += pageSize + }); + all.push(...batch); + if (batch.length < pageSize) break; + start += pageSize; } - return all + return all; }, buildExport(paroles, type, langs) { - const targetLangs = langs && langs.length ? langs : ALL_LANGS - const pairs = [] - const missing = [] - const nonKa = [] - const langCounts = {} + const targetLangs = langs && langs.length ? langs : ALL_LANGS; + const pairs = []; + const missing = []; + const nonKa = []; + const langCounts = {}; for (const parole of paroles) { - const source = stripMarkdown(parole.transcription) - const sourceLang = parole.langueSource || 'ka' - const artists = (parole.artistes || []).map(a => a.alias) - const paroleMeta = { title: parole.titre, artists } + const source = stripMarkdown(parole.transcription); + const sourceLang = parole.langueSource || 'ka'; + const artists = (parole.artistes || []).map(a => a.alias); + const paroleMeta = { title: parole.titre, artists }; if (sourceLang !== 'ka') { - nonKa.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, suspected_lang: sourceLang }) + nonKa.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, suspected_lang: sourceLang }); } else if (suspectFrench(source)) { - nonKa.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, suspected_lang: 'fr' }) + nonKa.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, suspected_lang: 'fr' }); } - const missingLangs = ALL_LANGS.filter(lang => !parole.traductions?.[LANG_MAP[lang].field]) + const missingLangs = ALL_LANGS.filter(lang => !parole.traductions?.[LANG_MAP[lang].field]); if (missingLangs.length > 0) { - missing.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, missing: missingLangs }) + missing.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, missing: missingLangs }); } for (const lang of targetLangs) { - const { field, targetLang, userPrompt } = LANG_MAP[lang] - if (lang === sourceLang) continue - const target = stripMarkdown(parole.traductions?.[field]) - if (!target) continue + const { field, targetLang, userPrompt } = LANG_MAP[lang]; + if (lang === sourceLang) continue; + const target = stripMarkdown(parole.traductions?.[field]); + if (!target) continue; - langCounts[lang] = (langCounts[lang] || 0) + 1 + langCounts[lang] = (langCounts[lang] || 0) + 1; if (type === 'instruct') { const systemPrompt = sourceLang === 'ka' ? 'Tu es un expert en langue KA (créole guadeloupéen/martiniquais). Traduis le texte KA suivant.' - : `Tu es un expert en traduction. Traduis le texte suivant (langue source : ${sourceLang}).` + : `Tu es un expert en traduction. Traduis le texte suivant (langue source : ${sourceLang}).`; pairs.push({ messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: `${userPrompt} :\n\n${source}` }, { role: 'assistant', content: target }, ], - }) + }); } else { pairs.push({ source_lang: sourceLang, @@ -185,7 +184,7 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({ source, target, ...paroleMeta, - }) + }); } } } @@ -197,60 +196,61 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({ languages: langCounts, missing_translations: missing, non_ka_transcriptions: nonKa, - } + }; - return { metadata, pairs } + return { metadata, pairs }; }, async bulkTranslateMissing() { const TARGET_LANGS = ALL_LANGS .filter(lang => lang !== 'fr') - .map(lang => ({ lang, ...LANG_MAP[lang] })) + .map(lang => ({ lang, ...LANG_MAP[lang] })); - const pageSize = 100 - let start = 0 - const all = [] - while (true) { + const pageSize = 100; + let start = 0; + const all = []; + for (;;) { const batch = await strapi.documents('api::parole.parole').findMany({ status: 'published', populate: ['traductions'], fields: ['documentId', 'slug', 'titre', 'transcription', 'langueSource'], limit: pageSize, start, - }) - all.push(...batch) - if (batch.length < pageSize) break - start += pageSize + }); + all.push(...batch); + if (batch.length < pageSize) break; + start += pageSize; } - const translated = [] - const skipped = [] - const errors = [] + const translated = []; + const skipped = []; + const errors = []; for (const parole of all) { const sourceFR = parole.traductions?.francais - || (parole.langueSource === 'fr' ? parole.transcription : null) + || (parole.langueSource === 'fr' ? parole.transcription : null); - if (!sourceFR) { skipped.push(parole.slug); continue } + if (!sourceFR) { skipped.push(parole.slug); continue; } - const missing = TARGET_LANGS.filter(({ field }) => !parole.traductions?.[field]) - if (missing.length === 0) { skipped.push(parole.slug); continue } + const missing = TARGET_LANGS.filter(({ field }) => !parole.traductions?.[field]); + if (missing.length === 0) { skipped.push(parole.slug); continue; } - const { id: _id, ...tradData } = parole.traductions || {} - const updatedTrad = { ...tradData } - const addedLangs = [] + // eslint-disable-next-line no-unused-vars -- id exclu intentionnellement du spread + const { id: _id, ...tradData } = parole.traductions || {}; + const updatedTrad = { ...tradData }; + const addedLangs = []; for (const { lang, field, deeplTarget, suffix } of missing) { try { - await sleep(700) - const result = await translator.get('FR', deeplTarget, sourceFR) - const text = result?.translations?.[0]?.text + await sleep(700); + const result = await translator.get('FR', deeplTarget, sourceFR); + const text = result?.translations?.[0]?.text; if (text) { - updatedTrad[field] = text + suffix - addedLangs.push(lang) + updatedTrad[field] = text + suffix; + addedLangs.push(lang); } } catch (err) { - errors.push({ slug: parole.slug, lang: deeplTarget, error: err.message }) + errors.push({ slug: parole.slug, lang: deeplTarget, error: err.message }); } } @@ -258,28 +258,28 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({ await strapi.documents('api::parole.parole').update({ documentId: parole.documentId, data: { traductions: updatedTrad }, - }) + }); await strapi.documents('api::parole.parole').publish({ documentId: parole.documentId, - }) - translated.push({ slug: parole.slug, langs: addedLangs }) + }); + translated.push({ slug: parole.slug, langs: addedLangs }); } } - return { translated, skipped, errors } + return { translated, skipped, errors }; }, parolesDiff(titre = '', oldString, newString) { - const patch = Diff.createPatch(titre, oldString, newString, 'supprimée', 'ajoutée') - const parsePatch = Diff.parsePatch(patch) + const patch = Diff.createPatch(titre, oldString, newString, 'supprimée', 'ajoutée'); + const parsePatch = Diff.parsePatch(patch); if (parsePatch[0].hunks.length > 0) { - const jsonDiff = Diff.diffWords(oldString, newString) + const jsonDiff = Diff.diffWords(oldString, newString); return { patch, jsonDiff - } + }; } } })); diff --git a/src/api/stats/controllers/stats.js b/src/api/stats/controllers/stats.js index 3a7946e..aaa7731 100644 --- a/src/api/stats/controllers/stats.js +++ b/src/api/stats/controllers/stats.js @@ -1,15 +1,15 @@ 'use strict'; module.exports = { - async count(ctx, next) { + async count() { const countArtiste = await strapi.documents('api::artiste.artiste').count({ publicationState: 'live' - }) + }); const countParole = await strapi.documents('api::parole.parole').count({ publicationState: 'live' - }) + }); - return {countArtiste, countParole} + return {countArtiste, countParole}; } -} +};