Compare commits
24 Commits
1d6e8f3866
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e18a3fa092 | |||
| 0bb06e0e2d | |||
| 1186463e11 | |||
| 34a4f594ce | |||
| e9505524bf | |||
| b33e04d9f4 | |||
| 21b9302d7f | |||
| 460560bb89 | |||
| ddf2ced098 | |||
| 5428ceb17b | |||
| 7df45f5c1c | |||
| a928acb7d2 | |||
| 4815157c3a | |||
| ee8efecdec | |||
| 95ce447496 | |||
| fa993f75b4 | |||
| c33debf153 | |||
| b2e1ab0361 | |||
| 1e642d194e | |||
| b8a2bc0aef | |||
| d64c647865 | |||
| 2bc35b639b | |||
| 14e013bac1 | |||
| 539c9cd104 |
@@ -7,7 +7,7 @@ env:
|
|||||||
DOCKER_REGISTRY_URL: git.harmonylab.ovh
|
DOCKER_REGISTRY_URL: git.harmonylab.ovh
|
||||||
DOCKER_REGISTRY_ORG: harmony
|
DOCKER_REGISTRY_ORG: harmony
|
||||||
RELEASE_VERSION: 1.2.0
|
RELEASE_VERSION: 1.2.0
|
||||||
DOCKPLOY_WEBHOOK_URL: http://192.168.0.220:3000/api/deploy/DxPrQ9JtATe40vH4N35KB
|
DOCKPLOY_WEBHOOK_URL: http://192.168.0.220:3000/api/deploy/HHWS7dR5rT-8vbXD6Zypv
|
||||||
DISCORD_WEBHOOK_URL: https://discord.com/api/webhooks/1433240422901088339/GAvL79ESBRabkB6rvxN2DsWI74KJ_Szgp9W2_PycPIY113rMCT_9LvIv-iTLCMD9W9qH
|
DISCORD_WEBHOOK_URL: https://discord.com/api/webhooks/1433240422901088339/GAvL79ESBRabkB6rvxN2DsWI74KJ_Szgp9W2_PycPIY113rMCT_9LvIv-iTLCMD9W9qH
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export default () => ({
|
|||||||
user: "admin@harmonychoral.com",
|
user: "admin@harmonychoral.com",
|
||||||
pass: "Apslxnap12bn23",
|
pass: "Apslxnap12bn23",
|
||||||
},
|
},
|
||||||
// ... any custom nodemailer options
|
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
defaultFrom: "admin@harmonychoral.com",
|
defaultFrom: "admin@harmonychoral.com",
|
||||||
@@ -17,31 +16,29 @@ export default () => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
upload: {
|
upload: {
|
||||||
config: {
|
config: {
|
||||||
provider: "aws-s3",
|
provider: "aws-s3",
|
||||||
providerOptions: {
|
providerOptions: {
|
||||||
//baseUrl: "http://192.168.0.211:9000/harmony",
|
|
||||||
baseUrl: "https://container.harmonylab.ovh/harmony",
|
baseUrl: "https://container.harmonylab.ovh/harmony",
|
||||||
s3Options: {
|
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: "admin",
|
accessKeyId: "admin",
|
||||||
secretAccessKey: "Apslxnap12bn23",
|
secretAccessKey: "Apslxnap12bn23",
|
||||||
},
|
},
|
||||||
//endpoint: "http://192.168.0.211:9000",
|
|
||||||
endpoint: "https://container.harmonylab.ovh",
|
endpoint: "https://container.harmonylab.ovh",
|
||||||
region: "eu-west-3",
|
|
||||||
forcePathStyle: true,
|
forcePathStyle: true,
|
||||||
|
region: "eu-west-3",
|
||||||
params: {
|
params: {
|
||||||
Bucket: "harmony",
|
Bucket: "harmony",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
"strapi-v5-plugin-populate-deep": {
|
"strapi-v5-plugin-populate-deep": {
|
||||||
config: {
|
config: {
|
||||||
defaultDepth: 3, // Default is 5
|
defaultDepth: 3,
|
||||||
skipCreatorFields: false,
|
skipCreatorFields: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
44
package-lock.json
generated
44
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "harmony-back",
|
"name": "harmony-back",
|
||||||
"version": "0.11.6",
|
"version": "0.12.16",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "harmony-back",
|
"name": "harmony-back",
|
||||||
"version": "0.11.6",
|
"version": "0.12.16",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strapi/plugin-cloud": "5.8.1",
|
"@strapi/plugin-cloud": "5.8.1",
|
||||||
"@strapi/plugin-documentation": "^5.12.6",
|
"@strapi/plugin-documentation": "^5.12.6",
|
||||||
@@ -373,6 +373,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.600.0.tgz",
|
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.600.0.tgz",
|
||||||
"integrity": "sha512-7+I8RWURGfzvChyNQSyj5/tKrqRbzRl7H+BnTOf/4Vsw1nFOi5ROhlhD4X/Y0QCTacxnaoNcIrqnY7uGGvVRzw==",
|
"integrity": "sha512-7+I8RWURGfzvChyNQSyj5/tKrqRbzRl7H+BnTOf/4Vsw1nFOi5ROhlhD4X/Y0QCTacxnaoNcIrqnY7uGGvVRzw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-crypto/sha256-browser": "5.2.0",
|
"@aws-crypto/sha256-browser": "5.2.0",
|
||||||
"@aws-crypto/sha256-js": "5.2.0",
|
"@aws-crypto/sha256-js": "5.2.0",
|
||||||
@@ -450,6 +451,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.600.0.tgz",
|
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.600.0.tgz",
|
||||||
"integrity": "sha512-KQG97B7LvTtTiGmjlrG1LRAY8wUvCQzrmZVV5bjrJ/1oXAU7DITYwVbSJeX9NWg6hDuSk0VE3MFwIXS2SvfLIA==",
|
"integrity": "sha512-KQG97B7LvTtTiGmjlrG1LRAY8wUvCQzrmZVV5bjrJ/1oXAU7DITYwVbSJeX9NWg6hDuSk0VE3MFwIXS2SvfLIA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-crypto/sha256-browser": "5.2.0",
|
"@aws-crypto/sha256-browser": "5.2.0",
|
||||||
"@aws-crypto/sha256-js": "5.2.0",
|
"@aws-crypto/sha256-js": "5.2.0",
|
||||||
@@ -2234,6 +2236,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.2.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.2.tgz",
|
||||||
"integrity": "sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==",
|
"integrity": "sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^6.5.0",
|
"@codemirror/state": "^6.5.0",
|
||||||
"style-mod": "^4.1.0",
|
"style-mod": "^4.1.0",
|
||||||
@@ -6051,6 +6054,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@strapi/admin/-/admin-5.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@strapi/admin/-/admin-5.8.1.tgz",
|
||||||
"integrity": "sha512-Rb6MMOhyjzYJktG4T8CheAIaMHwc5zqMBRz7oSlwgR7Y8VJ7YiN+1sn4fGRD5r4K0+7MLK2CbsYy3piu42GZdw==",
|
"integrity": "sha512-Rb6MMOhyjzYJktG4T8CheAIaMHwc5zqMBRz7oSlwgR7Y8VJ7YiN+1sn4fGRD5r4K0+7MLK2CbsYy3piu42GZdw==",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@casl/ability": "6.5.0",
|
"@casl/ability": "6.5.0",
|
||||||
"@internationalized/date": "3.5.4",
|
"@internationalized/date": "3.5.4",
|
||||||
@@ -6180,6 +6184,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@strapi/content-manager/-/content-manager-5.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@strapi/content-manager/-/content-manager-5.8.1.tgz",
|
||||||
"integrity": "sha512-aaJKJ3sLDerFvztEOs6sfcvAu7b3bDuDbj5Of0v4iEeGY0vWqFm2TKA6EeTSqKWsVDsM/BFmmxDVsgYqddqxiA==",
|
"integrity": "sha512-aaJKJ3sLDerFvztEOs6sfcvAu7b3bDuDbj5Of0v4iEeGY0vWqFm2TKA6EeTSqKWsVDsM/BFmmxDVsgYqddqxiA==",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-toolbar": "1.0.4",
|
"@radix-ui/react-toolbar": "1.0.4",
|
||||||
"@reduxjs/toolkit": "1.9.7",
|
"@reduxjs/toolkit": "1.9.7",
|
||||||
@@ -6415,6 +6420,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@strapi/data-transfer/-/data-transfer-5.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@strapi/data-transfer/-/data-transfer-5.8.1.tgz",
|
||||||
"integrity": "sha512-uIyYhoJOldzkAyeJi2tJ5JzWfTnHiVX79trLKZcZjyL357ipG3CUVhyoFOmy0xpNlBplq0v1RUGJ/GZBpNpkVA==",
|
"integrity": "sha512-uIyYhoJOldzkAyeJi2tJ5JzWfTnHiVX79trLKZcZjyL357ipG3CUVhyoFOmy0xpNlBplq0v1RUGJ/GZBpNpkVA==",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strapi/logger": "5.8.1",
|
"@strapi/logger": "5.8.1",
|
||||||
"@strapi/types": "5.8.1",
|
"@strapi/types": "5.8.1",
|
||||||
@@ -6580,6 +6586,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@strapi/icons/-/icons-2.0.0-rc.14.tgz",
|
"resolved": "https://registry.npmjs.org/@strapi/icons/-/icons-2.0.0-rc.14.tgz",
|
||||||
"integrity": "sha512-6kamFHcsoFpffp96HmFGgpM8FDezwb86sJ4c3Ydp7+eTgtEsHd8yLekAQhpaWXd2ZsHwRDbLLR0Tu1orgvRVYg==",
|
"integrity": "sha512-6kamFHcsoFpffp96HmFGgpM8FDezwb86sJ4c3Ydp7+eTgtEsHd8yLekAQhpaWXd2ZsHwRDbLLR0Tu1orgvRVYg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^17.0.0 || ^18.0.0",
|
"react": "^17.0.0 || ^18.0.0",
|
||||||
"react-dom": "^17.0.0 || ^18.0.0",
|
"react-dom": "^17.0.0 || ^18.0.0",
|
||||||
@@ -7109,6 +7116,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
|
||||||
"integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
|
"integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
@@ -7791,6 +7799,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@strapi/icons/-/icons-2.0.0-rc.23.tgz",
|
"resolved": "https://registry.npmjs.org/@strapi/icons/-/icons-2.0.0-rc.23.tgz",
|
||||||
"integrity": "sha512-sJ7iQ8kZ28z3mTkDm/gnsWIQljK3w0UaOk2irO77iSmbh+uR3W9gDF5CP/4Z+KDUqnjDke2kaOIPRI67etvi9A==",
|
"integrity": "sha512-sJ7iQ8kZ28z3mTkDm/gnsWIQljK3w0UaOk2irO77iSmbh+uR3W9gDF5CP/4Z+KDUqnjDke2kaOIPRI67etvi9A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^17.0.0 || ^18.0.0",
|
"react": "^17.0.0 || ^18.0.0",
|
||||||
"react-dom": "^17.0.0 || ^18.0.0",
|
"react-dom": "^17.0.0 || ^18.0.0",
|
||||||
@@ -8160,6 +8169,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@strapi/strapi/-/strapi-5.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@strapi/strapi/-/strapi-5.8.1.tgz",
|
||||||
"integrity": "sha512-i1l+CaLNjHdZ6o0GLwlK2eBRmQ/hQwC3Tl4aYK7IsRfbyeT0+1ScEtJF5WIn51ENmvnD+BCual6oz5D3T3g02A==",
|
"integrity": "sha512-i1l+CaLNjHdZ6o0GLwlK2eBRmQ/hQwC3Tl4aYK7IsRfbyeT0+1ScEtJF5WIn51ENmvnD+BCual6oz5D3T3g02A==",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "0.5.15",
|
"@pmmmwh/react-refresh-webpack-plugin": "0.5.15",
|
||||||
"@strapi/admin": "5.8.1",
|
"@strapi/admin": "5.8.1",
|
||||||
@@ -8648,6 +8658,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.1.0.tgz",
|
||||||
"integrity": "sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==",
|
"integrity": "sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.10.4",
|
"@babel/code-frame": "^7.10.4",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
@@ -9089,6 +9100,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
|
||||||
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
|
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@@ -9099,6 +9111,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
|
||||||
"integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
|
"integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^18.0.0"
|
"@types/react": "^18.0.0"
|
||||||
}
|
}
|
||||||
@@ -9507,6 +9520,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz",
|
||||||
"integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==",
|
"integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"json-schema-traverse": "^1.0.0",
|
"json-schema-traverse": "^1.0.0",
|
||||||
@@ -10155,6 +10169,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001688",
|
"caniuse-lite": "^1.0.30001688",
|
||||||
"electron-to-chromium": "^1.5.73",
|
"electron-to-chromium": "^1.5.73",
|
||||||
@@ -11613,6 +11628,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.21.0"
|
"@babel/runtime": "^7.21.0"
|
||||||
},
|
},
|
||||||
@@ -11961,7 +11977,8 @@
|
|||||||
"version": "0.0.1413902",
|
"version": "0.0.1413902",
|
||||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1413902.tgz",
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1413902.tgz",
|
||||||
"integrity": "sha512-yRtvFD8Oyk7C9Os3GmnFZLu53yAfsnyw1s+mLmHHUK0GQEc9zthHWvS1r67Zqzm5t7v56PILHIVZ7kmFMaL2yQ==",
|
"integrity": "sha512-yRtvFD8Oyk7C9Os3GmnFZLu53yAfsnyw1s+mLmHHUK0GQEc9zthHWvS1r67Zqzm5t7v56PILHIVZ7kmFMaL2yQ==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/dezalgo": {
|
"node_modules/dezalgo": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
@@ -12366,6 +12383,7 @@
|
|||||||
"integrity": "sha512-Kgq0/ZsAPzKrbOjCQcjoSmPoWhlcVnGAUo7jvaLHoxW1Drto0KGkR1xBNg2Cp43b9ImvxmPEJZ9xkfcnqPsfBw==",
|
"integrity": "sha512-Kgq0/ZsAPzKrbOjCQcjoSmPoWhlcVnGAUo7jvaLHoxW1Drto0KGkR1xBNg2Cp43b9ImvxmPEJZ9xkfcnqPsfBw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"esbuild": "bin/esbuild"
|
"esbuild": "bin/esbuild"
|
||||||
},
|
},
|
||||||
@@ -13381,6 +13399,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -15567,6 +15586,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/koa/-/koa-2.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/koa/-/koa-2.15.2.tgz",
|
||||||
"integrity": "sha512-MXTeZH3M6AJ8ukW2QZ8wqO3Dcdfh2WRRmjCBkEP+NhKNCiqlO5RDqHmSnsyNrbRJrdjyvIGSJho4vQiWgQJSVA==",
|
"integrity": "sha512-MXTeZH3M6AJ8ukW2QZ8wqO3Dcdfh2WRRmjCBkEP+NhKNCiqlO5RDqHmSnsyNrbRJrdjyvIGSJho4vQiWgQJSVA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "^1.3.5",
|
"accepts": "^1.3.5",
|
||||||
"cache-content-type": "^1.0.0",
|
"cache-content-type": "^1.0.0",
|
||||||
@@ -18830,6 +18850,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.8",
|
"nanoid": "^3.3.8",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -19531,6 +19552,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -19582,6 +19604,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -19729,6 +19752,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
|
||||||
"integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==",
|
"integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -19800,6 +19824,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.2.tgz",
|
||||||
"integrity": "sha512-O81EWqNJWqvlN/a7eTudAdQm0TbI7hw+WIi7OwwMcTn5JMyZ0ibTFNGz+t+Lju0df4LcqowCegcrK22lB1q9Kw==",
|
"integrity": "sha512-O81EWqNJWqvlN/a7eTudAdQm0TbI7hw+WIi7OwwMcTn5JMyZ0ibTFNGz+t+Lju0df4LcqowCegcrK22lB1q9Kw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-run/router": "1.21.1",
|
"@remix-run/router": "1.21.1",
|
||||||
"react-router": "6.28.2"
|
"react-router": "6.28.2"
|
||||||
@@ -20040,6 +20065,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.9.2"
|
"@babel/runtime": "^7.9.2"
|
||||||
}
|
}
|
||||||
@@ -20601,6 +20627,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
|
||||||
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
|
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
@@ -21023,6 +21050,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/slate/-/slate-0.94.1.tgz",
|
"resolved": "https://registry.npmjs.org/slate/-/slate-0.94.1.tgz",
|
||||||
"integrity": "sha512-GH/yizXr1ceBoZ9P9uebIaHe3dC/g6Plpf9nlUwnvoyf6V1UOYrRwkabtOCd3ZfIGxomY4P7lfgLr7FPH8/BKA==",
|
"integrity": "sha512-GH/yizXr1ceBoZ9P9uebIaHe3dC/g6Plpf9nlUwnvoyf6V1UOYrRwkabtOCd3ZfIGxomY4P7lfgLr7FPH8/BKA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"immer": "^9.0.6",
|
"immer": "^9.0.6",
|
||||||
"is-plain-object": "^5.0.0",
|
"is-plain-object": "^5.0.0",
|
||||||
@@ -21534,6 +21562,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.14.tgz",
|
||||||
"integrity": "sha512-KtfwhU5jw7UoxdM0g6XU9VZQFV4do+KrM8idiVCH5h4v49W+3p3yMe0icYwJgZQZepa5DbH04Qv8P0/RdcLcgg==",
|
"integrity": "sha512-KtfwhU5jw7UoxdM0g6XU9VZQFV4do+KrM8idiVCH5h4v49W+3p3yMe0icYwJgZQZepa5DbH04Qv8P0/RdcLcgg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/is-prop-valid": "1.2.2",
|
"@emotion/is-prop-valid": "1.2.2",
|
||||||
"@emotion/unitless": "0.8.1",
|
"@emotion/unitless": "0.8.1",
|
||||||
@@ -21771,6 +21800,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
|
||||||
"integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==",
|
"integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.8.2",
|
"acorn": "^8.8.2",
|
||||||
@@ -22050,6 +22080,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||||
"license": "(MIT OR CC0-1.0)",
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
@@ -22090,6 +22121,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.10.tgz",
|
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.10.tgz",
|
||||||
"integrity": "sha512-v10rtOFojrjW9og3T+6wAKeJaGMuojU87DXGZ33sfs+554wgPTRG+s07Ag1BjPZI85Y5QPVouPI63JQ6fcQM5w==",
|
"integrity": "sha512-v10rtOFojrjW9og3T+6wAKeJaGMuojU87DXGZ33sfs+554wgPTRG+s07Ag1BjPZI85Y5QPVouPI63JQ6fcQM5w==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lunr": "^2.3.9",
|
"lunr": "^2.3.9",
|
||||||
"marked": "^4.3.0",
|
"marked": "^4.3.0",
|
||||||
@@ -22121,6 +22153,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.17.1.tgz",
|
||||||
"integrity": "sha512-QzdU3fj0Kzw2XSdoL15ExLASt2WPqD7FbLeaqwT70+XjKyTshBnUlQA5nNREO1C2P8Uen0CDjsBLMsCQ+zd0lw==",
|
"integrity": "sha512-QzdU3fj0Kzw2XSdoL15ExLASt2WPqD7FbLeaqwT70+XjKyTshBnUlQA5nNREO1C2P8Uen0CDjsBLMsCQ+zd0lw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"handlebars": "^4.7.7"
|
"handlebars": "^4.7.7"
|
||||||
},
|
},
|
||||||
@@ -22133,6 +22166,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -22476,6 +22510,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.14.tgz",
|
||||||
"integrity": "sha512-TFQLuwWLPms+NBNlh0D9LZQ+HXW471COABxw/9TEUBrjuHMo9BrYBPrN/SYAwIuVL+rLerycxiLT41t4f5MZpA==",
|
"integrity": "sha512-TFQLuwWLPms+NBNlh0D9LZQ+HXW471COABxw/9TEUBrjuHMo9BrYBPrN/SYAwIuVL+rLerycxiLT41t4f5MZpA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.20.1",
|
"esbuild": "^0.20.1",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
@@ -22977,6 +23012,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz",
|
||||||
"integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==",
|
"integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint-scope": "^3.7.7",
|
"@types/eslint-scope": "^3.7.7",
|
||||||
"@types/estree": "^1.0.6",
|
"@types/estree": "^1.0.6",
|
||||||
@@ -23107,6 +23143,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.26.1.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.26.1.tgz",
|
||||||
"integrity": "sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A==",
|
"integrity": "sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-html-community": "0.0.8",
|
"ansi-html-community": "0.0.8",
|
||||||
"html-entities": "^2.1.0",
|
"html-entities": "^2.1.0",
|
||||||
@@ -23137,6 +23174,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "harmony-back",
|
"name": "harmony-back",
|
||||||
"version": "0.11.18",
|
"version": "0.12.19",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A Strapi application",
|
"description": "A Strapi application",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
166
public/confirmation.html
Normal file
166
public/confirmation.html
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<!-- Pré-en-tête (préheader) : aperçu dans certaines boîtes mail -->
|
||||||
|
<span style="display: none; max-height: 0; overflow: hidden"
|
||||||
|
>Confirme ton adresse e-mail en un clic pour activer ton compte.</span
|
||||||
|
>
|
||||||
|
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
width="100%"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
style="
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
padding: 24px 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
"Helvetica Neue", Arial;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
width="600"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
style="
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 6px 18px rgba(20, 30, 50, 0.08);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- En-tête -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 28px 32px 8px">
|
||||||
|
<div style="display: flex; align-items: center; gap: 12px">
|
||||||
|
<img
|
||||||
|
src="https://www.choralsync.com/logo_mini.png"
|
||||||
|
alt="Votre application"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
style="display: block; border-radius: 8px; border: 0"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h1 style="margin: 0; font-size: 18px; color: #0f1724">
|
||||||
|
ChoralSync
|
||||||
|
</h1>
|
||||||
|
<p style="margin: 2px 0 0; font-size: 12px; color: #6b7280">
|
||||||
|
Bienvenue — plus qu’une étape
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Corps -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 20px 32px 8px; color: #0f1724">
|
||||||
|
<h2 style="margin: 0 0 8px; font-size: 20px">
|
||||||
|
Bonjour {{USER_NAME}},
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #374151;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Merci de t’être inscrit. Clique sur le bouton ci-dessous pour
|
||||||
|
confirmer ton adresse e-mail et activer ton compte.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Bouton -->
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
style="margin: 20px 0"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a
|
||||||
|
href="{{CONFIRM_URL}}"
|
||||||
|
style="
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 22px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
#6366f1,
|
||||||
|
#06b6d4
|
||||||
|
);
|
||||||
|
color: #ffffff;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Confirmer mon e-mail
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Lien de secours -->
|
||||||
|
<p style="margin: 8px 0 0; font-size: 13px; color: #6b7280">
|
||||||
|
Si le bouton ne fonctionne pas, copie-colle ce lien dans ton
|
||||||
|
navigateur :
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #0f1724;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e6e9ef;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="{{CONFIRM_URL}}"
|
||||||
|
style="color: #0369a1; text-decoration: underline"
|
||||||
|
>{{CONFIRM_URL}}</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Pied de mail -->
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style="
|
||||||
|
padding: 18px 32px 28px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 13px;
|
||||||
|
border-top: 1px solid #f1f3f6;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<p style="margin: 0 0 6px">
|
||||||
|
Si tu n’es pas à l’origine de cette inscription, ignore cet
|
||||||
|
e-mail.
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0; font-size: 12px; color: #9ca3af">
|
||||||
|
© ChoralSync {{YEAR}} •
|
||||||
|
<a
|
||||||
|
href="https://www.choralsync.com"
|
||||||
|
style="color: #9ca3af; text-decoration: underline"
|
||||||
|
>www.choralsync.com</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div style="height: 14px"></div>
|
||||||
|
<p style="font-size: 12px; color: #9ca3af; margin: 0">
|
||||||
|
Besoin d’aide ? Réponds directement à cet e-mail ou consulte notre
|
||||||
|
<a href="https://www.choralsync.com/help" style="color: #0369a1"
|
||||||
|
>centre d’aide</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
126
report.md
Normal file
126
report.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Implementation Report: Chat Messaging Feature
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Successfully implemented the core chat messaging functionality with support for direct messages (1:1) and group conversations (2+). The implementation includes conversation creation with automatic member management, and message creation with permission-based access control.
|
||||||
|
|
||||||
|
## What Was Implemented
|
||||||
|
|
||||||
|
### 1. Chat Conversation Service (`src/api/chat-conversation/services/chat-conversation.ts`)
|
||||||
|
- **`createConversationWithMembers(userIds, creatorId, title?)`** service method
|
||||||
|
- Validates all recipient user IDs exist in the database
|
||||||
|
- Automatically includes creator in conversation members
|
||||||
|
- Deduplicates user list to prevent duplicates
|
||||||
|
- Determines conversation type: `isGroup = totalUsers > 2`
|
||||||
|
- Sets title only for group conversations
|
||||||
|
- Creates ChatConversationMember records with appropriate roles:
|
||||||
|
- `owner` role for conversation creator
|
||||||
|
- `member` role for other participants
|
||||||
|
- Captures `joinedAt` timestamp for each member
|
||||||
|
|
||||||
|
### 2. Chat Conversation Controller (`src/api/chat-conversation/controllers/chat-conversation.ts`)
|
||||||
|
- **`createConversation(ctx)` controller method**
|
||||||
|
- Authenticates user from `ctx.state.user.id`
|
||||||
|
- Validates `userIds` is non-empty array
|
||||||
|
- Calls service method to create conversation with members
|
||||||
|
- Returns created conversation with populated members
|
||||||
|
- Proper error handling with user-friendly messages
|
||||||
|
|
||||||
|
### 3. Chat Message Service (`src/api/chat-message/services/chat-message.ts`)
|
||||||
|
- **`createMessageInConversation(conversationId, senderId, content)` service method**
|
||||||
|
- Validates all required parameters
|
||||||
|
- Verifies conversation exists
|
||||||
|
- **Permission check**: Verifies sender is a member of the conversation
|
||||||
|
- Validates message content is non-empty
|
||||||
|
- Creates ChatMessage with sender and content
|
||||||
|
- Links message to conversation via `messages` relation
|
||||||
|
- Returns created message
|
||||||
|
|
||||||
|
### 4. Chat Message Controller (`src/api/chat-message/controllers/chat-message.ts`)
|
||||||
|
- **`createMessage(ctx)` controller method**
|
||||||
|
- Authenticates user from `ctx.state.user.id`
|
||||||
|
- Validates message content is provided
|
||||||
|
- Workflow supports two scenarios:
|
||||||
|
1. **Existing conversation**: If `conversationId` provided, creates message in that conversation
|
||||||
|
2. **New conversation**: If `conversationId` is null, creates new conversation with `recipientIds` first, then creates message
|
||||||
|
- Leverages service layer for all business logic
|
||||||
|
- Proper error handling with detailed messages
|
||||||
|
|
||||||
|
## Architecture Decisions
|
||||||
|
|
||||||
|
1. **Service-based approach**: All business logic encapsulated in service methods for reusability and testability
|
||||||
|
2. **Permission checks**: Message creation validates user membership in conversation
|
||||||
|
3. **Member creation**: Automatic ChatConversationMember records ensure consistent data relationships
|
||||||
|
4. **Group detection**: Automatic based on user count (>2 = group, ≤2 = direct message)
|
||||||
|
5. **Unidirectional relation**: ChatMessage has no back-relation to ChatConversation (by design, avoids TypeScript type issues on client)
|
||||||
|
|
||||||
|
## Workflow Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
createMessage(conversationId, content, recipientIds)
|
||||||
|
├─ If conversationId is null:
|
||||||
|
│ └─ createConversation(recipientIds)
|
||||||
|
│ ├─ Validate all user IDs exist
|
||||||
|
│ ├─ Determine isGroup (>2 users)
|
||||||
|
│ └─ Create ChatConversation + ChatConversationMembers
|
||||||
|
│
|
||||||
|
└─ createMessageInConversation(conversationId, userId, content)
|
||||||
|
├─ Verify conversation exists
|
||||||
|
├─ Verify user is member (permission check)
|
||||||
|
├─ Create ChatMessage
|
||||||
|
└─ Link to conversation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
✅ **Build**: `npm run build` completed successfully with no TypeScript errors
|
||||||
|
- Compilation time: 2.884s
|
||||||
|
- Total build time: ~14s
|
||||||
|
|
||||||
|
✅ **Type Safety**: Full TypeScript compilation with no errors
|
||||||
|
|
||||||
|
✅ **Edge Cases Handled**:
|
||||||
|
- Non-existent users validation
|
||||||
|
- Permission checks for message creation
|
||||||
|
- Empty message content validation
|
||||||
|
- Non-existent conversation validation
|
||||||
|
- Duplicate user removal in conversation creation
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
1. `/src/api/chat-conversation/services/chat-conversation.ts` — Added `createConversationWithMembers()`
|
||||||
|
2. `/src/api/chat-conversation/controllers/chat-conversation.ts` — Added `createConversation()` endpoint
|
||||||
|
3. `/src/api/chat-message/services/chat-message.ts` — Added `createMessageInConversation()`
|
||||||
|
4. `/src/api/chat-message/controllers/chat-message.ts` — Added `createMessage()` endpoint
|
||||||
|
|
||||||
|
## Testing Recommendations
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- Conversation creation with various user counts
|
||||||
|
- Message creation in existing conversations
|
||||||
|
- Permission validation for non-members
|
||||||
|
- User existence validation
|
||||||
|
- Content validation (empty/whitespace)
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- End-to-end message creation flow with conversation creation
|
||||||
|
- ChatConversationMember creation and role assignment
|
||||||
|
- Conversation isGroup flag accuracy
|
||||||
|
|
||||||
|
### API Tests
|
||||||
|
- POST `/api/chat-conversations/createConversation` with valid/invalid payloads
|
||||||
|
- POST `/api/chat-messages/createMessage` with conversation ID
|
||||||
|
- POST `/api/chat-messages/createMessage` with recipient IDs (new conversation)
|
||||||
|
|
||||||
|
## Known Limitations & Future Enhancements
|
||||||
|
|
||||||
|
1. **No bulk operations**: Currently single message/conversation per request
|
||||||
|
2. **No message updates/deletion**: Only creation implemented
|
||||||
|
3. **No read receipts**: `lastReadAt` field exists but not updated
|
||||||
|
4. **No pagination**: Conversation and message listing not implemented
|
||||||
|
5. **No media support**: Messages are text-only
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The messaging feature foundation is now complete with core conversation and message creation functionality. The implementation follows Strapi patterns, includes proper validation and permission checks, and is fully type-safe with TypeScript.
|
||||||
|
|
||||||
173
spec.md
Normal file
173
spec.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Technical Specification: Chat Messaging Feature
|
||||||
|
|
||||||
|
## Task Complexity: **Medium**
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
- Involves creating relationships between 3 entities (ChatMessage, ChatConversation, ChatConversationMember)
|
||||||
|
- Handles two conversation types with different logic (direct vs group)
|
||||||
|
- Requires user authentication and transactional data creation
|
||||||
|
- Some edge cases (creating conversation before message, handling members, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
- **Language**: TypeScript
|
||||||
|
- **Framework**: Strapi 4
|
||||||
|
- **Database**: Relational DB (presumed based on Strapi structure)
|
||||||
|
- **Existing patterns**: Core controller pattern with custom methods (see `post.ts`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
### Completed
|
||||||
|
- Schema definitions for `ChatMessage`, `ChatConversation`, `ChatConversationMember`
|
||||||
|
- Service layer structure in place (core service factories)
|
||||||
|
- Basic controller scaffolding
|
||||||
|
|
||||||
|
### Missing
|
||||||
|
- Custom controller methods for creating messages and conversations
|
||||||
|
- Business logic for handling conversation creation workflow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model Overview
|
||||||
|
|
||||||
|
### ChatConversation
|
||||||
|
- `title`: Optional conversation name
|
||||||
|
- `isGroup`: Boolean (true if 3+ users, false if 2 users)
|
||||||
|
- `creator`: Relation to user
|
||||||
|
- `messages`: OneToMany relation to ChatMessage
|
||||||
|
- `users`: OneToMany relation to user
|
||||||
|
|
||||||
|
### ChatConversationMember
|
||||||
|
- `user`: OneToOne relation to user
|
||||||
|
- `conversation`: OneToOne relation to ChatConversation
|
||||||
|
- `role`: Enum (member, admin, owner)
|
||||||
|
- `joinedAt`: DateTime
|
||||||
|
- `lastReadAt`: DateTime
|
||||||
|
|
||||||
|
### ChatMessage
|
||||||
|
- `sender`: OneToOne relation to user
|
||||||
|
- `content`: String (required)
|
||||||
|
- `isEdited`: Boolean (default: false)
|
||||||
|
- `deletedAt`: DateTime
|
||||||
|
- **Note**: No back-relation to ChatConversation (by design)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Approach
|
||||||
|
|
||||||
|
### 1. Controller Methods
|
||||||
|
|
||||||
|
#### `createMessage(ctx)`
|
||||||
|
- **Purpose**: Create a message in a conversation
|
||||||
|
- **Parameters**:
|
||||||
|
- `conversationId` (query/body): ID of target conversation (null if creating new conversation)
|
||||||
|
- `content` (body): Message content (required)
|
||||||
|
- `recipientIds` (body, optional): User IDs for new conversation
|
||||||
|
- **Logic**:
|
||||||
|
- Get authenticated user from `ctx.state.user.id`
|
||||||
|
- If `conversationId` is null/undefined:
|
||||||
|
- Call `createConversation()` with `recipientIds` and authenticated user
|
||||||
|
- Use returned conversation ID
|
||||||
|
- Create ChatMessage with:
|
||||||
|
- `sender`: authenticated user ID
|
||||||
|
- `content`: message content
|
||||||
|
- Add message to ChatConversation.messages relation
|
||||||
|
- Return created message with populated relations
|
||||||
|
|
||||||
|
#### `createConversation(ctx)`
|
||||||
|
- **Purpose**: Create a new conversation with one or more users
|
||||||
|
- **Parameters**:
|
||||||
|
- `userIds` (body): Array of user IDs to add to conversation
|
||||||
|
- `title` (body, optional): Conversation title (for groups)
|
||||||
|
- **Logic**:
|
||||||
|
- Get authenticated user from `ctx.state.user.id`
|
||||||
|
- Merge authenticated user into conversation members
|
||||||
|
- Remove duplicates from user list
|
||||||
|
- Determine if group: `isGroup = totalUsers > 2`
|
||||||
|
- Create ChatConversation:
|
||||||
|
- `isGroup`: boolean
|
||||||
|
- `creator`: authenticated user ID
|
||||||
|
- `title`: title (optional, for groups)
|
||||||
|
- Create ChatConversationMember records for each user:
|
||||||
|
- `user`: user ID
|
||||||
|
- `conversation`: created conversation ID
|
||||||
|
- `role`: "owner" for creator, "member" for others
|
||||||
|
- `joinedAt`: current timestamp
|
||||||
|
- Return created conversation
|
||||||
|
|
||||||
|
### 2. Integration Points
|
||||||
|
|
||||||
|
- **Authentication**: Use `ctx.state.user?.id` (follows existing pattern in `post.ts`)
|
||||||
|
- **Database queries**: Use `strapi.db.query()` pattern (established in codebase)
|
||||||
|
- **Error handling**: Follow Strapi context methods (`ctx.badRequest()`, `ctx.unauthorized()`)
|
||||||
|
|
||||||
|
### 3. Files to Modify
|
||||||
|
|
||||||
|
- `/Users/julien/projets/dev/harmony/harmony-back/src/api/chat-message/controllers/chat-message.ts` — Add `createMessage()` and helper logic
|
||||||
|
- `/Users/julien/projets/dev/harmony/harmony-back/src/api/chat-conversation/controllers/chat-conversation.ts` — Add `createConversation()`
|
||||||
|
|
||||||
|
### 4. No Schema Changes Required
|
||||||
|
- ChatMessage schema is intentionally unidirectional (no back-relation)
|
||||||
|
- All necessary relations already defined in ChatConversation and ChatConversationMember
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Approach
|
||||||
|
|
||||||
|
1. **Type checking**: Run Strapi build or `npm run build`
|
||||||
|
2. **Linting**: Run project linting command (TBD - will check package.json)
|
||||||
|
3. **Manual testing**:
|
||||||
|
- Create message in existing conversation
|
||||||
|
- Create message with null conversationId (should create conversation first)
|
||||||
|
- Create conversation with 2 users (direct message)
|
||||||
|
- Create conversation with 3+ users (group chat)
|
||||||
|
- Verify `isGroup` flag is set correctly
|
||||||
|
- Verify ChatConversationMember records are created with correct roles
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Decisions (Confirmed)
|
||||||
|
|
||||||
|
1. **User validation**: ✅ Validate recipient user IDs exist before creating conversation
|
||||||
|
2. **API routing**: ✅ `createMessage` belongs to chat-message controller
|
||||||
|
3. **Permission checks**: ✅ Verify user is member of conversation before sending message
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Implementation Plan
|
||||||
|
|
||||||
|
### Task 1: Implement `createConversation` in chat-conversation controller
|
||||||
|
- Validate all recipient user IDs exist in database
|
||||||
|
- Add authenticated user to members list
|
||||||
|
- Determine `isGroup` based on total user count (>2 = group)
|
||||||
|
- Create ChatConversation record with creator and title
|
||||||
|
- Create ChatConversationMember records for all users with appropriate roles
|
||||||
|
- Return created conversation with populated members
|
||||||
|
|
||||||
|
### Task 2: Implement `createMessage` in chat-message controller
|
||||||
|
- Validate authenticated user exists
|
||||||
|
- If conversationId is null: call service method to create conversation with recipientIds
|
||||||
|
- If conversationId is provided:
|
||||||
|
- Verify conversation exists
|
||||||
|
- Verify user is member of conversation (permission check)
|
||||||
|
- Validate message content (required, non-empty)
|
||||||
|
- Create ChatMessage with sender and content
|
||||||
|
- Add message to conversation.messages relation
|
||||||
|
- Return created message with populated sender relation
|
||||||
|
|
||||||
|
### Task 3: Add helper service methods in chat-conversation service
|
||||||
|
- `createConversationWithMembers()`: Centralized logic for creating conversation + members
|
||||||
|
- Used by both direct createConversation controller and createMessage
|
||||||
|
|
||||||
|
### Task 4: Add helper service methods in chat-message service
|
||||||
|
- `createMessageInConversation()`: Centralized logic for creating and linking message
|
||||||
|
|
||||||
|
### Task 5: Verification
|
||||||
|
- Build and type-check the project
|
||||||
|
- Run linting
|
||||||
|
- Verify error handling for edge cases
|
||||||
|
|
||||||
@@ -42,11 +42,6 @@
|
|||||||
"applies": {
|
"applies": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"location": {
|
|
||||||
"type": "component",
|
|
||||||
"repeatable": false,
|
|
||||||
"component": "address.full-address"
|
|
||||||
},
|
|
||||||
"contactname": {
|
"contactname": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -58,6 +53,19 @@
|
|||||||
},
|
},
|
||||||
"featured": {
|
"featured": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"medias": {
|
||||||
|
"allowedTypes": [
|
||||||
|
"images",
|
||||||
|
"files",
|
||||||
|
"videos",
|
||||||
|
"audios"
|
||||||
|
],
|
||||||
|
"type": "media",
|
||||||
|
"multiple": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,47 @@
|
|||||||
* ad controller
|
* ad controller
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { factories } from '@strapi/strapi'
|
import { factories } from "@strapi/strapi";
|
||||||
|
|
||||||
export default factories.createCoreController('api::ad.ad');
|
export default factories.createCoreController("api::ad.ad", ({ strapi }) => ({
|
||||||
|
async create(ctx) {
|
||||||
|
// 1) Prépare le payload + upload banner
|
||||||
|
const body = ctx.request.body as any;
|
||||||
|
const data =
|
||||||
|
typeof body?.data === "string" ? JSON.parse(body.data) : body?.data || {};
|
||||||
|
data.author = ctx.state.user.id;
|
||||||
|
data.medias = [];
|
||||||
|
|
||||||
|
const bannerInput = (ctx.request.files as any)?.medias;
|
||||||
|
if (bannerInput) {
|
||||||
|
const files = Array.isArray(bannerInput) ? bannerInput : [bannerInput];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const uploaded = await strapi
|
||||||
|
.plugin("upload")
|
||||||
|
.service("upload")
|
||||||
|
.upload({
|
||||||
|
data: {
|
||||||
|
fileInfo: {
|
||||||
|
alternativeText: data?.name || "media",
|
||||||
|
caption: "media",
|
||||||
|
name: file.originalFilename || "media",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
files: file,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uploaded?.[0]?.id) {
|
||||||
|
data.medias.push(uploaded[0].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//delete data.medias;
|
||||||
|
|
||||||
|
// 2) Crée le group via core controller
|
||||||
|
ctx.request.body = { data };
|
||||||
|
const response = await super.create(ctx); // { data: { id, attributes }, meta: {} }
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"kind": "collectionType",
|
||||||
|
"collectionName": "chat_conversation_members",
|
||||||
|
"info": {
|
||||||
|
"singularName": "chat-conversation-member",
|
||||||
|
"pluralName": "chat-conversation-members",
|
||||||
|
"displayName": "ChatConversationMember"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": false
|
||||||
|
},
|
||||||
|
"pluginOptions": {},
|
||||||
|
"attributes": {
|
||||||
|
"user": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "oneToOne",
|
||||||
|
"target": "plugin::users-permissions.user"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "enumeration",
|
||||||
|
"enum": [
|
||||||
|
"member",
|
||||||
|
"admin",
|
||||||
|
"owner"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"joinedAt": {
|
||||||
|
"type": "datetime"
|
||||||
|
},
|
||||||
|
"lastReadAt": {
|
||||||
|
"type": "datetime"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "oneToOne",
|
||||||
|
"target": "api::chat-conversation.chat-conversation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* chat-conversation-member controller
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi'
|
||||||
|
|
||||||
|
export default factories.createCoreController('api::chat-conversation-member.chat-conversation-member');
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* chat-conversation-member router
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreRouter('api::chat-conversation-member.chat-conversation-member');
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* chat-conversation-member service
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreService('api::chat-conversation-member.chat-conversation-member');
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"kind": "collectionType",
|
||||||
|
"collectionName": "chat_conversations",
|
||||||
|
"info": {
|
||||||
|
"singularName": "chat-conversation",
|
||||||
|
"pluralName": "chat-conversations",
|
||||||
|
"displayName": "ChatConversation",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": false
|
||||||
|
},
|
||||||
|
"pluginOptions": {},
|
||||||
|
"attributes": {
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isGroup": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"creator": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "oneToOne",
|
||||||
|
"target": "plugin::users-permissions.user"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "oneToMany",
|
||||||
|
"target": "api::chat-message.chat-message"
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "oneToMany",
|
||||||
|
"target": "plugin::users-permissions.user"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/api/chat-conversation/controllers/chat-conversation.ts
Normal file
37
src/api/chat-conversation/controllers/chat-conversation.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* chat-conversation controller
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from "@strapi/strapi";
|
||||||
|
|
||||||
|
export default factories.createCoreController(
|
||||||
|
"api::chat-conversation.chat-conversation",
|
||||||
|
({ strapi }) => ({
|
||||||
|
async create(ctx) {
|
||||||
|
const userId = ctx.state.user?.id;
|
||||||
|
if (!userId) {
|
||||||
|
return ctx.unauthorized(
|
||||||
|
"You must be logged in to create a conversation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { users, title, isGroup } = ctx.request.body.data;
|
||||||
|
|
||||||
|
if (!users || !Array.isArray(users) || users.length === 0) {
|
||||||
|
return ctx.badRequest("userIds must be a non-empty array");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conversation = await strapi
|
||||||
|
.service("api::chat-conversation.chat-conversation")
|
||||||
|
.createConversationWithMembers(users, userId, title);
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
data: conversation,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return ctx.badRequest(error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
7
src/api/chat-conversation/routes/chat-conversation.ts
Normal file
7
src/api/chat-conversation/routes/chat-conversation.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* chat-conversation router
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreRouter('api::chat-conversation.chat-conversation');
|
||||||
67
src/api/chat-conversation/services/chat-conversation.ts
Normal file
67
src/api/chat-conversation/services/chat-conversation.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* chat-conversation service
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreService(
|
||||||
|
'api::chat-conversation.chat-conversation',
|
||||||
|
({ strapi }) => ({
|
||||||
|
async createConversationWithMembers(
|
||||||
|
userIds: number[],
|
||||||
|
creatorId: number,
|
||||||
|
title?: string
|
||||||
|
) {
|
||||||
|
if (!userIds || userIds.length === 0) {
|
||||||
|
throw new Error('At least one user ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!creatorId) {
|
||||||
|
throw new Error('Creator ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueUserIds = [...new Set([...userIds, creatorId])];
|
||||||
|
|
||||||
|
for (const userId of uniqueUserIds) {
|
||||||
|
const userExists = await strapi.db
|
||||||
|
.query('plugin::users-permissions.user')
|
||||||
|
.findOne({
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userExists) {
|
||||||
|
throw new Error(`User with ID ${userId} not found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isGroup = uniqueUserIds.length > 2;
|
||||||
|
|
||||||
|
const conversation = await strapi.db
|
||||||
|
.query('api::chat-conversation.chat-conversation')
|
||||||
|
.create({
|
||||||
|
data: {
|
||||||
|
title: isGroup ? title : null,
|
||||||
|
isGroup,
|
||||||
|
creator: creatorId,
|
||||||
|
users: uniqueUserIds.map((id) => ({ id })),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const userId of uniqueUserIds) {
|
||||||
|
const role = userId === creatorId ? 'owner' : 'member';
|
||||||
|
await strapi.db
|
||||||
|
.query('api::chat-conversation-member.chat-conversation-member')
|
||||||
|
.create({
|
||||||
|
data: {
|
||||||
|
user: userId,
|
||||||
|
conversation: conversation.id,
|
||||||
|
role,
|
||||||
|
joinedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return conversation;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
42
src/api/chat-message/content-types/chat-message/schema.json
Normal file
42
src/api/chat-message/content-types/chat-message/schema.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"kind": "collectionType",
|
||||||
|
"collectionName": "chat_messages",
|
||||||
|
"info": {
|
||||||
|
"singularName": "chat-message",
|
||||||
|
"pluralName": "chat-messages",
|
||||||
|
"displayName": "ChatMessage",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": false
|
||||||
|
},
|
||||||
|
"pluginOptions": {},
|
||||||
|
"attributes": {
|
||||||
|
"sender": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "oneToOne",
|
||||||
|
"target": "plugin::users-permissions.user"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"isEdited": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"deletedAt": {
|
||||||
|
"type": "datetime"
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"allowedTypes": [
|
||||||
|
"images",
|
||||||
|
"files",
|
||||||
|
"videos",
|
||||||
|
"audios"
|
||||||
|
],
|
||||||
|
"type": "media",
|
||||||
|
"multiple": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
152
src/api/chat-message/controllers/chat-message.ts
Normal file
152
src/api/chat-message/controllers/chat-message.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* chat-message controller
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from "@strapi/strapi";
|
||||||
|
|
||||||
|
export default factories.createCoreController(
|
||||||
|
"api::chat-message.chat-message",
|
||||||
|
({ strapi }) => ({
|
||||||
|
async create(ctx) {
|
||||||
|
const userId = ctx.state.user?.id;
|
||||||
|
if (!userId) {
|
||||||
|
return ctx.unauthorized("You must be logged in to send a message");
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = ctx.request.body as any;
|
||||||
|
const data =
|
||||||
|
typeof body?.data === "string"
|
||||||
|
? JSON.parse(body.data)
|
||||||
|
: body?.data || {};
|
||||||
|
|
||||||
|
const { conversation, content, recipientIds } = data;
|
||||||
|
|
||||||
|
if (!content || content.trim() === "") {
|
||||||
|
return ctx.badRequest("Message content is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalConversationId = conversation;
|
||||||
|
|
||||||
|
if (!finalConversationId) {
|
||||||
|
if (
|
||||||
|
!recipientIds ||
|
||||||
|
!Array.isArray(recipientIds) ||
|
||||||
|
recipientIds.length === 0
|
||||||
|
) {
|
||||||
|
return ctx.badRequest(
|
||||||
|
"Either conversationId or recipientIds must be provided"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newConversation = await strapi
|
||||||
|
.service("api::chat-conversation.chat-conversation")
|
||||||
|
.createConversationWithMembers(recipientIds, userId);
|
||||||
|
finalConversationId = newConversation.id;
|
||||||
|
} catch (error: any) {
|
||||||
|
return ctx.badRequest(
|
||||||
|
`Failed to create conversation: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let mediaId: number | undefined;
|
||||||
|
const mediaInput = (ctx.request.files as any)?.media;
|
||||||
|
if (mediaInput) {
|
||||||
|
const file = Array.isArray(mediaInput) ? mediaInput[0] : mediaInput;
|
||||||
|
const uploaded = await strapi
|
||||||
|
.plugin("upload")
|
||||||
|
.service("upload")
|
||||||
|
.upload({
|
||||||
|
data: {
|
||||||
|
fileInfo: {
|
||||||
|
alternativeText: data?.name || "media",
|
||||||
|
caption: "media",
|
||||||
|
name: file.originalFilename || "media",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
files: file,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uploaded?.[0]?.id) {
|
||||||
|
mediaId = uploaded[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const message = await strapi
|
||||||
|
.service("api::chat-message.chat-message")
|
||||||
|
.createMessageInConversation(
|
||||||
|
finalConversationId,
|
||||||
|
userId,
|
||||||
|
content,
|
||||||
|
mediaId
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentUser = await strapi.entityService.findOne(
|
||||||
|
"plugin::users-permissions.user",
|
||||||
|
userId,
|
||||||
|
{ populate: { avatar: true } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const conversation = await strapi.entityService.findOne(
|
||||||
|
"api::chat-conversation.chat-conversation",
|
||||||
|
finalConversationId,
|
||||||
|
{ populate: { users: true } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const conversationMembers = (conversation as any)?.users || [];
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
for (const member of conversationMembers) {
|
||||||
|
if (member.id === userId) continue;
|
||||||
|
|
||||||
|
const existingNotifications = await strapi.entityService.findMany(
|
||||||
|
"api::notification.notification",
|
||||||
|
{
|
||||||
|
filters: {
|
||||||
|
$and: [
|
||||||
|
{ type: "messageSent" },
|
||||||
|
{ target_user: member.id },
|
||||||
|
{ floodId: finalConversationId },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
sort: { createdAt: "desc" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasNotificationToday = existingNotifications?.some(
|
||||||
|
(notification: any) => {
|
||||||
|
const notificationDate = new Date(notification.createdAt);
|
||||||
|
notificationDate.setHours(0, 0, 0, 0);
|
||||||
|
return notificationDate.getTime() === today.getTime();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasNotificationToday) {
|
||||||
|
await strapi
|
||||||
|
.service("api::notification.notification")
|
||||||
|
?.createNotification({
|
||||||
|
message: `Tu as reçu un message de ${currentUser?.name}`,
|
||||||
|
type: "messageSent",
|
||||||
|
target_user: member.id,
|
||||||
|
source: "user",
|
||||||
|
floodId: finalConversationId,
|
||||||
|
payload: {
|
||||||
|
avatar: (currentUser as any)?.avatar?.url,
|
||||||
|
url: `/app/chat/${finalConversationId}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
data: message,
|
||||||
|
conversationId: finalConversationId,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return ctx.badRequest(`Failed to create message: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
7
src/api/chat-message/routes/chat-message.ts
Normal file
7
src/api/chat-message/routes/chat-message.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* chat-message router
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreRouter('api::chat-message.chat-message');
|
||||||
95
src/api/chat-message/services/chat-message.ts
Normal file
95
src/api/chat-message/services/chat-message.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* chat-message service
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreService(
|
||||||
|
'api::chat-message.chat-message',
|
||||||
|
({ strapi }) => ({
|
||||||
|
async createMessageInConversation(
|
||||||
|
conversationId: number,
|
||||||
|
senderId: number,
|
||||||
|
content: string,
|
||||||
|
mediaId?: number
|
||||||
|
) {
|
||||||
|
if (!conversationId) {
|
||||||
|
throw new Error('Conversation ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!senderId) {
|
||||||
|
throw new Error('Sender ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content || content.trim() === '') {
|
||||||
|
throw new Error('Message content is required and cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversation = await strapi.db
|
||||||
|
.query('api::chat-conversation.chat-conversation')
|
||||||
|
.findOne({
|
||||||
|
where: { id: conversationId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error(`Conversation with ID ${conversationId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = await strapi.db
|
||||||
|
.query('api::chat-conversation-member.chat-conversation-member')
|
||||||
|
.findOne({
|
||||||
|
where: {
|
||||||
|
user: { id: senderId },
|
||||||
|
conversation: { id: conversationId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
throw new Error(
|
||||||
|
'User is not a member of this conversation'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaId) {
|
||||||
|
const media = await strapi.db
|
||||||
|
.query('plugin::upload.file')
|
||||||
|
.findOne({
|
||||||
|
where: { id: mediaId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!media) {
|
||||||
|
throw new Error(`Media with ID ${mediaId} not found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageData: any = {
|
||||||
|
sender: senderId,
|
||||||
|
content: content.trim(),
|
||||||
|
isEdited: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mediaId) {
|
||||||
|
messageData.media = mediaId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await strapi.db
|
||||||
|
.query('api::chat-message.chat-message')
|
||||||
|
.create({
|
||||||
|
data: messageData,
|
||||||
|
});
|
||||||
|
|
||||||
|
await strapi.db
|
||||||
|
.query('api::chat-conversation.chat-conversation')
|
||||||
|
.update({
|
||||||
|
where: { id: conversationId },
|
||||||
|
data: {
|
||||||
|
messages: {
|
||||||
|
connect: [{ id: message.id }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
57
src/api/contact/content-types/contact/lifecycles.ts
Normal file
57
src/api/contact/content-types/contact/lifecycles.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// src/api/contact/content-types/contact/lifecycles.js
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* Se déclenche après qu'un contact a été créé.
|
||||||
|
* @param {Object} result - L'entrée de contact qui vient d'être créée.
|
||||||
|
*/
|
||||||
|
async afterCreate(result) {
|
||||||
|
// On vérifie si l'état du contact est bien 'pending'
|
||||||
|
if (result.result.state === "pending") {
|
||||||
|
const populatedContact = await strapi
|
||||||
|
.documents("api::contact.contact")
|
||||||
|
.findOne({
|
||||||
|
documentId: result.result.documentId, // On utilise documentId au lieu de id
|
||||||
|
populate: {
|
||||||
|
owner: true, // On peuple la relation 'owner'
|
||||||
|
user: true, // On peuple la relation 'user'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { user, owner } = populatedContact;
|
||||||
|
|
||||||
|
// On s'assure que le receiver et le sender existent bien
|
||||||
|
if (user && owner) {
|
||||||
|
try {
|
||||||
|
await strapi.service("api::notification.notification")?.addActivity({
|
||||||
|
userId: owner.id,
|
||||||
|
activityMessage: `${owner.username} a invité ${user.username} à être ami avec lui.`,
|
||||||
|
activityUser: user.username,
|
||||||
|
activityType: "user",
|
||||||
|
});
|
||||||
|
// Appel du service de notification
|
||||||
|
await strapi
|
||||||
|
.service("api::notification.notification")
|
||||||
|
?.createNotification({
|
||||||
|
title: "Demande d'ami",
|
||||||
|
message: `Tu as été invité par ${owner.username} à être son ami.`,
|
||||||
|
type: "success",
|
||||||
|
target_user: user.id,
|
||||||
|
source: "system",
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
strapi.log.info(
|
||||||
|
`Notification sent to user ${user.id} for new contact invitation from ${owner.id}.`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// C'est une bonne pratique d'attraper les erreurs pour ne pas
|
||||||
|
// bloquer la création du contact si l'envoi de la notification échoue.
|
||||||
|
strapi.log.error(
|
||||||
|
"Failed to send notification after contact creation:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -7,6 +7,78 @@ import { factories } from "@strapi/strapi";
|
|||||||
export default factories.createCoreController(
|
export default factories.createCoreController(
|
||||||
"api::contact.contact",
|
"api::contact.contact",
|
||||||
({ strapi }) => ({
|
({ strapi }) => ({
|
||||||
|
async activities(ctx) {
|
||||||
|
const userId = ctx.state.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return ctx.unauthorized("User not authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("GET /contacts/activities - Requête reçue:", {
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const friends = await strapi
|
||||||
|
.documents("api::contact.contact")
|
||||||
|
.findMany({
|
||||||
|
filters: {
|
||||||
|
owner: { id: userId },
|
||||||
|
state: "accepted",
|
||||||
|
},
|
||||||
|
populate: {
|
||||||
|
user: {
|
||||||
|
fields: ["id", "username"],
|
||||||
|
populate: {
|
||||||
|
activities: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("GET /contacts/activities - Amis trouvés:", {
|
||||||
|
count: friends.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
const friendsWithLatestActivity = friends.map((contact) => {
|
||||||
|
const user = contact.user;
|
||||||
|
let latestActivity = null;
|
||||||
|
|
||||||
|
if (user.activities && Array.isArray(user.activities) && user.activities.length > 0) {
|
||||||
|
latestActivity = user.activities.reduce((latest, activity) => {
|
||||||
|
const latestDate = new Date(latest.activityDate).getTime();
|
||||||
|
const currentDate = new Date(activity.activityDate).getTime();
|
||||||
|
return currentDate > latestDate ? activity : latest;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
latestActivity: latestActivity,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: friendsWithLatestActivity,
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
pageSize: friendsWithLatestActivity.length,
|
||||||
|
pageCount: 1,
|
||||||
|
total: friendsWithLatestActivity.length,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("GET /contacts/activities - Erreur:", {
|
||||||
|
error: error.message,
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
async find(ctx) {
|
async find(ctx) {
|
||||||
// Log de la requête entrante
|
// Log de la requête entrante
|
||||||
console.log("GET /contacts - Requête reçue:", {
|
console.log("GET /contacts - Requête reçue:", {
|
||||||
@@ -72,8 +144,8 @@ export default factories.createCoreController(
|
|||||||
.documents("api::contact.contact")
|
.documents("api::contact.contact")
|
||||||
.findFirst({
|
.findFirst({
|
||||||
filters: {
|
filters: {
|
||||||
owner: owner,
|
owner: { id: owner },
|
||||||
user: user,
|
user: { id: user },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -148,10 +220,10 @@ export default factories.createCoreController(
|
|||||||
filters: {
|
filters: {
|
||||||
$or: [
|
$or: [
|
||||||
{
|
{
|
||||||
user: id,
|
user: { id: id },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
owner: id,
|
owner: { id: id },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -192,5 +264,90 @@ export default factories.createCoreController(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async suggestions(ctx) {
|
||||||
|
const userId = ctx.state.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return ctx.unauthorized("User not authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("GET /contacts/suggestions - Requête reçue:", {
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userFriends = await strapi
|
||||||
|
.documents("api::contact.contact")
|
||||||
|
.findMany({
|
||||||
|
filters: {
|
||||||
|
owner: { id: userId },
|
||||||
|
state: "accepted",
|
||||||
|
},
|
||||||
|
populate: {
|
||||||
|
user: {
|
||||||
|
fields: ["id", "username"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("GET /contacts/suggestions - Amis trouvés:", {
|
||||||
|
count: userFriends.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
const friendIds = new Set(userFriends.map((contact) => contact.user.id));
|
||||||
|
friendIds.add(userId);
|
||||||
|
|
||||||
|
const suggestedFriendIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const friend of userFriends) {
|
||||||
|
const friendsFriendsContacts = await strapi
|
||||||
|
.documents("api::contact.contact")
|
||||||
|
.findMany({
|
||||||
|
filters: {
|
||||||
|
owner: { id: friend.user.id },
|
||||||
|
state: "accepted",
|
||||||
|
},
|
||||||
|
populate: {
|
||||||
|
user: {
|
||||||
|
fields: ["id", "username"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const friendFriend of friendsFriendsContacts) {
|
||||||
|
if (!friendIds.has(friendFriend.user.id)) {
|
||||||
|
suggestedFriendIds.add(JSON.stringify({
|
||||||
|
id: friendFriend.user.id,
|
||||||
|
username: friendFriend.user.username,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const suggestions = Array.from(suggestedFriendIds)
|
||||||
|
.map((str) => JSON.parse(str))
|
||||||
|
.sort((a, b) => a.username.localeCompare(b.username));
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: suggestions,
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
pageSize: suggestions.length,
|
||||||
|
pageCount: 1,
|
||||||
|
total: suggestions.length,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("GET /contacts/suggestions - Erreur:", {
|
||||||
|
error: error.message,
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
58
src/api/contact/document-middleware.ts
Normal file
58
src/api/contact/document-middleware.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// path: src/api/contact/document-middleware.ts
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Déclenché à la création d’un document Contact
|
||||||
|
*/
|
||||||
|
async beforeCreate(event) {
|
||||||
|
// Pas besoin ici, mais tu peux faire des validations avant création
|
||||||
|
},
|
||||||
|
|
||||||
|
async afterCreate(event) {
|
||||||
|
const { result } = event;
|
||||||
|
|
||||||
|
// On récupère les infos du contact
|
||||||
|
const contact = result;
|
||||||
|
|
||||||
|
// On agit seulement sur les contacts en attente
|
||||||
|
if (contact.state !== "pending") return;
|
||||||
|
|
||||||
|
const fromUserId = contact.fromUser?.id || contact.fromUser;
|
||||||
|
const toUserId = contact.toUser?.id || contact.toUser;
|
||||||
|
|
||||||
|
if (!fromUserId || !toUserId) {
|
||||||
|
strapi.log.warn("Notification non créée : fromUser ou toUser manquant.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
/*await strapi.service("api::notification.notification").create({
|
||||||
|
data: {
|
||||||
|
type: "contact-request",
|
||||||
|
title: "Nouvelle demande de contact",
|
||||||
|
message: `L'utilisateur #${fromUserId} vous a envoyé une demande de contact.`,
|
||||||
|
user: toUserId,
|
||||||
|
meta: {
|
||||||
|
fromUserId,
|
||||||
|
contactId: contact.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});*/
|
||||||
|
|
||||||
|
strapi.log.info(`Notification envoyée à l’utilisateur ${toUserId}`);
|
||||||
|
} catch (error) {
|
||||||
|
strapi.log.error(
|
||||||
|
"Erreur lors de la création de la notification :",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async beforeUpdate(event) {
|
||||||
|
// pour plus tard si tu veux : accepter/refuser…
|
||||||
|
},
|
||||||
|
|
||||||
|
async afterUpdate(event) {
|
||||||
|
// idem, pour envoyer une notif sur "accepted" ou "rejected"
|
||||||
|
},
|
||||||
|
};
|
||||||
33
src/api/contact/routes/01-custom.ts
Normal file
33
src/api/contact/routes/01-custom.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Custom contact routes
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
type: "content-api",
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: "/contacts/activities",
|
||||||
|
handler: "contact.activities",
|
||||||
|
config: {
|
||||||
|
// Ajoutez ce bloc 'auth' pour être explicite
|
||||||
|
auth: {
|
||||||
|
scope: ["api::contact.contact.activities"], // Nom exact de la permission
|
||||||
|
},
|
||||||
|
policies: [], // On garde les politiques par défaut
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: "/contacts/suggestions",
|
||||||
|
handler: "contact.suggestions",
|
||||||
|
config: {
|
||||||
|
// Ajoutez ce bloc 'auth' pour être explicite
|
||||||
|
auth: {
|
||||||
|
scope: ["api::contact.contact.activities"], // Nom exact de la permission
|
||||||
|
},
|
||||||
|
policies: [], // On garde les politiques par défaut
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -23,8 +23,10 @@ export default ({ strapi }: { strapi: Core.Strapi }) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const groupId = ctx.request.body.data.group;
|
||||||
// Format tags from array of strings to array of component objects
|
// Format tags from array of strings to array of component objects
|
||||||
const data = ctx.request.body.data;
|
const data = ctx.request.body.data;
|
||||||
|
delete data.group;
|
||||||
if (data.tags && Array.isArray(data.tags)) {
|
if (data.tags && Array.isArray(data.tags)) {
|
||||||
data.tags = data.tags
|
data.tags = data.tags
|
||||||
.filter((tag: any) => {
|
.filter((tag: any) => {
|
||||||
@@ -51,14 +53,17 @@ export default ({ strapi }: { strapi: Core.Strapi }) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Create the owner relationship
|
// 2. Create the owner relationship
|
||||||
|
const contextType = groupId && groupId !== 0 ? "group" : "user";
|
||||||
|
const contextId = groupId && groupId !== 0 ? groupId : userId;
|
||||||
|
|
||||||
const ownerRelationship = await strapi.db
|
const ownerRelationship = await strapi.db
|
||||||
.query("api::event-relationship.event-relationship")
|
.query("api::event-relationship.event-relationship")
|
||||||
.create({
|
.create({
|
||||||
data: {
|
data: {
|
||||||
author: userId,
|
author: userId,
|
||||||
event: event.id,
|
event: event.id,
|
||||||
contextType: "user",
|
contextType,
|
||||||
contextId: userId,
|
contextId,
|
||||||
relation: "owner",
|
relation: "owner",
|
||||||
metas: {
|
metas: {
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
@@ -70,6 +75,16 @@ export default ({ strapi }: { strapi: Core.Strapi }) =>
|
|||||||
`Event ${event.id} created by user ${userId} with owner relationship ${ownerRelationship.id}`
|
`Event ${event.id} created by user ${userId} with owner relationship ${ownerRelationship.id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (contextType === "group") {
|
||||||
|
await strapi
|
||||||
|
.service("api::group.group")
|
||||||
|
.addActivity(
|
||||||
|
contextId,
|
||||||
|
ctx.state.user?.username || `User ${userId}`,
|
||||||
|
`Evènement créé : ${event.title}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ctx.send({ data: event }, 201);
|
ctx.send({ data: event }, 201);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
strapi.log.error("Error in create event controller:", error);
|
strapi.log.error("Error in create event controller:", error);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Context } from "koa";
|
|||||||
export default ({ strapi }: { strapi: Core.Strapi }) =>
|
export default ({ strapi }: { strapi: Core.Strapi }) =>
|
||||||
async function feed(ctx: Context) {
|
async function feed(ctx: Context) {
|
||||||
const userId = ctx.state.user?.id;
|
const userId = ctx.state.user?.id;
|
||||||
|
const { contextId, contextType } = ctx.query;
|
||||||
if (!userId) return ctx.badRequest("userId is required");
|
if (!userId) return ctx.badRequest("userId is required");
|
||||||
|
|
||||||
// Récupérer et valider la query avec les fonctions de Strapi
|
// Récupérer et valider la query avec les fonctions de Strapi
|
||||||
@@ -26,7 +26,12 @@ export default ({ strapi }: { strapi: Core.Strapi }) =>
|
|||||||
{ auth: ctx.state.auth }
|
{ auth: ctx.state.auth }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { _limit = 20, _start = 0 } = sanitizedQueryParams;
|
const {
|
||||||
|
_limit = 20,
|
||||||
|
_start = 0,
|
||||||
|
contextType,
|
||||||
|
contextId,
|
||||||
|
} = sanitizedQueryParams;
|
||||||
limit = Math.max(1, Math.min(parseInt(String(_limit)) || 20, 100)); // Entre 1 et 100
|
limit = Math.max(1, Math.min(parseInt(String(_limit)) || 20, 100)); // Entre 1 et 100
|
||||||
start = Math.max(0, parseInt(String(_start)) || 0); // Minimum 0
|
start = Math.max(0, parseInt(String(_start)) || 0); // Minimum 0
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -100,11 +105,15 @@ export default ({ strapi }: { strapi: Core.Strapi }) =>
|
|||||||
.map((membership) => membership.group.id)
|
.map((membership) => membership.group.id)
|
||||||
.filter((groupId) => !groupIds.includes(groupId)); // Exclure mes propres groupes
|
.filter((groupId) => !groupIds.includes(groupId)); // Exclure mes propres groupes
|
||||||
|
|
||||||
// 4️⃣ Récupérer le count total pour la pagination
|
// Construire la condition where en fonction des paramètres contextId et contextType
|
||||||
const totalCount = await strapi.db
|
const whereCondition =
|
||||||
.query("api::event-relationship.event-relationship")
|
contextId && contextType
|
||||||
.count({
|
? {
|
||||||
where: {
|
relation: "owner",
|
||||||
|
contextType,
|
||||||
|
contextId: parseInt(String(contextId)),
|
||||||
|
}
|
||||||
|
: {
|
||||||
relation: "owner",
|
relation: "owner",
|
||||||
$or: [
|
$or: [
|
||||||
// EventRelationships de l'utilisateur courant
|
// EventRelationships de l'utilisateur courant
|
||||||
@@ -120,30 +129,20 @@ export default ({ strapi }: { strapi: Core.Strapi }) =>
|
|||||||
// EventRelationships système
|
// EventRelationships système
|
||||||
{ contextType: "system" },
|
{ contextType: "system" },
|
||||||
],
|
],
|
||||||
},
|
};
|
||||||
|
|
||||||
|
// 4️⃣ Récupérer le count total pour la pagination
|
||||||
|
const totalCount = await strapi.db
|
||||||
|
.query("api::event-relationship.event-relationship")
|
||||||
|
.count({
|
||||||
|
where: whereCondition,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4️⃣ Récupérer le feed avec pagination
|
// 4️⃣ Récupérer le feed avec pagination
|
||||||
const feed = await strapi.db
|
const feed = await strapi.db
|
||||||
.query("api::event-relationship.event-relationship")
|
.query("api::event-relationship.event-relationship")
|
||||||
.findMany({
|
.findMany({
|
||||||
where: {
|
where: whereCondition,
|
||||||
relation: "owner",
|
|
||||||
$or: [
|
|
||||||
// EventRelationships de l'utilisateur courant
|
|
||||||
{ author: { id: parseInt(userId) } },
|
|
||||||
// EventRelationships des amis
|
|
||||||
{ author: { id: friendIds } },
|
|
||||||
// EventRelationships des contacts suivis
|
|
||||||
{ author: { id: followIds } },
|
|
||||||
// EventRelationships des groupes de l'utilisateur
|
|
||||||
{ contextType: "group", contextId: groupIds },
|
|
||||||
// EventRelationships des groupes des amis
|
|
||||||
{ contextType: "group", contextId: friendsGroupIds },
|
|
||||||
// EventRelationships système
|
|
||||||
{ contextType: "system" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
...queryParams,
|
...queryParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,9 @@
|
|||||||
"admin",
|
"admin",
|
||||||
"owner",
|
"owner",
|
||||||
"follow",
|
"follow",
|
||||||
"pending"
|
"pending",
|
||||||
|
"invited",
|
||||||
|
"rejected"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,25 @@ export default factories.createCoreController(
|
|||||||
);
|
);
|
||||||
membershipMeta.success = true;
|
membershipMeta.success = true;
|
||||||
membershipMeta.membershipId = membership?.id as number;
|
membershipMeta.membershipId = membership?.id as number;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await strapi.service("api::notification.notification").createNotification({
|
||||||
|
title: "Groupe créé",
|
||||||
|
message: `Vous avez créé un nouveau groupe`,
|
||||||
|
type: "success",
|
||||||
|
target_user: userId,
|
||||||
|
source: "group-create",
|
||||||
|
payload: {
|
||||||
|
groupId,
|
||||||
|
membershipId: membership?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (notificationErr: any) {
|
||||||
|
strapi.log.warn(
|
||||||
|
"[group.create] Erreur création notification:",
|
||||||
|
notificationErr
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
strapi.log.error(
|
strapi.log.error(
|
||||||
@@ -97,5 +116,98 @@ export default factories.createCoreController(
|
|||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
async invite(ctx) {
|
||||||
|
const body = ctx.request.body as any;
|
||||||
|
const data =
|
||||||
|
typeof body?.data === "string"
|
||||||
|
? JSON.parse(body.data)
|
||||||
|
: body?.data || {};
|
||||||
|
|
||||||
|
const groupId = ctx.params.id;
|
||||||
|
const userIds = data?.usersIds || [];
|
||||||
|
|
||||||
|
if (!groupId) {
|
||||||
|
return ctx.badRequest("Missing groupId");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(userIds) || userIds.length === 0) {
|
||||||
|
return ctx.badRequest("userIds must be a non-empty array");
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = {
|
||||||
|
success: [],
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const userId of userIds) {
|
||||||
|
try {
|
||||||
|
const existingMembership = await strapi.entityService.findMany(
|
||||||
|
"api::group-membership.group-membership",
|
||||||
|
{
|
||||||
|
filters: {
|
||||||
|
user: userId,
|
||||||
|
group: groupId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingMembership && existingMembership.length > 0) {
|
||||||
|
results.errors.push({
|
||||||
|
userId,
|
||||||
|
error: "User is already a member of this group",
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const membership = await strapi.entityService.create(
|
||||||
|
"api::group-membership.group-membership",
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
user: userId,
|
||||||
|
group: groupId,
|
||||||
|
role: "invited",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await strapi.service("api::notification.notification").createNotification({
|
||||||
|
title: "Invitation de groupe",
|
||||||
|
message: `Vous avez été invité à rejoindre un groupe`,
|
||||||
|
type: "info",
|
||||||
|
target_user: userId,
|
||||||
|
source: "group-invite",
|
||||||
|
payload: {
|
||||||
|
groupId,
|
||||||
|
membershipId: membership?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (notificationErr: any) {
|
||||||
|
strapi.log.warn(
|
||||||
|
"[group.invite] Erreur création notification:",
|
||||||
|
notificationErr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.success.push({
|
||||||
|
userId,
|
||||||
|
membershipId: membership?.id,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
strapi.log.error(
|
||||||
|
"[group.invite] Erreur création GroupMembership:",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
results.errors.push({
|
||||||
|
userId,
|
||||||
|
error: err?.message || "Unknown error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
data: results,
|
||||||
|
};
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
13
src/api/group/routes/custom.ts
Normal file
13
src/api/group/routes/custom.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Custom post routes
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: "/group/:id/invite",
|
||||||
|
handler: "group.invite",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -4,4 +4,25 @@
|
|||||||
|
|
||||||
import { factories } from '@strapi/strapi';
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
export default factories.createCoreService('api::group.group');
|
export default factories.createCoreService('api::group.group', ({ strapi }) => ({
|
||||||
|
async addActivity(groupId: number, userName: string, message: string) {
|
||||||
|
const group = await strapi.entityService.findOne('api::group.group', groupId, {
|
||||||
|
populate: ['activities'],
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
throw new Error(`Group with id ${groupId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const activities = group.activities || [];
|
||||||
|
activities.push({
|
||||||
|
activityUser: userName,
|
||||||
|
activityMessage: message,
|
||||||
|
activityDate: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return await strapi.entityService.update('api::group.group', groupId, {
|
||||||
|
data: { activities },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|||||||
30
src/api/mails/content-types/mails/schema.json
Normal file
30
src/api/mails/content-types/mails/schema.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"kind": "singleType",
|
||||||
|
"collectionName": "mailss",
|
||||||
|
"info": {
|
||||||
|
"singularName": "mails",
|
||||||
|
"pluralName": "mailss",
|
||||||
|
"displayName": "Mails",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": false
|
||||||
|
},
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"onCreateUser": {
|
||||||
|
"type": "component",
|
||||||
|
"repeatable": false,
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"component": "mail.mail"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/api/mails/controllers/mails.ts
Normal file
7
src/api/mails/controllers/mails.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* mails controller
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi'
|
||||||
|
|
||||||
|
export default factories.createCoreController('api::mails.mails');
|
||||||
7
src/api/mails/routes/mails.ts
Normal file
7
src/api/mails/routes/mails.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* mails router
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreRouter('api::mails.mails');
|
||||||
7
src/api/mails/services/mails.ts
Normal file
7
src/api/mails/services/mails.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* mails service
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreService('api::mails.mails');
|
||||||
@@ -4,26 +4,24 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"singularName": "notification",
|
"singularName": "notification",
|
||||||
"pluralName": "notifications",
|
"pluralName": "notifications",
|
||||||
"displayName": "Notification"
|
"displayName": "Notification",
|
||||||
|
"description": ""
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"draftAndPublish": false
|
"draftAndPublish": false
|
||||||
},
|
},
|
||||||
"pluginOptions": {},
|
"pluginOptions": {},
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"title": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"message": {
|
"message": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "enumeration",
|
"type": "enumeration",
|
||||||
"enum": [
|
"enum": [
|
||||||
"info",
|
"accountCreation",
|
||||||
"success",
|
"friendRequest",
|
||||||
"warning",
|
"messageSent",
|
||||||
"error"
|
"postCreated"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"target_user": {
|
"target_user": {
|
||||||
@@ -35,11 +33,20 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
"source": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"payload": {
|
"payload": {
|
||||||
"type": "json"
|
"type": "json"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"type": "enumeration",
|
||||||
|
"enum": [
|
||||||
|
"system",
|
||||||
|
"user",
|
||||||
|
"group",
|
||||||
|
"choral"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"floodId": {
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ interface NotificationPayload {
|
|||||||
type?: "info" | "success" | "warning" | "error";
|
type?: "info" | "success" | "warning" | "error";
|
||||||
target_user?: number;
|
target_user?: number;
|
||||||
source?: string;
|
source?: string;
|
||||||
|
floodId?: number;
|
||||||
payload?: Record<string, any>;
|
payload?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,12 +15,21 @@ interface NotificationEntity {
|
|||||||
type: string;
|
type: string;
|
||||||
target_user?: number;
|
target_user?: number;
|
||||||
source?: string;
|
source?: string;
|
||||||
|
floodId?: number;
|
||||||
payload?: Record<string, any>;
|
payload?: Record<string, any>;
|
||||||
read: boolean;
|
read: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ActivityPayload {
|
||||||
|
userId: number;
|
||||||
|
activityMessage: string;
|
||||||
|
activityUser: string;
|
||||||
|
activityType: string;
|
||||||
|
activityAvatar?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* notification service
|
* notification service
|
||||||
*/
|
*/
|
||||||
@@ -30,22 +40,57 @@ export default factories.createCoreService(
|
|||||||
({ strapi }: { strapi }) => ({
|
({ strapi }: { strapi }) => ({
|
||||||
//Custom service for add notification.
|
//Custom service for add notification.
|
||||||
async createNotification({
|
async createNotification({
|
||||||
title,
|
|
||||||
message,
|
message,
|
||||||
type = "info",
|
type = "info",
|
||||||
target_user,
|
target_user,
|
||||||
source,
|
source,
|
||||||
|
floodId,
|
||||||
payload,
|
payload,
|
||||||
}: NotificationPayload): Promise<NotificationEntity> {
|
}: NotificationPayload): Promise<NotificationEntity> {
|
||||||
const data = { title, message, type, target_user, source, payload };
|
const data = { message, type, target_user, source, floodId, payload };
|
||||||
|
|
||||||
const notification = await strapi.entityService.create(
|
const notification = await strapi.entityService.create(
|
||||||
"api::notification.notification",
|
"api::notification.notification",
|
||||||
{ data }
|
{ data }
|
||||||
);
|
);
|
||||||
|
|
||||||
strapi.log.info(`🔔 Notification créée: ${title}`);
|
strapi.log.info(`🔔 Notification créée: ${message}`);
|
||||||
return notification;
|
return notification;
|
||||||
},
|
},
|
||||||
|
async addActivity({
|
||||||
|
userId,
|
||||||
|
activityMessage,
|
||||||
|
activityUser,
|
||||||
|
activityType,
|
||||||
|
}: ActivityPayload): Promise<void> {
|
||||||
|
const user = await strapi.entityService.findOne(
|
||||||
|
"plugin::users-permissions.user",
|
||||||
|
userId,
|
||||||
|
{ populate: ["activities", "avatar"] }
|
||||||
|
);
|
||||||
|
|
||||||
|
const existingActivities = user?.activities || [];
|
||||||
|
const newActivity = {
|
||||||
|
activityMessage,
|
||||||
|
activityUser,
|
||||||
|
activityDate: new Date(),
|
||||||
|
activityType,
|
||||||
|
activityAvatar: user.avatar?.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
await strapi.entityService.update(
|
||||||
|
"plugin::users-permissions.user",
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
activities: [...existingActivities, newActivity],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
strapi.log.info(
|
||||||
|
`✅ Activity added for user ${activityUser}: ${activityMessage}`
|
||||||
|
);
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
25
src/api/page/content-types/page/schema.json
Normal file
25
src/api/page/content-types/page/schema.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"kind": "collectionType",
|
||||||
|
"collectionName": "pages",
|
||||||
|
"info": {
|
||||||
|
"singularName": "page",
|
||||||
|
"pluralName": "pages",
|
||||||
|
"displayName": "Page"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": false
|
||||||
|
},
|
||||||
|
"pluginOptions": {},
|
||||||
|
"attributes": {
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "richtext"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "uid",
|
||||||
|
"targetField": "title"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/api/page/controllers/page.ts
Normal file
7
src/api/page/controllers/page.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* page controller
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi'
|
||||||
|
|
||||||
|
export default factories.createCoreController('api::page.page');
|
||||||
7
src/api/page/routes/page.ts
Normal file
7
src/api/page/routes/page.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* page router
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreRouter('api::page.page');
|
||||||
7
src/api/page/services/page.ts
Normal file
7
src/api/page/services/page.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* page service
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreService('api::page.page');
|
||||||
@@ -36,6 +36,7 @@ export default factories.createCoreController(
|
|||||||
|
|
||||||
delete data.author;
|
delete data.author;
|
||||||
delete data.isPublic;
|
delete data.isPublic;
|
||||||
|
delete data.comments;
|
||||||
ctx.request.body = { data };
|
ctx.request.body = { data };
|
||||||
const result = await super.create(ctx);
|
const result = await super.create(ctx);
|
||||||
|
|
||||||
|
|||||||
22
src/components/configuration/parameter.json
Normal file
22
src/components/configuration/parameter.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"collectionName": "components_configuration_parameters",
|
||||||
|
"info": {
|
||||||
|
"displayName": "parameter",
|
||||||
|
"icon": "cog"
|
||||||
|
},
|
||||||
|
"options": {},
|
||||||
|
"attributes": {
|
||||||
|
"emailNotification": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"pushNotification": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"sendNewsgroup": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/components/configuration/privacy.json
Normal file
35
src/components/configuration/privacy.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"collectionName": "components_configuration_privacies",
|
||||||
|
"info": {
|
||||||
|
"displayName": "privacy",
|
||||||
|
"icon": "key",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"options": {},
|
||||||
|
"attributes": {
|
||||||
|
"profileVisibility": {
|
||||||
|
"type": "enumeration",
|
||||||
|
"enum": [
|
||||||
|
"public",
|
||||||
|
"contacts",
|
||||||
|
"private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"allowMessage": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"allowInvite": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"allowTag": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"showOnline": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,28 @@
|
|||||||
},
|
},
|
||||||
"activityDate": {
|
"activityDate": {
|
||||||
"type": "datetime"
|
"type": "datetime"
|
||||||
|
},
|
||||||
|
"activityAvatar": {
|
||||||
|
"type": "media",
|
||||||
|
"multiple": false,
|
||||||
|
"required": false,
|
||||||
|
"allowedTypes": [
|
||||||
|
"images",
|
||||||
|
"files",
|
||||||
|
"videos",
|
||||||
|
"audios"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"activityType": {
|
||||||
|
"type": "enumeration",
|
||||||
|
"enum": [
|
||||||
|
"user",
|
||||||
|
"group",
|
||||||
|
"choral",
|
||||||
|
"message",
|
||||||
|
"post",
|
||||||
|
"contact"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/components/mail/mail.json
Normal file
17
src/components/mail/mail.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"collectionName": "components_mail_mail",
|
||||||
|
"info": {
|
||||||
|
"displayName": "mail",
|
||||||
|
"icon": "paperPlane",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"options": {},
|
||||||
|
"attributes": {
|
||||||
|
"subject": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/components/user/language.json
Normal file
17
src/components/user/language.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"collectionName": "components_user_languages",
|
||||||
|
"info": {
|
||||||
|
"displayName": "language",
|
||||||
|
"icon": "feather",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"options": {},
|
||||||
|
"attributes": {
|
||||||
|
"language": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
166
src/email-templates/confirmation.html
Normal file
166
src/email-templates/confirmation.html
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<!-- Pré-en-tête (préheader) : aperçu dans certaines boîtes mail -->
|
||||||
|
<span style="display: none; max-height: 0; overflow: hidden"
|
||||||
|
>Confirme ton adresse e-mail en un clic pour activer ton compte.</span
|
||||||
|
>
|
||||||
|
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
width="100%"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
style="
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
padding: 24px 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
"Helvetica Neue", Arial;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
width="600"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
style="
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 6px 18px rgba(20, 30, 50, 0.08);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- En-tête -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 28px 32px 8px">
|
||||||
|
<div style="display: flex; align-items: center; gap: 12px">
|
||||||
|
<img
|
||||||
|
src="https://www.choralsync.com/logo_mini.png"
|
||||||
|
alt="Votre application"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
style="display: block; border-radius: 8px; border: 0"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h1 style="margin: 0; font-size: 18px; color: #0f1724">
|
||||||
|
ChoralSync
|
||||||
|
</h1>
|
||||||
|
<p style="margin: 2px 0 0; font-size: 12px; color: #6b7280">
|
||||||
|
Bienvenue — plus qu’une étape
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Corps -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 20px 32px 8px; color: #0f1724">
|
||||||
|
<h2 style="margin: 0 0 8px; font-size: 20px">
|
||||||
|
Bonjour {{USER_NAME}},
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #374151;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Merci de t’être inscrit. Clique sur le bouton ci-dessous pour
|
||||||
|
confirmer ton adresse e-mail et activer ton compte.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Bouton -->
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
style="margin: 20px 0"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a
|
||||||
|
href="{{CONFIRM_URL}}"
|
||||||
|
style="
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 22px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
#6366f1,
|
||||||
|
#06b6d4
|
||||||
|
);
|
||||||
|
color: #ffffff;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Confirmer mon e-mail
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Lien de secours -->
|
||||||
|
<p style="margin: 8px 0 0; font-size: 13px; color: #6b7280">
|
||||||
|
Si le bouton ne fonctionne pas, copie-colle ce lien dans ton
|
||||||
|
navigateur :
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #0f1724;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e6e9ef;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="{{CONFIRM_URL}}"
|
||||||
|
style="color: #0369a1; text-decoration: underline"
|
||||||
|
>{{CONFIRM_URL}}</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Pied de mail -->
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style="
|
||||||
|
padding: 18px 32px 28px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 13px;
|
||||||
|
border-top: 1px solid #f1f3f6;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<p style="margin: 0 0 6px">
|
||||||
|
Si tu n’es pas à l’origine de cette inscription, ignore cet
|
||||||
|
e-mail.
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0; font-size: 12px; color: #9ca3af">
|
||||||
|
© ChoralSync {{YEAR}} •
|
||||||
|
<a
|
||||||
|
href="https://www.choralsync.com"
|
||||||
|
style="color: #9ca3af; text-decoration: underline"
|
||||||
|
>www.choralsync.com</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div style="height: 14px"></div>
|
||||||
|
<p style="font-size: 12px; color: #9ca3af; margin: 0">
|
||||||
|
Besoin d’aide ? Réponds directement à cet e-mail ou consulte notre
|
||||||
|
<a href="https://www.choralsync.com/help" style="color: #0369a1"
|
||||||
|
>centre d’aide</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
File diff suppressed because it is too large
Load Diff
166
src/extensions/users-permissions/confirmation.html
Normal file
166
src/extensions/users-permissions/confirmation.html
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<!-- Pré-en-tête (préheader) : aperçu dans certaines boîtes mail -->
|
||||||
|
<span style="display: none; max-height: 0; overflow: hidden"
|
||||||
|
>Confirme ton adresse e-mail en un clic pour activer ton compte.</span
|
||||||
|
>
|
||||||
|
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
width="100%"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
style="
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
padding: 24px 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
"Helvetica Neue", Arial;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
width="600"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
style="
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 6px 18px rgba(20, 30, 50, 0.08);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- En-tête -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 28px 32px 8px">
|
||||||
|
<div style="display: flex; align-items: center; gap: 12px">
|
||||||
|
<img
|
||||||
|
src="https://www.choralsync.com/logo_mini.png"
|
||||||
|
alt="Votre application"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
style="display: block; border-radius: 8px; border: 0"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h1 style="margin: 0; font-size: 18px; color: #0f1724">
|
||||||
|
ChoralSync
|
||||||
|
</h1>
|
||||||
|
<p style="margin: 2px 0 0; font-size: 12px; color: #6b7280">
|
||||||
|
Bienvenue — plus qu’une étape
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Corps -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 20px 32px 8px; color: #0f1724">
|
||||||
|
<h2 style="margin: 0 0 8px; font-size: 20px">
|
||||||
|
Bonjour {{USER_NAME}},
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #374151;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Merci de t’être inscrit. Clique sur le bouton ci-dessous pour
|
||||||
|
confirmer ton adresse e-mail et activer ton compte.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Bouton -->
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
style="margin: 20px 0"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a
|
||||||
|
href="{{CONFIRM_URL}}"
|
||||||
|
style="
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 22px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
#6366f1,
|
||||||
|
#06b6d4
|
||||||
|
);
|
||||||
|
color: #ffffff;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Confirmer mon e-mail
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Lien de secours -->
|
||||||
|
<p style="margin: 8px 0 0; font-size: 13px; color: #6b7280">
|
||||||
|
Si le bouton ne fonctionne pas, copie-colle ce lien dans ton
|
||||||
|
navigateur :
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #0f1724;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e6e9ef;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="{{CONFIRM_URL}}"
|
||||||
|
style="color: #0369a1; text-decoration: underline"
|
||||||
|
>{{CONFIRM_URL}}</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Pied de mail -->
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style="
|
||||||
|
padding: 18px 32px 28px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 13px;
|
||||||
|
border-top: 1px solid #f1f3f6;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<p style="margin: 0 0 6px">
|
||||||
|
Si tu n’es pas à l’origine de cette inscription, ignore cet
|
||||||
|
e-mail.
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0; font-size: 12px; color: #9ca3af">
|
||||||
|
© ChoralSync {{YEAR}} •
|
||||||
|
<a
|
||||||
|
href="https://www.choralsync.com"
|
||||||
|
style="color: #9ca3af; text-decoration: underline"
|
||||||
|
>www.choralsync.com</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div style="height: 14px"></div>
|
||||||
|
<p style="font-size: 12px; color: #9ca3af; margin: 0">
|
||||||
|
Besoin d’aide ? Réponds directement à cet e-mail ou consulte notre
|
||||||
|
<a href="https://www.choralsync.com/help" style="color: #0369a1"
|
||||||
|
>centre d’aide</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
@@ -73,22 +73,6 @@
|
|||||||
"images"
|
"images"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nbFollowers": {
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0
|
|
||||||
},
|
|
||||||
"nbFollowing": {
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0
|
|
||||||
},
|
|
||||||
"nbPosts": {
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0
|
|
||||||
},
|
|
||||||
"nbSaved": {
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0
|
|
||||||
},
|
|
||||||
"choralOwner": {
|
"choralOwner": {
|
||||||
"type": "relation",
|
"type": "relation",
|
||||||
"relation": "oneToOne",
|
"relation": "oneToOne",
|
||||||
@@ -224,6 +208,36 @@
|
|||||||
"relation": "oneToMany",
|
"relation": "oneToMany",
|
||||||
"target": "api::post-ownership.post-ownership",
|
"target": "api::post-ownership.post-ownership",
|
||||||
"mappedBy": "author"
|
"mappedBy": "author"
|
||||||
|
},
|
||||||
|
"activities": {
|
||||||
|
"type": "component",
|
||||||
|
"repeatable": true,
|
||||||
|
"component": "group.activity"
|
||||||
|
},
|
||||||
|
"bio": {
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"experience": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
"parameter": {
|
||||||
|
"type": "component",
|
||||||
|
"repeatable": false,
|
||||||
|
"component": "configuration.parameter"
|
||||||
|
},
|
||||||
|
"privacy": {
|
||||||
|
"type": "component",
|
||||||
|
"repeatable": false,
|
||||||
|
"component": "configuration.privacy"
|
||||||
|
},
|
||||||
|
"education": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,88 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
const lod = require("lodash");
|
const lod = require("lodash");
|
||||||
|
const utils = require("@strapi/utils");
|
||||||
|
const { concat, compact, isArray, toNumber, getOr } = require("lodash/fp");
|
||||||
|
const cryptoLib = require("crypto");
|
||||||
|
const bcrypt = require("bcryptjs");
|
||||||
|
const fs = require("fs").promises;
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
module.exports = (plugin) => {
|
module.exports = (plugin) => {
|
||||||
const rawProviders = plugin.services.providers({ strapi });
|
const rawProviders = plugin.services.providers({ strapi });
|
||||||
|
const { ApplicationError, ValidationError, ForbiddenError } = utils.errors;
|
||||||
|
const USER_MODEL_UID = "plugin::users-permissions.user";
|
||||||
|
|
||||||
|
const sanitizeUser = (user, ctx) => {
|
||||||
|
const { auth } = ctx.state;
|
||||||
|
const userSchema = strapi.getModel("plugin::users-permissions.user");
|
||||||
|
|
||||||
|
return strapi.contentAPI.sanitize.output(user, userSchema, { auth });
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureHashedPasswords = async (values) => {
|
||||||
|
const attributes = strapi.getModel(USER_MODEL_UID).attributes;
|
||||||
|
|
||||||
|
for (const key in values) {
|
||||||
|
if (attributes[key] && attributes[key].type === "password") {
|
||||||
|
// Check if a custom encryption.rounds has been set on the password attribute
|
||||||
|
const rounds = toNumber(
|
||||||
|
getOr(10, "encryption.rounds", attributes[key])
|
||||||
|
);
|
||||||
|
values[key] = await bcrypt.hash(values[key], rounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values["confirmed"] = false;
|
||||||
|
return values;
|
||||||
|
};
|
||||||
|
|
||||||
|
const edit = async (userId, params = {}) => {
|
||||||
|
return strapi.db.query(USER_MODEL_UID).update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: await ensureHashedPasswords(params),
|
||||||
|
populate: ["role"],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendConfirmationEmail = async (user) => {
|
||||||
|
// Génération du token de confirmation
|
||||||
|
|
||||||
|
const templates = await strapi
|
||||||
|
.documents("api::mails.mails")
|
||||||
|
.findFirst({ populate: "*" });
|
||||||
|
//const onCreateUser = templates.onCreateUser;
|
||||||
|
|
||||||
|
const onCreateUser = templates?.onCreateUser;
|
||||||
|
|
||||||
|
const confirmationToken = cryptoLib.randomBytes(20).toString("hex");
|
||||||
|
|
||||||
|
await edit(user.id, { confirmationToken });
|
||||||
|
const confirmUrl = `${process.env.NEXTJS_URL}/confirmation/submit?confirmation=${confirmationToken}`;
|
||||||
|
|
||||||
|
// Lecture du template HTML depuis le fichier
|
||||||
|
|
||||||
|
const htmlPath = path.join(process.cwd(), "public", "confirmation.html");
|
||||||
|
//let html = await fs.readFile(htmlPath, "utf-8");
|
||||||
|
let html = onCreateUser?.message;
|
||||||
|
|
||||||
|
// Remplacement des variables
|
||||||
|
html = html
|
||||||
|
.replace(/{{USER_NAME}}/g, user.username || user.email)
|
||||||
|
.replace(/{{CONFIRM_URL}}/g, confirmUrl)
|
||||||
|
.replace(/{{YEAR}}/g, new Date().getFullYear().toString());
|
||||||
|
|
||||||
|
// Envoi de l'e-mail
|
||||||
|
await strapi
|
||||||
|
.plugin("email")
|
||||||
|
.service("email")
|
||||||
|
.send({
|
||||||
|
to: user.email,
|
||||||
|
subject: onCreateUser?.subject ?? "Confirme ton adresse e-mail",
|
||||||
|
html,
|
||||||
|
from: "ChoralSync <admin@harmonychoral.com>",
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
};
|
||||||
|
|
||||||
const getService = (name) => {
|
const getService = (name) => {
|
||||||
return strapi.plugin("users-permissions").service(name);
|
return strapi.plugin("users-permissions").service(name);
|
||||||
@@ -33,12 +114,10 @@ module.exports = (plugin) => {
|
|||||||
throw new Error("No access_token.");
|
throw new Error("No access_token.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the profile.
|
|
||||||
const profile = await getProfile(provider, query);
|
const profile = await getProfile(provider, query);
|
||||||
|
|
||||||
const email = lod.toLower(profile.email);
|
const email = lod.toLower(profile.email);
|
||||||
|
|
||||||
// We need at least the mail.
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
throw new Error("Email was not available.");
|
throw new Error("Email was not available.");
|
||||||
}
|
}
|
||||||
@@ -49,9 +128,13 @@ module.exports = (plugin) => {
|
|||||||
where: { email },
|
where: { email },
|
||||||
});
|
});
|
||||||
|
|
||||||
const advancedSettings = await strapi
|
const advancedSettings = (await strapi
|
||||||
.store({ type: "plugin", name: "users-permissions", key: "advanced" })
|
.store({ type: "plugin", name: "users-permissions", key: "advanced" })
|
||||||
.get();
|
.get()) as {
|
||||||
|
allow_register: boolean;
|
||||||
|
unique_email?: boolean;
|
||||||
|
default_role?: string;
|
||||||
|
};
|
||||||
|
|
||||||
let user = lod.find(users, { provider });
|
let user = lod.find(users, { provider });
|
||||||
if (lod.isEmpty(user)) {
|
if (lod.isEmpty(user)) {
|
||||||
@@ -70,15 +153,13 @@ module.exports = (plugin) => {
|
|||||||
throw new Error("Email is already taken.");
|
throw new Error("Email is already taken.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve default role.
|
|
||||||
const defaultRole = await strapi.db
|
const defaultRole = await strapi.db
|
||||||
.query("plugin::users-permissions.role")
|
.query("plugin::users-permissions.role")
|
||||||
.findOne({ where: { type: advancedSettings.default_role } });
|
.findOne({ where: { type: advancedSettings.default_role } });
|
||||||
|
|
||||||
// Create the new user.
|
|
||||||
const newUser = {
|
const newUser = {
|
||||||
...profile,
|
...profile,
|
||||||
email, // overwrite with lowercased email
|
email,
|
||||||
provider,
|
provider,
|
||||||
role: defaultRole.id,
|
role: defaultRole.id,
|
||||||
confirmed: true,
|
confirmed: true,
|
||||||
@@ -93,6 +174,243 @@ module.exports = (plugin) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const originalMe = plugin.controllers.user.me;
|
||||||
|
|
||||||
|
plugin.controllers.user.me = async (ctx) => {
|
||||||
|
const fullUser = await strapi.db
|
||||||
|
.query("plugin::users-permissions.user")
|
||||||
|
.findOne({
|
||||||
|
where: { id: ctx.state.user.id },
|
||||||
|
populate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fullUser.contacts && Array.isArray(fullUser.contacts)) {
|
||||||
|
fullUser.contacts = fullUser.contacts.filter(
|
||||||
|
(contact) => contact.state === "accepted"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullUser.post_ownerships && Array.isArray(fullUser.post_ownerships)) {
|
||||||
|
fullUser.post_ownerships = fullUser.post_ownerships.filter(
|
||||||
|
(ownership) =>
|
||||||
|
ownership.contextType === "user" && ownership.relation === "owner"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
fullUser.group_memberships &&
|
||||||
|
Array.isArray(fullUser.group_memberships)
|
||||||
|
) {
|
||||||
|
fullUser.group_memberships = fullUser.group_memberships.filter(
|
||||||
|
(membership) => ["member", "admin", "owner"].includes(membership.role)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = ctx.state.user;
|
||||||
|
|
||||||
|
const userId = user.id;
|
||||||
|
|
||||||
|
const [
|
||||||
|
contactsCount,
|
||||||
|
groupsCount,
|
||||||
|
postsCount,
|
||||||
|
eventsCount,
|
||||||
|
followersCount,
|
||||||
|
followingCount,
|
||||||
|
] = await Promise.all([
|
||||||
|
strapi.db.query("api::contact.contact").count({
|
||||||
|
where: {
|
||||||
|
$or: [{ owner: userId }, { user: userId }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
strapi.db.query("api::group-membership.group-membership").count({
|
||||||
|
where: {
|
||||||
|
user: userId,
|
||||||
|
role: { $in: ["owner", "member", "admin"] },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
strapi.db
|
||||||
|
.query("api::post-ownership.post-ownership")
|
||||||
|
.count({ where: { author: userId } }),
|
||||||
|
strapi.db
|
||||||
|
.query("api::event-relationship.event-relationship")
|
||||||
|
.count({ where: { author: userId } }),
|
||||||
|
strapi.db.query("api::contact.contact").count({
|
||||||
|
where: {
|
||||||
|
user: userId,
|
||||||
|
state: "follow",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
strapi.db.query("api::contact.contact").count({
|
||||||
|
where: {
|
||||||
|
owner: userId,
|
||||||
|
state: "follow",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const result = {
|
||||||
|
...JSON.parse(JSON.stringify(fullUser)),
|
||||||
|
stats: {
|
||||||
|
contacts: contactsCount,
|
||||||
|
groups: groupsCount,
|
||||||
|
posts: postsCount,
|
||||||
|
events: eventsCount,
|
||||||
|
followers: followersCount,
|
||||||
|
following: followingCount,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalFindOne = plugin.controllers.user.findOne;
|
||||||
|
|
||||||
|
plugin.controllers.user.findOne = async (ctx) => {
|
||||||
|
// 1️⃣ Appel du controller d'origine (permissions, policies)
|
||||||
|
await originalFindOne(ctx);
|
||||||
|
|
||||||
|
// 2️⃣ Récupère l'utilisateur renvoyé
|
||||||
|
let user = ctx.body;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return ctx.notFound("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4️⃣ Ajoute un champ calculé
|
||||||
|
user.profileCompleted = Boolean(user.username && user.surname);
|
||||||
|
|
||||||
|
// 3️⃣ Supprime les champs sensibles
|
||||||
|
const sensitive = ["password", "resetPasswordToken", "confirmationToken"];
|
||||||
|
sensitive.forEach((key) => delete user[key]); //post_ownerships
|
||||||
|
|
||||||
|
// 5️⃣ Refetch avec relations peuplées
|
||||||
|
try {
|
||||||
|
const populatedUser = await strapi.entityService.findOne(
|
||||||
|
"plugin::users-permissions.user",
|
||||||
|
user.id,
|
||||||
|
{
|
||||||
|
populate: {
|
||||||
|
post_ownerships: {
|
||||||
|
populate: {
|
||||||
|
post: {
|
||||||
|
populate: {
|
||||||
|
media: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contacts: {
|
||||||
|
populate: {
|
||||||
|
owner: {
|
||||||
|
populate: {
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
populate: {
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
group_memberships: {
|
||||||
|
populate: {
|
||||||
|
group: {
|
||||||
|
populate: {
|
||||||
|
banner: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
activities: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Fusionne les données originales (permissions/serialization) avec les relations
|
||||||
|
user = { ...user, ...populatedUser };
|
||||||
|
|
||||||
|
if (user.contacts && Array.isArray(user.contacts)) {
|
||||||
|
user.contacts = user.contacts.filter(
|
||||||
|
(contact) => contact.state === "accepted"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.post_ownerships && Array.isArray(user.post_ownerships)) {
|
||||||
|
user.post_ownerships = user.post_ownerships.filter(
|
||||||
|
(ownership) =>
|
||||||
|
ownership.contextType === "user" && ownership.relation === "owner"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.group_memberships && Array.isArray(user.group_memberships)) {
|
||||||
|
user.group_memberships = user.group_memberships.filter((membership) =>
|
||||||
|
["member", "admin", "owner"].includes(membership.role)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventRelationships = await strapi.db
|
||||||
|
.query("api::event-relationship.event-relationship")
|
||||||
|
.findMany({
|
||||||
|
where: { author: user.id, relation: "owner", contextType: "user" },
|
||||||
|
populate: {
|
||||||
|
event: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
user.event_relationships = eventRelationships || [];
|
||||||
|
const [
|
||||||
|
contactsCount,
|
||||||
|
groupsCount,
|
||||||
|
postsCount,
|
||||||
|
eventsCount,
|
||||||
|
followersCount,
|
||||||
|
followingCount,
|
||||||
|
] = await Promise.all([
|
||||||
|
strapi.db.query("api::contact.contact").count({
|
||||||
|
where: {
|
||||||
|
$or: [{ owner: user.id }, { user: user.id }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
strapi.db.query("api::group-membership.group-membership").count({
|
||||||
|
where: {
|
||||||
|
user: user.id,
|
||||||
|
role: { $in: ["owner", "member", "admin"] },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
strapi.db
|
||||||
|
.query("api::post-ownership.post-ownership")
|
||||||
|
.count({ where: { author: user.id } }),
|
||||||
|
strapi.db
|
||||||
|
.query("api::event-relationship.event-relationship")
|
||||||
|
.count({ where: { author: user.id } }),
|
||||||
|
strapi.db.query("api::contact.contact").count({
|
||||||
|
where: {
|
||||||
|
user: user.id,
|
||||||
|
state: "follow",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
strapi.db.query("api::contact.contact").count({
|
||||||
|
where: {
|
||||||
|
owner: user.id,
|
||||||
|
state: "follow",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
user.stats = {
|
||||||
|
contacts: contactsCount,
|
||||||
|
groups: groupsCount,
|
||||||
|
posts: postsCount,
|
||||||
|
events: eventsCount,
|
||||||
|
followers: followersCount,
|
||||||
|
following: followingCount,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erreur populate relations user:", err);
|
||||||
|
// fallback : retourne juste l'utilisateur original
|
||||||
|
}
|
||||||
|
ctx.body = user;
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
const uploadImage = async (ctx, label: string, username: string) => {
|
const uploadImage = async (ctx, label: string, username: string) => {
|
||||||
const key = `${label}Image`;
|
const key = `${label}Image`;
|
||||||
if (ctx.request.files[key]) {
|
if (ctx.request.files[key]) {
|
||||||
@@ -122,7 +440,6 @@ module.exports = (plugin) => {
|
|||||||
plugin.controllers.user.updateMe = async (ctx) => {
|
plugin.controllers.user.updateMe = async (ctx) => {
|
||||||
const user = ctx.state.user;
|
const user = ctx.state.user;
|
||||||
|
|
||||||
// User has to be logged in to update themselves
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return ctx.unauthorized();
|
return ctx.unauthorized();
|
||||||
}
|
}
|
||||||
@@ -139,9 +456,31 @@ module.exports = (plugin) => {
|
|||||||
"voice",
|
"voice",
|
||||||
"job",
|
"job",
|
||||||
"dob",
|
"dob",
|
||||||
|
"phone",
|
||||||
|
"bio",
|
||||||
|
"experience",
|
||||||
|
"tags",
|
||||||
|
"languages",
|
||||||
|
"parameter",
|
||||||
|
"privacy",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Make sure there is no duplicate user with the same username
|
/*
|
||||||
|
if (data.tags) {
|
||||||
|
newData.tags = data.tags.map((tag) => ({
|
||||||
|
__component: "social.tags",
|
||||||
|
text: tag.text,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.languages) {
|
||||||
|
newData.languages = data.languages.map((lang) => ({
|
||||||
|
__component: "user.language",
|
||||||
|
language: lang.language,
|
||||||
|
level: lang.level,
|
||||||
|
}));
|
||||||
|
}*/
|
||||||
|
|
||||||
if (newData.username) {
|
if (newData.username) {
|
||||||
const userWithSameUsername = await strapi
|
const userWithSameUsername = await strapi
|
||||||
.query("plugin::users-permissions.user")
|
.query("plugin::users-permissions.user")
|
||||||
@@ -152,7 +491,6 @@ module.exports = (plugin) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure there is no duplicate user with the same email
|
|
||||||
if (newData.email) {
|
if (newData.email) {
|
||||||
const userWithSameEmail = await strapi
|
const userWithSameEmail = await strapi
|
||||||
.query("plugin::users-permissions.user")
|
.query("plugin::users-permissions.user")
|
||||||
@@ -167,12 +505,59 @@ module.exports = (plugin) => {
|
|||||||
ctx.request.body = newData;
|
ctx.request.body = newData;
|
||||||
ctx.params = { id: user.id };
|
ctx.params = { id: user.id };
|
||||||
|
|
||||||
|
const keysExcludingParameterAndPrivacy = Object.keys(newData).filter(
|
||||||
|
(key) => key !== "parameter" && key !== "privacy"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (keysExcludingParameterAndPrivacy.length === 0) {
|
||||||
|
await strapi.entityService
|
||||||
|
.update("plugin::users-permissions.user", user.id, {
|
||||||
|
data: newData,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
ctx.response.status = 200;
|
||||||
|
ctx.response.body = res;
|
||||||
|
return ctx;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
const avatarId = await uploadImage(ctx, "avatar", newData.username);
|
const avatarId = await uploadImage(ctx, "avatar", newData.username);
|
||||||
if (avatarId != 0) ctx.request.body.avatar = avatarId;
|
if (avatarId != 0) ctx.request.body.avatar = avatarId;
|
||||||
const backgroundId = await uploadImage(ctx, "background", newData.username);
|
|
||||||
if (backgroundId != 0) ctx.request.body.background = backgroundId;
|
|
||||||
|
|
||||||
return plugin.controllers.user.update(ctx);
|
return plugin.controllers.user.update(ctx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
plugin.contentTypes.user.lifecycles = {
|
||||||
|
async afterCreate(event) {
|
||||||
|
const { result } = event;
|
||||||
|
|
||||||
|
await sendConfirmationEmail(result);
|
||||||
|
|
||||||
|
await strapi
|
||||||
|
.service("api::notification.notification")
|
||||||
|
?.addActivity({
|
||||||
|
userId: result.id,
|
||||||
|
activityMessage: `Bienvenue ${result.username}, Ton compte est maintenant activé.`,
|
||||||
|
activityUser: result.username,
|
||||||
|
activityType: "user",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Appel du service de notification
|
||||||
|
await strapi
|
||||||
|
.service("api::notification.notification")
|
||||||
|
?.createNotification({
|
||||||
|
title: "Bienvenue !",
|
||||||
|
message: `Ton compte est maintenant activé.`,
|
||||||
|
type: "success",
|
||||||
|
target_user: result.id,
|
||||||
|
source: "system",
|
||||||
|
payload: { email: result.email },
|
||||||
|
});
|
||||||
|
|
||||||
|
strapi.log.info(
|
||||||
|
`🔔 Notification envoyée pour l'utilisateur ${result.id}`
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
plugin.routes["content-api"].routes.push({
|
plugin.routes["content-api"].routes.push({
|
||||||
|
|||||||
73
stack docker/gitea.yaml
Normal file
73
stack docker/gitea.yaml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
gitea:
|
||||||
|
external: false
|
||||||
|
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
image: docker.io/gitea/gitea:1.23.1
|
||||||
|
container_name: gitea
|
||||||
|
environment:
|
||||||
|
- USER_UID=1000
|
||||||
|
- USER_GID=1000
|
||||||
|
- GITEA__database__DB_TYPE=mysql
|
||||||
|
- GITEA__database__HOST=db:3306
|
||||||
|
- GITEA__database__NAME=gitea
|
||||||
|
- GITEA__database__USER=gitea
|
||||||
|
- GITEA__database__PASSWD=Apslxnap12bn23
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- gitea
|
||||||
|
volumes:
|
||||||
|
- ./gitea:/data
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
- "222:22"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: docker.io/library/mysql:8
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- MYSQL_ROOT_PASSWORD=gitea
|
||||||
|
- MYSQL_USER=gitea
|
||||||
|
- MYSQL_PASSWORD=Apslxnap12bn23
|
||||||
|
- MYSQL_DATABASE=gitea
|
||||||
|
networks:
|
||||||
|
- gitea
|
||||||
|
volumes:
|
||||||
|
- ./mysql:/var/lib/mysql
|
||||||
|
runner:
|
||||||
|
image: docker.io/gitea/act_runner:nightly
|
||||||
|
environment:
|
||||||
|
TZ: "Europe/Paris"
|
||||||
|
GITEA_INSTANCE_URL: "http://192.168.0.211:3000"
|
||||||
|
GITEA_RUNNER_REGISTRATION_TOKEN: "4jooaQuS4GQWQKxQptbHuaZkvkhw926YZfITv0Ec"
|
||||||
|
GITEA_RUNNER_NAME: "harmony-runner"
|
||||||
|
volumes:
|
||||||
|
- /opt/actrunner:/data:rw
|
||||||
|
- ./data:/data
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
runner2:
|
||||||
|
image: docker.io/gitea/act_runner:nightly
|
||||||
|
environment:
|
||||||
|
TZ: "Europe/Paris"
|
||||||
|
GITEA_INSTANCE_URL: "http://192.168.0.211:3000"
|
||||||
|
GITEA_RUNNER_REGISTRATION_TOKEN: "6EzcNWaeVrQ0igKNDDjLA2DhwmEG3XA748geBe8B"
|
||||||
|
GITEA_RUNNER_NAME: "harmony-runner2"
|
||||||
|
volumes:
|
||||||
|
- /opt/actrunner2:/data:rw
|
||||||
|
- ./data2:/data
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
registry-ui:
|
||||||
|
image: ghcr.io/eznix86/docker-registry-ui:latest
|
||||||
|
ports:
|
||||||
|
- "8011:80"
|
||||||
|
environment:
|
||||||
|
- REGISTRY_URL=https://git.harmonylab.ovh/harmony/v2
|
||||||
|
- REGISTRY_AUTH=YWRtaW46QXBzbHhuYXAxMmJuMjM=
|
||||||
|
- LOG_LEVEL=debug
|
||||||
26
stack docker/matterhub.yaml
Normal file
26
stack docker/matterhub.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
matter-hub:
|
||||||
|
image: ghcr.io/t0bst4r/home-assistant-matter-hub:latest
|
||||||
|
container_name: home-assistant-matter-hub
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host # indispensable pour mDNS / Matter
|
||||||
|
environment:
|
||||||
|
# Adresse de ton instance Home Assistant
|
||||||
|
# Utilise l’IP locale plutôt que le domaine si possible
|
||||||
|
- HAMH_HOME_ASSISTANT_URL=https://ha.beyonder.synology.me
|
||||||
|
# Ton token long-lived (Home Assistant)
|
||||||
|
- HAMH_HOME_ASSISTANT_ACCESS_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMjU2ZGQ3OTk4MmI0NDdkODNlMjBkYjU4ZDRmOTg1NiIsImlhdCI6MTc2Mjc5NjIwMiwiZXhwIjoyMDc4MTU2MjAyfQ.n2E62mMtmfEqwAjusIGlI0FC5d5Q6jr55UxGAL4u2gc
|
||||||
|
# Niveau de log (debug si tu veux voir les tentatives d’appairage)
|
||||||
|
- HAMH_LOG_LEVEL=debug
|
||||||
|
# Port HTTP de l’interface locale du hub
|
||||||
|
- HAMH_HTTP_PORT=8482
|
||||||
|
volumes:
|
||||||
|
- ./home-assistant-matter-hub:/data
|
||||||
|
# Petit délai pour que le réseau LAN soit prêt
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8482/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
25
stack docker/s3.yaml
Normal file
25
stack docker/s3.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
services:
|
||||||
|
minio:
|
||||||
|
image: bitnami/minio:latest
|
||||||
|
container_name: minio
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
networks:
|
||||||
|
- s3-net
|
||||||
|
volumes:
|
||||||
|
- /data/minio:/bitnami/minio/data
|
||||||
|
env_file: stack.env
|
||||||
|
environment:
|
||||||
|
- MINIO_ROOT_USER=${MINIO_ACCESS_KEY}
|
||||||
|
- MINIO_ROOT_PASSWORD=${MINIO_SECRET_KEY}
|
||||||
|
- MINIO_DEFAULT_BUCKETS=${MINIO_DEFAULT_BUCKETS}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 20s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
s3-net:
|
||||||
69
types/generated/components.d.ts
vendored
69
types/generated/components.d.ts
vendored
@@ -19,6 +19,39 @@ export interface AddressFullAddress extends Struct.ComponentSchema {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfigurationParameter extends Struct.ComponentSchema {
|
||||||
|
collectionName: 'components_configuration_parameters';
|
||||||
|
info: {
|
||||||
|
displayName: 'parameter';
|
||||||
|
icon: 'cog';
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
emailNotification: Schema.Attribute.Boolean &
|
||||||
|
Schema.Attribute.DefaultTo<false>;
|
||||||
|
pushNotification: Schema.Attribute.Boolean &
|
||||||
|
Schema.Attribute.DefaultTo<false>;
|
||||||
|
sendNewsgroup: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigurationPrivacy extends Struct.ComponentSchema {
|
||||||
|
collectionName: 'components_configuration_privacies';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'privacy';
|
||||||
|
icon: 'key';
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
allowInvite: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<true>;
|
||||||
|
allowMessage: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<true>;
|
||||||
|
allowTag: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<true>;
|
||||||
|
profileVisibility: Schema.Attribute.Enumeration<
|
||||||
|
['public', 'contacts', 'private']
|
||||||
|
>;
|
||||||
|
showOnline: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<true>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface GroupActivity extends Struct.ComponentSchema {
|
export interface GroupActivity extends Struct.ComponentSchema {
|
||||||
collectionName: 'components_group_activities';
|
collectionName: 'components_group_activities';
|
||||||
info: {
|
info: {
|
||||||
@@ -26,12 +59,31 @@ export interface GroupActivity extends Struct.ComponentSchema {
|
|||||||
displayName: 'Activity';
|
displayName: 'Activity';
|
||||||
};
|
};
|
||||||
attributes: {
|
attributes: {
|
||||||
|
activityAvatar: Schema.Attribute.Media<
|
||||||
|
'images' | 'files' | 'videos' | 'audios'
|
||||||
|
>;
|
||||||
activityDate: Schema.Attribute.DateTime;
|
activityDate: Schema.Attribute.DateTime;
|
||||||
activityMessage: Schema.Attribute.String;
|
activityMessage: Schema.Attribute.String;
|
||||||
|
activityType: Schema.Attribute.Enumeration<
|
||||||
|
['user', 'group', 'choral', 'message', 'post', 'contact']
|
||||||
|
>;
|
||||||
activityUser: Schema.Attribute.String;
|
activityUser: Schema.Attribute.String;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MailMail extends Struct.ComponentSchema {
|
||||||
|
collectionName: 'components_mail_mail';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'mail';
|
||||||
|
icon: 'paperPlane';
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
message: Schema.Attribute.Text;
|
||||||
|
subject: Schema.Attribute.String;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface SocialTags extends Struct.ComponentSchema {
|
export interface SocialTags extends Struct.ComponentSchema {
|
||||||
collectionName: 'components_social_tags';
|
collectionName: 'components_social_tags';
|
||||||
info: {
|
info: {
|
||||||
@@ -43,6 +95,19 @@ export interface SocialTags extends Struct.ComponentSchema {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserLanguage extends Struct.ComponentSchema {
|
||||||
|
collectionName: 'components_user_languages';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'language';
|
||||||
|
icon: 'feather';
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
language: Schema.Attribute.String;
|
||||||
|
level: Schema.Attribute.Integer;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserPermissions extends Struct.ComponentSchema {
|
export interface UserPermissions extends Struct.ComponentSchema {
|
||||||
collectionName: 'components_user_permissions';
|
collectionName: 'components_user_permissions';
|
||||||
info: {
|
info: {
|
||||||
@@ -63,8 +128,12 @@ declare module '@strapi/strapi' {
|
|||||||
export module Public {
|
export module Public {
|
||||||
export interface ComponentSchemas {
|
export interface ComponentSchemas {
|
||||||
'address.full-address': AddressFullAddress;
|
'address.full-address': AddressFullAddress;
|
||||||
|
'configuration.parameter': ConfigurationParameter;
|
||||||
|
'configuration.privacy': ConfigurationPrivacy;
|
||||||
'group.activity': GroupActivity;
|
'group.activity': GroupActivity;
|
||||||
|
'mail.mail': MailMail;
|
||||||
'social.tags': SocialTags;
|
'social.tags': SocialTags;
|
||||||
|
'user.language': UserLanguage;
|
||||||
'user.permissions': UserPermissions;
|
'user.permissions': UserPermissions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
212
types/generated/contentTypes.d.ts
vendored
212
types/generated/contentTypes.d.ts
vendored
@@ -400,7 +400,11 @@ export interface ApiAdAd extends Struct.CollectionTypeSchema {
|
|||||||
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
localizations: Schema.Attribute.Relation<'oneToMany', 'api::ad.ad'> &
|
localizations: Schema.Attribute.Relation<'oneToMany', 'api::ad.ad'> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
location: Schema.Attribute.Component<'address.full-address', false>;
|
location: Schema.Attribute.Text;
|
||||||
|
medias: Schema.Attribute.Media<
|
||||||
|
'images' | 'files' | 'videos' | 'audios',
|
||||||
|
true
|
||||||
|
>;
|
||||||
publishedAt: Schema.Attribute.DateTime;
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
title: Schema.Attribute.String;
|
title: Schema.Attribute.String;
|
||||||
updatedAt: Schema.Attribute.DateTime;
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
@@ -584,6 +588,124 @@ export interface ApiChannelChannel extends Struct.CollectionTypeSchema {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiChatConversationMemberChatConversationMember
|
||||||
|
extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'chat_conversation_members';
|
||||||
|
info: {
|
||||||
|
displayName: 'ChatConversationMember';
|
||||||
|
pluralName: 'chat-conversation-members';
|
||||||
|
singularName: 'chat-conversation-member';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
conversation: Schema.Attribute.Relation<
|
||||||
|
'oneToOne',
|
||||||
|
'api::chat-conversation.chat-conversation'
|
||||||
|
>;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
joinedAt: Schema.Attribute.DateTime;
|
||||||
|
lastReadAt: Schema.Attribute.DateTime;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'api::chat-conversation-member.chat-conversation-member'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
role: Schema.Attribute.Enumeration<['member', 'admin', 'owner']>;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
user: Schema.Attribute.Relation<
|
||||||
|
'oneToOne',
|
||||||
|
'plugin::users-permissions.user'
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiChatConversationChatConversation
|
||||||
|
extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'chat_conversations';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'ChatConversation';
|
||||||
|
pluralName: 'chat-conversations';
|
||||||
|
singularName: 'chat-conversation';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
creator: Schema.Attribute.Relation<
|
||||||
|
'oneToOne',
|
||||||
|
'plugin::users-permissions.user'
|
||||||
|
>;
|
||||||
|
isGroup: Schema.Attribute.Boolean;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'api::chat-conversation.chat-conversation'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
messages: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'api::chat-message.chat-message'
|
||||||
|
>;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
title: Schema.Attribute.String;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
users: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'plugin::users-permissions.user'
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiChatMessageChatMessage extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'chat_messages';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'ChatMessage';
|
||||||
|
pluralName: 'chat-messages';
|
||||||
|
singularName: 'chat-message';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
content: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
deletedAt: Schema.Attribute.DateTime;
|
||||||
|
isEdited: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'api::chat-message.chat-message'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
media: Schema.Attribute.Media<'images' | 'files' | 'videos' | 'audios'>;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
sender: Schema.Attribute.Relation<
|
||||||
|
'oneToOne',
|
||||||
|
'plugin::users-permissions.user'
|
||||||
|
>;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiChoralMembershipChoralMembership
|
export interface ApiChoralMembershipChoralMembership
|
||||||
extends Struct.CollectionTypeSchema {
|
extends Struct.CollectionTypeSchema {
|
||||||
collectionName: 'choral_memberships';
|
collectionName: 'choral_memberships';
|
||||||
@@ -1047,7 +1169,7 @@ export interface ApiGroupMembershipGroupMembership
|
|||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
publishedAt: Schema.Attribute.DateTime;
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
role: Schema.Attribute.Enumeration<
|
role: Schema.Attribute.Enumeration<
|
||||||
['member', 'admin', 'owner', 'follow', 'pending']
|
['member', 'admin', 'owner', 'follow', 'pending', 'invited', 'rejected']
|
||||||
>;
|
>;
|
||||||
updatedAt: Schema.Attribute.DateTime;
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
@@ -1136,6 +1258,41 @@ export interface ApiInviteInvite extends Struct.CollectionTypeSchema {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiMailsMails extends Struct.SingleTypeSchema {
|
||||||
|
collectionName: 'mailss';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'Mails';
|
||||||
|
pluralName: 'mailss';
|
||||||
|
singularName: 'mails';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
i18n: {
|
||||||
|
localized: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
locale: Schema.Attribute.String;
|
||||||
|
localizations: Schema.Attribute.Relation<'oneToMany', 'api::mails.mails'>;
|
||||||
|
onCreateUser: Schema.Attribute.Component<'mail.mail', false> &
|
||||||
|
Schema.Attribute.SetPluginOptions<{
|
||||||
|
i18n: {
|
||||||
|
localized: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiMessageMessage extends Struct.CollectionTypeSchema {
|
export interface ApiMessageMessage extends Struct.CollectionTypeSchema {
|
||||||
collectionName: 'messages';
|
collectionName: 'messages';
|
||||||
info: {
|
info: {
|
||||||
@@ -1176,6 +1333,7 @@ export interface ApiNotificationNotification
|
|||||||
extends Struct.CollectionTypeSchema {
|
extends Struct.CollectionTypeSchema {
|
||||||
collectionName: 'notifications';
|
collectionName: 'notifications';
|
||||||
info: {
|
info: {
|
||||||
|
description: '';
|
||||||
displayName: 'Notification';
|
displayName: 'Notification';
|
||||||
pluralName: 'notifications';
|
pluralName: 'notifications';
|
||||||
singularName: 'notification';
|
singularName: 'notification';
|
||||||
@@ -1187,6 +1345,7 @@ export interface ApiNotificationNotification
|
|||||||
createdAt: Schema.Attribute.DateTime;
|
createdAt: Schema.Attribute.DateTime;
|
||||||
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
|
floodId: Schema.Attribute.Integer;
|
||||||
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
localizations: Schema.Attribute.Relation<
|
localizations: Schema.Attribute.Relation<
|
||||||
'oneToMany',
|
'oneToMany',
|
||||||
@@ -1197,13 +1356,41 @@ export interface ApiNotificationNotification
|
|||||||
payload: Schema.Attribute.JSON;
|
payload: Schema.Attribute.JSON;
|
||||||
publishedAt: Schema.Attribute.DateTime;
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
read: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
|
read: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
|
||||||
source: Schema.Attribute.String;
|
source: Schema.Attribute.Enumeration<['system', 'user', 'group', 'choral']>;
|
||||||
target_user: Schema.Attribute.Relation<
|
target_user: Schema.Attribute.Relation<
|
||||||
'oneToOne',
|
'oneToOne',
|
||||||
'plugin::users-permissions.user'
|
'plugin::users-permissions.user'
|
||||||
>;
|
>;
|
||||||
|
type: Schema.Attribute.Enumeration<
|
||||||
|
['accountCreation', 'friendRequest', 'messageSent', 'postCreated']
|
||||||
|
>;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiPagePage extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'pages';
|
||||||
|
info: {
|
||||||
|
displayName: 'Page';
|
||||||
|
pluralName: 'pages';
|
||||||
|
singularName: 'page';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
content: Schema.Attribute.RichText;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<'oneToMany', 'api::page.page'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
slug: Schema.Attribute.UID<'title'>;
|
||||||
title: Schema.Attribute.String;
|
title: Schema.Attribute.String;
|
||||||
type: Schema.Attribute.Enumeration<['info', 'success', 'warning', 'error']>;
|
|
||||||
updatedAt: Schema.Attribute.DateTime;
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
@@ -1828,6 +2015,7 @@ export interface PluginUsersPermissionsUser
|
|||||||
draftAndPublish: false;
|
draftAndPublish: false;
|
||||||
};
|
};
|
||||||
attributes: {
|
attributes: {
|
||||||
|
activities: Schema.Attribute.Component<'group.activity', true>;
|
||||||
address: Schema.Attribute.Text;
|
address: Schema.Attribute.Text;
|
||||||
announcements: Schema.Attribute.Relation<
|
announcements: Schema.Attribute.Relation<
|
||||||
'oneToMany',
|
'oneToMany',
|
||||||
@@ -1837,6 +2025,7 @@ export interface PluginUsersPermissionsUser
|
|||||||
background: Schema.Attribute.Media<
|
background: Schema.Attribute.Media<
|
||||||
'images' | 'files' | 'videos' | 'audios'
|
'images' | 'files' | 'videos' | 'audios'
|
||||||
>;
|
>;
|
||||||
|
bio: Schema.Attribute.Text;
|
||||||
blocked: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
|
blocked: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
|
||||||
board: Schema.Attribute.Relation<'oneToOne', 'api::board.board'>;
|
board: Schema.Attribute.Relation<'oneToOne', 'api::board.board'>;
|
||||||
choral_memberships: Schema.Attribute.Relation<
|
choral_memberships: Schema.Attribute.Relation<
|
||||||
@@ -1861,11 +2050,13 @@ export interface PluginUsersPermissionsUser
|
|||||||
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
dob: Schema.Attribute.Date;
|
dob: Schema.Attribute.Date;
|
||||||
|
education: Schema.Attribute.String;
|
||||||
email: Schema.Attribute.Email &
|
email: Schema.Attribute.Email &
|
||||||
Schema.Attribute.Required &
|
Schema.Attribute.Required &
|
||||||
Schema.Attribute.SetMinMaxLength<{
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
minLength: 6;
|
minLength: 6;
|
||||||
}>;
|
}>;
|
||||||
|
experience: Schema.Attribute.Integer;
|
||||||
friends: Schema.Attribute.Relation<
|
friends: Schema.Attribute.Relation<
|
||||||
'oneToMany',
|
'oneToMany',
|
||||||
'plugin::users-permissions.user'
|
'plugin::users-permissions.user'
|
||||||
@@ -1878,6 +2069,7 @@ export interface PluginUsersPermissionsUser
|
|||||||
job: Schema.Attribute.Enumeration<
|
job: Schema.Attribute.Enumeration<
|
||||||
['choir_director', 'choir_addict', 'choir_master', 'choir_singer', 'none']
|
['choir_director', 'choir_addict', 'choir_master', 'choir_singer', 'none']
|
||||||
>;
|
>;
|
||||||
|
languages: Schema.Attribute.JSON;
|
||||||
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
localizations: Schema.Attribute.Relation<
|
localizations: Schema.Attribute.Relation<
|
||||||
'oneToMany',
|
'oneToMany',
|
||||||
@@ -1885,10 +2077,7 @@ export interface PluginUsersPermissionsUser
|
|||||||
> &
|
> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
name: Schema.Attribute.String;
|
name: Schema.Attribute.String;
|
||||||
nbFollowers: Schema.Attribute.Integer & Schema.Attribute.DefaultTo<0>;
|
parameter: Schema.Attribute.Component<'configuration.parameter', false>;
|
||||||
nbFollowing: Schema.Attribute.Integer & Schema.Attribute.DefaultTo<0>;
|
|
||||||
nbPosts: Schema.Attribute.Integer & Schema.Attribute.DefaultTo<0>;
|
|
||||||
nbSaved: Schema.Attribute.Integer & Schema.Attribute.DefaultTo<0>;
|
|
||||||
password: Schema.Attribute.Password &
|
password: Schema.Attribute.Password &
|
||||||
Schema.Attribute.Private &
|
Schema.Attribute.Private &
|
||||||
Schema.Attribute.SetMinMaxLength<{
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
@@ -1899,6 +2088,7 @@ export interface PluginUsersPermissionsUser
|
|||||||
'oneToMany',
|
'oneToMany',
|
||||||
'api::post-ownership.post-ownership'
|
'api::post-ownership.post-ownership'
|
||||||
>;
|
>;
|
||||||
|
privacy: Schema.Attribute.Component<'configuration.privacy', false>;
|
||||||
provider: Schema.Attribute.String;
|
provider: Schema.Attribute.String;
|
||||||
publishedAt: Schema.Attribute.DateTime;
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
related_contacts: Schema.Attribute.Relation<
|
related_contacts: Schema.Attribute.Relation<
|
||||||
@@ -1912,6 +2102,7 @@ export interface PluginUsersPermissionsUser
|
|||||||
>;
|
>;
|
||||||
saved_posts: Schema.Attribute.Relation<'oneToMany', 'api::post.post'>;
|
saved_posts: Schema.Attribute.Relation<'oneToMany', 'api::post.post'>;
|
||||||
surname: Schema.Attribute.String;
|
surname: Schema.Attribute.String;
|
||||||
|
tags: Schema.Attribute.JSON;
|
||||||
updatedAt: Schema.Attribute.DateTime;
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
@@ -1943,6 +2134,9 @@ declare module '@strapi/strapi' {
|
|||||||
'api::board-list.board-list': ApiBoardListBoardList;
|
'api::board-list.board-list': ApiBoardListBoardList;
|
||||||
'api::board.board': ApiBoardBoard;
|
'api::board.board': ApiBoardBoard;
|
||||||
'api::channel.channel': ApiChannelChannel;
|
'api::channel.channel': ApiChannelChannel;
|
||||||
|
'api::chat-conversation-member.chat-conversation-member': ApiChatConversationMemberChatConversationMember;
|
||||||
|
'api::chat-conversation.chat-conversation': ApiChatConversationChatConversation;
|
||||||
|
'api::chat-message.chat-message': ApiChatMessageChatMessage;
|
||||||
'api::choral-membership.choral-membership': ApiChoralMembershipChoralMembership;
|
'api::choral-membership.choral-membership': ApiChoralMembershipChoralMembership;
|
||||||
'api::choral-permission.choral-permission': ApiChoralPermissionChoralPermission;
|
'api::choral-permission.choral-permission': ApiChoralPermissionChoralPermission;
|
||||||
'api::choral.choral': ApiChoralChoral;
|
'api::choral.choral': ApiChoralChoral;
|
||||||
@@ -1956,8 +2150,10 @@ declare module '@strapi/strapi' {
|
|||||||
'api::group-membership.group-membership': ApiGroupMembershipGroupMembership;
|
'api::group-membership.group-membership': ApiGroupMembershipGroupMembership;
|
||||||
'api::group.group': ApiGroupGroup;
|
'api::group.group': ApiGroupGroup;
|
||||||
'api::invite.invite': ApiInviteInvite;
|
'api::invite.invite': ApiInviteInvite;
|
||||||
|
'api::mails.mails': ApiMailsMails;
|
||||||
'api::message.message': ApiMessageMessage;
|
'api::message.message': ApiMessageMessage;
|
||||||
'api::notification.notification': ApiNotificationNotification;
|
'api::notification.notification': ApiNotificationNotification;
|
||||||
|
'api::page.page': ApiPagePage;
|
||||||
'api::permissions-template.permissions-template': ApiPermissionsTemplatePermissionsTemplate;
|
'api::permissions-template.permissions-template': ApiPermissionsTemplatePermissionsTemplate;
|
||||||
'api::post-ownership.post-ownership': ApiPostOwnershipPostOwnership;
|
'api::post-ownership.post-ownership': ApiPostOwnershipPostOwnership;
|
||||||
'api::post.post': ApiPostPost;
|
'api::post.post': ApiPostPost;
|
||||||
|
|||||||
Reference in New Issue
Block a user