diff --git a/backend/package-lock.json b/backend/package-lock.json index d8fbed5..502abc6 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1895,7 +1895,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "dev": true, "license": "MIT", "optional": true }, @@ -3486,7 +3485,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -3499,7 +3497,6 @@ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", "deprecated": "This functionality has been moved to @npmcli/fs", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -3514,7 +3511,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, "license": "MIT", "optional": true, "bin": { @@ -3529,7 +3525,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -5621,7 +5616,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, "license": "ISC", "optional": true }, @@ -5723,7 +5717,6 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -5737,7 +5730,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -5919,7 +5911,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "dev": true, "license": "ISC", "optional": true }, @@ -6003,7 +5994,6 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", "deprecated": "This package is no longer supported.", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -6691,7 +6681,6 @@ "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -6722,7 +6711,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -6736,7 +6724,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -6750,7 +6737,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, "license": "MIT", "optional": true, "bin": { @@ -6765,7 +6751,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -6782,7 +6767,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -7052,7 +7036,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -7163,7 +7146,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, "license": "ISC", "optional": true, "bin": { @@ -7288,7 +7270,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true, "license": "ISC", "optional": true }, @@ -7738,7 +7719,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true, "license": "MIT", "optional": true }, @@ -8127,7 +8107,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -8138,7 +8117,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, "license": "MIT", "optional": true }, @@ -9450,7 +9428,6 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", "deprecated": "This package is no longer supported.", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -9972,7 +9949,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true, "license": "ISC", "optional": true }, @@ -10052,7 +10028,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, "license": "BSD-2-Clause", "optional": true }, @@ -10137,7 +10112,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -10233,7 +10207,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -10243,7 +10217,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -10254,7 +10227,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true, "license": "ISC", "optional": true }, @@ -10537,7 +10509,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true, "license": "MIT", "optional": true }, @@ -12069,7 +12040,6 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12098,7 +12068,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -12109,7 +12078,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12123,7 +12091,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12139,7 +12106,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12154,7 +12120,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12168,7 +12133,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12182,7 +12146,6 @@ "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -12193,7 +12156,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -12363,7 +12325,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12377,7 +12338,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12391,7 +12351,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -12399,7 +12358,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12418,7 +12376,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12432,7 +12389,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -12440,7 +12396,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12454,7 +12409,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12468,7 +12422,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -12476,7 +12429,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12490,7 +12442,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12504,7 +12455,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -12512,7 +12462,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12526,7 +12475,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12540,7 +12488,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -12797,7 +12744,6 @@ "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12839,7 +12785,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12931,7 +12876,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12971,7 +12915,6 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", "deprecated": "This package is no longer supported.", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -13172,7 +13115,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -13596,7 +13538,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true, "license": "ISC", "optional": true }, @@ -13604,7 +13545,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -13619,7 +13559,6 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -14333,7 +14272,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, "license": "ISC", "optional": true }, @@ -14537,7 +14475,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/simple-concat": { @@ -14636,7 +14574,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -14753,7 +14690,6 @@ "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -14769,7 +14705,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -14785,7 +14720,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -14888,7 +14822,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14902,7 +14835,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14916,7 +14848,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -16253,7 +16184,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -16264,7 +16194,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -16711,7 +16640,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { diff --git a/backend/src/auth/services/authorization.service.ts b/backend/src/auth/services/authorization.service.ts index 58d5dc5..fec2b37 100644 --- a/backend/src/auth/services/authorization.service.ts +++ b/backend/src/auth/services/authorization.service.ts @@ -1,11 +1,11 @@ -import { Injectable } from "@nestjs/common"; -import { InjectRepository } from "@nestjs/typeorm"; -import { Repository } from "typeorm"; -import { Dispute } from "../../entities/dispute.entity"; -import { Participant } from "../../entities/participant.entity"; -import { Split } from "../../entities/split.entity"; -import { Group } from "../../group/entities/group.entity"; -import { Receipt } from "../../receipts/entities/receipt.entity"; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Dispute } from '../../entities/dispute.entity'; +import { Participant } from '../../entities/participant.entity'; +import { Split } from '../../entities/split.entity'; +import { Group } from '../../group/entities/group.entity'; +import { Receipt } from '../../receipts/entities/receipt.entity'; @Injectable() export class AuthorizationService { @@ -22,18 +22,12 @@ export class AuthorizationService { private groupRepository: Repository, ) {} - // Split authorization methods async canAccessSplit(userId: string, splitId: string): Promise { const split = await this.splitRepository.findOne({ where: { id: splitId }, - relations: ["participants"], + relations: ['participants'], }); - - if (!split) { - return false; - } - - // Check if user is a participant or creator + if (!split) return false; return ( split.participants.some((p: Participant) => p.userId === userId) || split.creatorWalletAddress === userId @@ -41,31 +35,19 @@ export class AuthorizationService { } async canCreatePayment(userId: string, splitId: string): Promise { - // Users can create payments for splits they participate in return this.canAccessSplit(userId, splitId); } async canAddParticipant(userId: string, splitId: string): Promise { - // Only split creator can add participants (or participants can invite others) - const split = await this.splitRepository.findOne({ - where: { id: splitId }, - }); - + const split = await this.splitRepository.findOne({ where: { id: splitId } }); return ( split?.creatorWalletAddress === userId || (await this.canAccessSplit(userId, splitId)) ); } - async canRemoveParticipant( - userId: string, - splitId: string, - ): Promise { - // Only split creator can remove participants - const split = await this.splitRepository.findOne({ - where: { id: splitId }, - }); - + async canRemoveParticipant(userId: string, splitId: string): Promise { + const split = await this.splitRepository.findOne({ where: { id: splitId } }); return split?.creatorWalletAddress === userId; } @@ -74,142 +56,100 @@ export class AuthorizationService { splitId: string, participantId: string, ): Promise { - // Users can only create payments for themselves or if they're the split creator - if (!(await this.canAccessSplit(userId, splitId))) { - return false; - } - + if (!(await this.canAccessSplit(userId, splitId))) return false; const participant = await this.participantRepository.findOne({ where: { id: participantId, splitId }, }); - - if (!participant) { - return false; - } - - // User can create payment for themselves or if they're the creator + if (!participant) return false; return ( participant.userId === userId || (await this.isSplitCreator(userId, splitId)) ); } - async canAccessParticipantPayments( - userId: string, - participantId: string, - ): Promise { + async canAccessParticipantPayments(userId: string, participantId: string): Promise { const participant = await this.participantRepository.findOne({ where: { id: participantId }, - relations: ["split"], + relations: ['split'], }); - - if (!participant) { - return false; - } - - // User can access their own payments or if they can access the split + if (!participant) return false; return ( participant.userId === userId || (await this.canAccessSplit(userId, participant.splitId)) ); } - // Receipt authorization methods async canAccessReceipt(userId: string, receiptId: string): Promise { const receipt = await this.receiptRepository.findOne({ where: { id: receiptId }, - relations: ["split"], + relations: ['split'], }); - - if (!receipt) { - return false; - } - + if (!receipt) return false; return this.canAccessSplit(userId, receipt.splitId); } - // Dispute authorization methods async canAccessDispute(userId: string, disputeId: string): Promise { - const dispute = await this.disputeRepository.findOne({ - where: { id: disputeId }, - }); - - if (!dispute) { - return false; - } - - // Users can access disputes for splits they participate in + const dispute = await this.disputeRepository.findOne({ where: { id: disputeId } }); + if (!dispute) return false; return this.canAccessSplit(userId, dispute.splitId); } async isAdmin(userId: string): Promise { - // TODO: Implement admin check based on user roles - // For now, return false - no admin functionality return false; } - // Group authorization methods async canAccessGroup(userId: string, groupId: string): Promise { - const group = await this.groupRepository.findOne({ - where: { id: groupId }, - }); - - if (!group) { - return false; - } - - // Check if user is creator or member + const group = await this.groupRepository.findOne({ where: { id: groupId } }); + if (!group) return false; return ( group.creatorId === userId || group.members.some((member: any) => member.wallet === userId) ); } - async canManageGroupMembers( - userId: string, - groupId: string, - ): Promise { - const group = await this.groupRepository.findOne({ - where: { id: groupId }, - }); - - if (!group) { - return false; - } - - // Only creator and admins can manage members + async canManageGroupMembers(userId: string, groupId: string): Promise { + const group = await this.groupRepository.findOne({ where: { id: groupId } }); + if (!group) return false; return ( group.creatorId === userId || - group.members.some( - (member: any) => member.wallet === userId && member.role === "admin", - ) + group.members.some((member: any) => member.wallet === userId && member.role === 'admin') ); } async canCreateGroupSplit(userId: string, groupId: string): Promise { - // Any group member can create splits return this.canAccessGroup(userId, groupId); } - // Helper methods - private async isSplitCreator( - userId: string, - splitId: string, - ): Promise { - const split = await this.splitRepository.findOne({ - where: { id: splitId }, + // ? NEW - Can user generate a short link for this split? + async canGenerateShortLink(userId: string, splitId: string): Promise { + return this.canAccessSplit(userId, splitId); + } + + // ? NEW - Can user delete a short link? + async canDeleteShortLink(userId: string, splitId: string): Promise { + return this.isSplitCreator(userId, splitId); + } + + // ? NEW - Can user view analytics for a short link? + async canViewShortLinkAnalytics(userId: string, splitId: string): Promise { + return this.isSplitCreator(userId, splitId); + } + + // ? NEW - Is participant actually part of this split? + async isParticipantInSplit(participantId: string, splitId: string): Promise { + const participant = await this.participantRepository.findOne({ + where: { id: participantId, splitId }, }); + return !!participant; + } + private async isSplitCreator(userId: string, splitId: string): Promise { + const split = await this.splitRepository.findOne({ where: { id: splitId } }); return split?.creatorWalletAddress === userId; } - // Batch authorization for multiple resources - async filterAccessibleSplits( - userId: string, - splitIds: string[], - ): Promise { + async filterAccessibleSplits(userId: string, splitIds: string[]): Promise { const splits = await this.splitRepository.findByIds(splitIds); - return splits .filter( (split: Split) => @@ -219,39 +159,25 @@ export class AuthorizationService { .map((split: Split) => split.id); } - async filterAccessibleReceipts( - userId: string, - receiptIds: string[], - ): Promise { + async filterAccessibleReceipts(userId: string, receiptIds: string[]): Promise { const receipts = await this.receiptRepository.findByIds(receiptIds); - const accessibleSplitIds = await this.filterAccessibleSplits( userId, receipts.map((r) => r.splitId), ); - return receipts - .filter((receipt: Receipt) => - accessibleSplitIds.includes(receipt.splitId), - ) + .filter((receipt: Receipt) => accessibleSplitIds.includes(receipt.splitId)) .map((receipt: Receipt) => receipt.id); } - async filterAccessibleDisputes( - userId: string, - disputeIds: string[], - ): Promise { + async filterAccessibleDisputes(userId: string, disputeIds: string[]): Promise { const disputes = await this.disputeRepository.findByIds(disputeIds); - const accessibleSplitIds = await this.filterAccessibleSplits( userId, disputes.map((d) => d.splitId), ); - return disputes - .filter((dispute: Dispute) => - accessibleSplitIds.includes(dispute.splitId), - ) + .filter((dispute: Dispute) => accessibleSplitIds.includes(dispute.splitId)) .map((dispute: Dispute) => dispute.id); } } diff --git a/backend/src/short-links/entities/link-access-log.entity.ts b/backend/src/short-links/entities/link-access-log.entity.ts index 978d962..f089c3a 100644 --- a/backend/src/short-links/entities/link-access-log.entity.ts +++ b/backend/src/short-links/entities/link-access-log.entity.ts @@ -4,15 +4,15 @@ import { Column, ManyToOne, CreateDateColumn, -} from "typeorm"; -import { SplitShortLink } from "./split-short-link.entity"; +} from 'typeorm'; +import { SplitShortLink } from './split-short-link.entity'; -@Entity("link_access_logs") +@Entity('link_access_logs') export class LinkAccessLog { - @PrimaryGeneratedColumn("uuid") + @PrimaryGeneratedColumn('uuid') id!: string; - @ManyToOne(() => SplitShortLink, { onDelete: "CASCADE" }) + @ManyToOne(() => SplitShortLink, { onDelete: 'CASCADE' }) shortLink!: SplitShortLink; @CreateDateColumn() diff --git a/backend/src/short-links/short-links.service.ts b/backend/src/short-links/short-links.service.ts index a2f5879..f03d4b5 100644 --- a/backend/src/short-links/short-links.service.ts +++ b/backend/src/short-links/short-links.service.ts @@ -3,13 +3,14 @@ import { BadRequestException, NotFoundException, ForbiddenException, -} from "@nestjs/common"; -import { InjectRepository } from "@nestjs/typeorm"; -import { Repository } from "typeorm"; -import { SplitShortLink } from "./entities/split-short-link.entity"; -import { LinkAccessLog } from "./entities/link-access-log.entity"; -import { GenerateLinkDto } from "./dto/generate-link.dto"; -import * as crypto from "crypto"; +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { SplitShortLink } from './entities/split-short-link.entity'; +import { LinkAccessLog } from './entities/link-access-log.entity'; +import { GenerateLinkDto } from './dto/generate-link.dto'; +import { AuthorizationService } from '../auth/services/authorization.service'; +import * as crypto from 'crypto'; @Injectable() export class ShortLinksService { @@ -19,36 +20,50 @@ export class ShortLinksService { @InjectRepository(LinkAccessLog) private accessLogRepo: Repository, + + private readonly authorizationService: AuthorizationService, ) {} // Generate 6-char unique short code private async generateUniqueCode(): Promise { - // Initialise `code` to a definite string so it is never "used before assigned" - let code = crypto.randomBytes(4).toString("base64url").slice(0, 6); + let code = crypto.randomBytes(4).toString('base64url').slice(0, 6); let exists = true; - while (exists) { - code = crypto.randomBytes(4).toString("base64url").slice(0, 6); - const found = await this.shortLinkRepo.findOne({ - where: { shortCode: code }, - }); + code = crypto.randomBytes(4).toString('base64url').slice(0, 6); + const found = await this.shortLinkRepo.findOne({ where: { shortCode: code } }); exists = !!found; } - return code; } - // Generate link + // ? Generate link - now checks split ownership async generate(dto: GenerateLinkDto, wallet: string) { + // Check user is allowed to generate a link for this split + const canGenerate = await this.authorizationService.canGenerateShortLink(wallet, dto.splitId); + if (!canGenerate) { + throw new ForbiddenException('You are not a member of this split'); + } + + // If a target participant is provided, make sure they belong to this split + if (dto.targetParticipantId) { + const validParticipant = await this.authorizationService.isParticipantInSplit( + dto.targetParticipantId, + dto.splitId, + ); + if (!validParticipant) { + throw new BadRequestException('Target participant does not belong to this split'); + } + } + + // Check link count limit per user per split const count = await this.shortLinkRepo.count({ where: { split: { id: dto.splitId }, createdBy: wallet, }, }); - if (count >= 20) { - throw new ForbiddenException("Link generation limit reached"); + throw new ForbiddenException('Link generation limit reached'); } const shortCode = await this.generateUniqueCode(); @@ -65,6 +80,7 @@ export class ShortLinksService { ? ({ id: dto.targetParticipantId } as any) : null, expiresAt: expiry, + maxAccesses: dto.maxAccesses ?? null, createdBy: wallet, }); @@ -72,49 +88,67 @@ export class ShortLinksService { return { shortCode, - url: `${process.env.FRONTEND_URL}/l/${shortCode}`, + url: ${process.env.FRONTEND_URL}/l/, sep0007: this.buildSep0007Uri(dto.splitId), expiresAt: expiry, }; } - // Resolve link - async resolve( - shortCode: string, - ip: string, - userAgent: string, - userId?: string, - ) { + // ? Resolve link - checks expiry and max access + async resolve(shortCode: string, ip: string, userAgent: string, userId?: string) { const link = await this.shortLinkRepo.findOne({ where: { shortCode }, - relations: ["split"], + relations: ['split'], }); - if (!link) throw new NotFoundException("Link not found"); - if (link.expiresAt < new Date()) - throw new BadRequestException("Link expired"); + if (!link) throw new NotFoundException('Link not found'); + + // Check expiry + if (link.expiresAt < new Date()) { + throw new BadRequestException('Link has expired'); + } + + // Check max access limit if (link.maxAccesses && link.accessCount >= link.maxAccesses) { - throw new ForbiddenException("Max access reached"); + throw new ForbiddenException('This link has reached its maximum number of uses'); } + // Increment access count link.accessCount++; await this.shortLinkRepo.save(link); + // Log access await this.accessLogRepo.save({ shortLink: link, - ipHash: crypto.createHash("sha256").update(ip).digest("hex"), + ipHash: crypto.createHash('sha256').update(ip).digest('hex'), userAgent, resolvedUserId: userId, }); return { - redirectUrl: `${process.env.FRONTEND_URL}/splits/${link.split.id}`, + redirectUrl: ${process.env.FRONTEND_URL}/splits/, linkType: link.linkType, }; } - // Analytics - async analytics(shortCode: string) { + // ? Analytics - only split creator can view + async analytics(shortCode: string, userId: string) { + const link = await this.shortLinkRepo.findOne({ + where: { shortCode }, + relations: ['split'], + }); + + if (!link) throw new NotFoundException('Link not found'); + + // Only the split creator can see analytics + const canView = await this.authorizationService.canViewShortLinkAnalytics( + userId, + link.split.id, + ); + if (!canView) { + throw new ForbiddenException('Only the split creator can view analytics'); + } + const logs = await this.accessLogRepo.find({ where: { shortLink: { shortCode } }, }); @@ -124,20 +158,32 @@ export class ShortLinksService { uniqueIPs: new Set(logs.map((l) => l.ipHash)).size, lastAccess: [...logs].sort( (a, b) => b.accessedAt.getTime() - a.accessedAt.getTime(), - )[0], + )[0] ?? null, }; } - // Delete — renamed from `delete` (reserved word) to `remove` - async remove(shortCode: string): Promise { - const result = await this.shortLinkRepo.delete({ shortCode }); - if (result.affected === 0) { - throw new NotFoundException(`Short link "${shortCode}" not found`); + // ? Delete - only split creator can delete + async remove(shortCode: string, userId: string): Promise { + const link = await this.shortLinkRepo.findOne({ + where: { shortCode }, + relations: ['split'], + }); + + if (!link) throw new NotFoundException(Short link not found); + + // Only split creator can delete links + const canDelete = await this.authorizationService.canDeleteShortLink( + userId, + link.split.id, + ); + if (!canDelete) { + throw new ForbiddenException('Only the split creator can delete links'); } + + await this.shortLinkRepo.delete({ shortCode }); } - // SEP-0007 URI private buildSep0007Uri(splitId: string): string { - return `web+stellar:pay?destination=${process.env.PLATFORM_WALLET}&memo=${splitId}`; + return web+stellar:pay?destination=&memo=; } } diff --git a/backend/src/split-comments/dto/split-comment.dto.ts b/backend/src/split-comments/dto/split-comment.dto.ts index efeed1a..160bd77 100644 --- a/backend/src/split-comments/dto/split-comment.dto.ts +++ b/backend/src/split-comments/dto/split-comment.dto.ts @@ -1,19 +1,4 @@ -import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm"; - -@Entity('split_comments') export class CreateSplitCommentDto { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column() splitId!: string; - - @Column() - userId!: string; - - @Column('text') comment!: string; - - @CreateDateColumn() - createdAt!: Date; } diff --git a/backend/src/split-comments/provider/provider.service.ts b/backend/src/split-comments/provider/provider.service.ts index 15b2f7f..0de3260 100644 --- a/backend/src/split-comments/provider/provider.service.ts +++ b/backend/src/split-comments/provider/provider.service.ts @@ -1,13 +1,11 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { EventEmitter2 } from '@nestjs/event-emitter'; - import { SplitComment } from '../split-comment.entity'; import { CreateSplitCommentDto } from '../dto/split-comment.dto'; import { MentionService } from '@/mentions/provider/service'; - @Injectable() export class SplitCommentService { constructor( @@ -25,7 +23,6 @@ export class SplitCommentService { }); const mentions = this.mentionService.extractMentions(dto.comment); - if (mentions.length) { this.eventEmitter.emit('comment.mentioned', { splitId: dto.splitId, @@ -37,4 +34,30 @@ export class SplitCommentService { return comment; } + + async listComments(splitId: string, page: number, limit: number) { + const [data, total] = await this.repo.findAndCount({ + where: { splitId }, + order: { createdAt: 'DESC' }, + skip: (page - 1) * limit, + take: limit, + }); + + return { data, total, page, limit }; + } + + async deleteComment(id: string, userId: string) { + const comment = await this.repo.findOne({ where: { id } }); + + if (!comment) { + throw new NotFoundException('Comment not found'); + } + + if (comment.userId !== userId) { + throw new ForbiddenException('You can only delete your own comments'); + } + + await this.repo.remove(comment); + return { message: 'Comment deleted successfully' }; + } } diff --git a/backend/src/split-comments/split-comment.entity.ts b/backend/src/split-comments/split-comment.entity.ts index a17d2f7..35e2ab6 100644 --- a/backend/src/split-comments/split-comment.entity.ts +++ b/backend/src/split-comments/split-comment.entity.ts @@ -1,6 +1,6 @@ -import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from "typeorm"; +import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; -@Entity ('split_comments') +@Entity('split_comments') @Index(['splitId']) export class SplitComment { @PrimaryGeneratedColumn('uuid') diff --git a/backend/src/split-comments/split-comments.controller.ts b/backend/src/split-comments/split-comments.controller.ts index 5007a30..22bf374 100644 --- a/backend/src/split-comments/split-comments.controller.ts +++ b/backend/src/split-comments/split-comments.controller.ts @@ -1,4 +1,38 @@ -import { Controller } from '@nestjs/common'; +import { + Controller, + Post, + Get, + Delete, + Body, + Param, + Query, + Req, +} from '@nestjs/common'; +import { SplitCommentService } from './provider/provider.service'; +import { CreateSplitCommentDto } from './dto/split-comment.dto'; @Controller('split-comments') -export class SplitCommentsController {} +export class SplitCommentsController { + constructor(private readonly splitCommentService: SplitCommentService) {} + + @Post() + async create(@Body() dto: CreateSplitCommentDto, @Req() req: any) { + const userId = req.user?.id ?? 'test-user'; + return this.splitCommentService.createComment(userId, dto); + } + + @Get(':splitId') + async list( + @Param('splitId') splitId: string, + @Query('page') page = 1, + @Query('limit') limit = 10, + ) { + return this.splitCommentService.listComments(splitId, Number(page), Number(limit)); + } + + @Delete(':id') + async delete(@Param('id') id: string, @Req() req: any) { + const userId = req.user?.id ?? 'test-user'; + return this.splitCommentService.deleteComment(id, userId); + } +} diff --git a/backend/src/split-comments/split-comments.spec.ts b/backend/src/split-comments/split-comments.spec.ts new file mode 100644 index 0000000..188b68a --- /dev/null +++ b/backend/src/split-comments/split-comments.spec.ts @@ -0,0 +1,90 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SplitCommentService } from './provider/provider.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { SplitComment } from './split-comment.entity'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { MentionService } from '@/mentions/provider/service'; + +const mockRepo = { + save: jest.fn(), + findAndCount: jest.fn(), + findOne: jest.fn(), + remove: jest.fn(), +}; + +const mockMentionService = { + extractMentions: jest.fn(), +}; + +const mockEventEmitter = { + emit: jest.fn(), +}; + +describe('SplitCommentService', () => { + let service: SplitCommentService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SplitCommentService, + { provide: getRepositoryToken(SplitComment), useValue: mockRepo }, + { provide: MentionService, useValue: mockMentionService }, + { provide: EventEmitter2, useValue: mockEventEmitter }, + ], + }).compile(); + + service = module.get(SplitCommentService); + }); + + afterEach(() => jest.clearAllMocks()); + + it('should create a comment', async () => { + const dto = { splitId: 'split-1', comment: 'hello world' }; + mockRepo.save.mockResolvedValue({ id: 'c1', ...dto, userId: 'u1' }); + mockMentionService.extractMentions.mockReturnValue([]); + + const result = await service.createComment('u1', dto); + + expect(mockRepo.save).toHaveBeenCalled(); + expect(result.splitId).toBe('split-1'); + }); + + it('should emit mention event when comment has mentions', async () => { + const dto = { splitId: 'split-1', comment: 'hello @john' }; + mockRepo.save.mockResolvedValue({ id: 'c1', ...dto, userId: 'u1' }); + mockMentionService.extractMentions.mockReturnValue(['john']); + + await service.createComment('u1', dto); + + expect(mockEventEmitter.emit).toHaveBeenCalledWith('comment.mentioned', { + splitId: 'split-1', + mentionedUsernames: ['john'], + actorId: 'u1', + commentId: 'c1', + }); + }); + + it('should list comments with pagination', async () => { + mockRepo.findAndCount.mockResolvedValue([[{ id: 'c1' }], 1]); + + const result = await service.listComments('split-1', 1, 10); + + expect(result.total).toBe(1); + expect(result.data.length).toBe(1); + }); + + it('should delete a comment if user is the owner', async () => { + mockRepo.findOne.mockResolvedValue({ id: 'c1', userId: 'u1' }); + mockRepo.remove.mockResolvedValue({}); + + const result = await service.deleteComment('c1', 'u1'); + + expect(result.message).toBe('Comment deleted successfully'); + }); + + it('should throw error if user tries to delete someone elses comment', async () => { + mockRepo.findOne.mockResolvedValue({ id: 'c1', userId: 'u1' }); + + await expect(service.deleteComment('c1', 'u2')).rejects.toThrow('You can only delete your own comments'); + }); +}