Compare commits

..

24 Commits

Author SHA1 Message Date
e18a3fa092 0.12.19 : better notifications
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 7m2s
2026-01-19 00:07:21 +01:00
0bb06e0e2d 0.12.18 : add create message, conversation
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 7m23s
2026-01-16 13:23:27 +01:00
1186463e11 0.12.17 : change ad feature
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 7m25s
2026-01-15 09:38:47 +01:00
34a4f594ce 0.12.16 : add custom endpoints for contacts
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 11m26s
2025-12-12 19:29:10 +01:00
e9505524bf 0.12.15 : add activity as a service
Some checks failed
Build release Docker image / Build Docker Images (push) Failing after 8s
2025-12-11 17:32:27 +01:00
b33e04d9f4 0.12.14 : add notification on friend invite
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 7m0s
2025-12-11 15:36:43 +01:00
21b9302d7f 0.12.13 : add page mdx
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 6m59s
2025-12-09 23:24:50 +01:00
460560bb89 0.12.12 : add mail templates as single type
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 6m55s
2025-12-02 23:06:50 +01:00
ddf2ced098 0.12.11 : fix template again
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 7m22s
2025-11-30 21:43:22 +01:00
5428ceb17b 0.12.10 : fix email template
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 12m55s
2025-11-30 21:21:55 +01:00
7df45f5c1c 0.12.9 : fix send email validation
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 12m51s
2025-11-30 20:51:56 +01:00
a928acb7d2 0.12.8 : add activity and notification after user creation
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 6m47s
2025-11-21 13:50:05 +01:00
4815157c3a Test
Some checks failed
Build release Docker image / Build Docker Images (push) Failing after 4m22s
2025-11-20 01:42:45 +01:00
ee8efecdec Try to debug build 2025-11-20 01:38:16 +01:00
95ce447496 0.12.5
Some checks failed
Build release Docker image / Build Docker Images (push) Failing after 2m4s
2025-11-16 18:09:48 +01:00
fa993f75b4 0.12.5 : add message 2025-11-16 18:09:37 +01:00
c33debf153 0.12.4 : improve find user 2025-11-15 18:13:24 +01:00
b2e1ab0361 0.12.3 : change updateMe to handle components 2025-11-13 23:01:28 +01:00
1e642d194e 0.12.2 : fix update user 2025-11-12 21:12:29 +01:00
b8a2bc0aef 0.12.1 : update user 2025-11-12 15:49:52 +01:00
d64c647865 0.12.0 : modify user 2025-11-11 19:09:04 +01:00
2bc35b639b 0.11.21 : change on event and group 2025-11-11 02:04:43 +01:00
14e013bac1 0.11.20 : modify event controller for group owner 2025-11-10 23:10:23 +01:00
539c9cd104 0.11.19 : add simple notification 2025-11-10 14:53:54 +01:00
57 changed files with 20031 additions and 525 deletions

View File

@@ -7,7 +7,7 @@ env:
DOCKER_REGISTRY_URL: git.harmonylab.ovh
DOCKER_REGISTRY_ORG: harmony
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
jobs:

View File

@@ -9,7 +9,6 @@ export default () => ({
user: "admin@harmonychoral.com",
pass: "Apslxnap12bn23",
},
// ... any custom nodemailer options
},
settings: {
defaultFrom: "admin@harmonychoral.com",
@@ -17,31 +16,29 @@ export default () => ({
},
},
},
upload: {
config: {
provider: "aws-s3",
providerOptions: {
//baseUrl: "http://192.168.0.211:9000/harmony",
baseUrl: "https://container.harmonylab.ovh/harmony",
s3Options: {
credentials: {
accessKeyId: "admin",
secretAccessKey: "Apslxnap12bn23",
},
//endpoint: "http://192.168.0.211:9000",
endpoint: "https://container.harmonylab.ovh",
region: "eu-west-3",
forcePathStyle: true,
params: {
Bucket: "harmony",
},
credentials: {
accessKeyId: "admin",
secretAccessKey: "Apslxnap12bn23",
},
endpoint: "https://container.harmonylab.ovh",
forcePathStyle: true,
region: "eu-west-3",
params: {
Bucket: "harmony",
},
},
},
},
"strapi-v5-plugin-populate-deep": {
config: {
defaultDepth: 3, // Default is 5
defaultDepth: 3,
skipCreatorFields: false,
},
},

44
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "harmony-back",
"version": "0.11.6",
"version": "0.12.16",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "harmony-back",
"version": "0.11.6",
"version": "0.12.16",
"dependencies": {
"@strapi/plugin-cloud": "5.8.1",
"@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",
"integrity": "sha512-7+I8RWURGfzvChyNQSyj5/tKrqRbzRl7H+BnTOf/4Vsw1nFOi5ROhlhD4X/Y0QCTacxnaoNcIrqnY7uGGvVRzw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@aws-crypto/sha256-browser": "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",
"integrity": "sha512-KQG97B7LvTtTiGmjlrG1LRAY8wUvCQzrmZVV5bjrJ/1oXAU7DITYwVbSJeX9NWg6hDuSk0VE3MFwIXS2SvfLIA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@aws-crypto/sha256-browser": "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",
"integrity": "sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/state": "^6.5.0",
"style-mod": "^4.1.0",
@@ -6051,6 +6054,7 @@
"resolved": "https://registry.npmjs.org/@strapi/admin/-/admin-5.8.1.tgz",
"integrity": "sha512-Rb6MMOhyjzYJktG4T8CheAIaMHwc5zqMBRz7oSlwgR7Y8VJ7YiN+1sn4fGRD5r4K0+7MLK2CbsYy3piu42GZdw==",
"license": "SEE LICENSE IN LICENSE",
"peer": true,
"dependencies": {
"@casl/ability": "6.5.0",
"@internationalized/date": "3.5.4",
@@ -6180,6 +6184,7 @@
"resolved": "https://registry.npmjs.org/@strapi/content-manager/-/content-manager-5.8.1.tgz",
"integrity": "sha512-aaJKJ3sLDerFvztEOs6sfcvAu7b3bDuDbj5Of0v4iEeGY0vWqFm2TKA6EeTSqKWsVDsM/BFmmxDVsgYqddqxiA==",
"license": "SEE LICENSE IN LICENSE",
"peer": true,
"dependencies": {
"@radix-ui/react-toolbar": "1.0.4",
"@reduxjs/toolkit": "1.9.7",
@@ -6415,6 +6420,7 @@
"resolved": "https://registry.npmjs.org/@strapi/data-transfer/-/data-transfer-5.8.1.tgz",
"integrity": "sha512-uIyYhoJOldzkAyeJi2tJ5JzWfTnHiVX79trLKZcZjyL357ipG3CUVhyoFOmy0xpNlBplq0v1RUGJ/GZBpNpkVA==",
"license": "SEE LICENSE IN LICENSE",
"peer": true,
"dependencies": {
"@strapi/logger": "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",
"integrity": "sha512-6kamFHcsoFpffp96HmFGgpM8FDezwb86sJ4c3Ydp7+eTgtEsHd8yLekAQhpaWXd2ZsHwRDbLLR0Tu1orgvRVYg==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "^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",
"integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -7791,6 +7799,7 @@
"resolved": "https://registry.npmjs.org/@strapi/icons/-/icons-2.0.0-rc.23.tgz",
"integrity": "sha512-sJ7iQ8kZ28z3mTkDm/gnsWIQljK3w0UaOk2irO77iSmbh+uR3W9gDF5CP/4Z+KDUqnjDke2kaOIPRI67etvi9A==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "^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",
"integrity": "sha512-i1l+CaLNjHdZ6o0GLwlK2eBRmQ/hQwC3Tl4aYK7IsRfbyeT0+1ScEtJF5WIn51ENmvnD+BCual6oz5D3T3g02A==",
"license": "SEE LICENSE IN LICENSE",
"peer": true,
"dependencies": {
"@pmmmwh/react-refresh-webpack-plugin": "0.5.15",
"@strapi/admin": "5.8.1",
@@ -8648,6 +8658,7 @@
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.1.0.tgz",
"integrity": "sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -9089,6 +9100,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -9099,6 +9111,7 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
"integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -9507,6 +9520,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz",
"integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"json-schema-traverse": "^1.0.0",
@@ -10155,6 +10169,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
@@ -11613,6 +11628,7 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.21.0"
},
@@ -11961,7 +11977,8 @@
"version": "0.0.1413902",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1413902.tgz",
"integrity": "sha512-yRtvFD8Oyk7C9Os3GmnFZLu53yAfsnyw1s+mLmHHUK0GQEc9zthHWvS1r67Zqzm5t7v56PILHIVZ7kmFMaL2yQ==",
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/dezalgo": {
"version": "1.0.4",
@@ -12366,6 +12383,7 @@
"integrity": "sha512-Kgq0/ZsAPzKrbOjCQcjoSmPoWhlcVnGAUo7jvaLHoxW1Drto0KGkR1xBNg2Cp43b9ImvxmPEJZ9xkfcnqPsfBw==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -13381,6 +13399,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -15567,6 +15586,7 @@
"resolved": "https://registry.npmjs.org/koa/-/koa-2.15.2.tgz",
"integrity": "sha512-MXTeZH3M6AJ8ukW2QZ8wqO3Dcdfh2WRRmjCBkEP+NhKNCiqlO5RDqHmSnsyNrbRJrdjyvIGSJho4vQiWgQJSVA==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^1.3.5",
"cache-content-type": "^1.0.0",
@@ -18830,6 +18850,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
@@ -19531,6 +19552,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -19582,6 +19604,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -19729,6 +19752,7 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
"integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -19800,6 +19824,7 @@
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.2.tgz",
"integrity": "sha512-O81EWqNJWqvlN/a7eTudAdQm0TbI7hw+WIi7OwwMcTn5JMyZ0ibTFNGz+t+Lju0df4LcqowCegcrK22lB1q9Kw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@remix-run/router": "1.21.1",
"react-router": "6.28.2"
@@ -20040,6 +20065,7 @@
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.9.2"
}
@@ -20601,6 +20627,7 @@
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
@@ -21023,6 +21050,7 @@
"resolved": "https://registry.npmjs.org/slate/-/slate-0.94.1.tgz",
"integrity": "sha512-GH/yizXr1ceBoZ9P9uebIaHe3dC/g6Plpf9nlUwnvoyf6V1UOYrRwkabtOCd3ZfIGxomY4P7lfgLr7FPH8/BKA==",
"license": "MIT",
"peer": true,
"dependencies": {
"immer": "^9.0.6",
"is-plain-object": "^5.0.0",
@@ -21534,6 +21562,7 @@
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.14.tgz",
"integrity": "sha512-KtfwhU5jw7UoxdM0g6XU9VZQFV4do+KrM8idiVCH5h4v49W+3p3yMe0icYwJgZQZepa5DbH04Qv8P0/RdcLcgg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@emotion/is-prop-valid": "1.2.2",
"@emotion/unitless": "0.8.1",
@@ -21771,6 +21800,7 @@
"resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
"integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==",
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
@@ -22050,6 +22080,7 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"license": "(MIT OR CC0-1.0)",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -22090,6 +22121,7 @@
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.10.tgz",
"integrity": "sha512-v10rtOFojrjW9og3T+6wAKeJaGMuojU87DXGZ33sfs+554wgPTRG+s07Ag1BjPZI85Y5QPVouPI63JQ6fcQM5w==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"lunr": "^2.3.9",
"marked": "^4.3.0",
@@ -22121,6 +22153,7 @@
"resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.17.1.tgz",
"integrity": "sha512-QzdU3fj0Kzw2XSdoL15ExLASt2WPqD7FbLeaqwT70+XjKyTshBnUlQA5nNREO1C2P8Uen0CDjsBLMsCQ+zd0lw==",
"license": "MIT",
"peer": true,
"dependencies": {
"handlebars": "^4.7.7"
},
@@ -22133,6 +22166,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -22476,6 +22510,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.14.tgz",
"integrity": "sha512-TFQLuwWLPms+NBNlh0D9LZQ+HXW471COABxw/9TEUBrjuHMo9BrYBPrN/SYAwIuVL+rLerycxiLT41t4f5MZpA==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.20.1",
"postcss": "^8.4.38",
@@ -22977,6 +23012,7 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz",
"integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6",
@@ -23107,6 +23143,7 @@
"resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.26.1.tgz",
"integrity": "sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A==",
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-html-community": "0.0.8",
"html-entities": "^2.1.0",
@@ -23137,6 +23174,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "harmony-back",
"version": "0.11.18",
"version": "0.12.19",
"private": true,
"description": "A Strapi application",
"scripts": {

166
public/confirmation.html Normal file
View 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, &quot;Segoe UI&quot;, Roboto,
&quot;Helvetica Neue&quot;, 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 quune é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 nes pas à lorigine 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 daide ? Réponds directement à cet e-mail ou consulte notre
<a href="https://www.choralsync.com/help" style="color: #0369a1"
>centre daide</a
>.
</p>
</td>
</tr>
</table>

126
report.md Normal file
View 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
View 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

View File

@@ -42,11 +42,6 @@
"applies": {
"type": "integer"
},
"location": {
"type": "component",
"repeatable": false,
"component": "address.full-address"
},
"contactname": {
"type": "string"
},
@@ -58,6 +53,19 @@
},
"featured": {
"type": "boolean"
},
"location": {
"type": "text"
},
"medias": {
"allowedTypes": [
"images",
"files",
"videos",
"audios"
],
"type": "media",
"multiple": true
}
}
}

View File

@@ -2,6 +2,47 @@
* 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;
},
}));

View File

@@ -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"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* chat-conversation-member controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::chat-conversation-member.chat-conversation-member');

View File

@@ -0,0 +1,7 @@
/**
* chat-conversation-member router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::chat-conversation-member.chat-conversation-member');

View File

@@ -0,0 +1,7 @@
/**
* chat-conversation-member service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::chat-conversation-member.chat-conversation-member');

View File

@@ -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"
}
}
}

View 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);
}
},
})
);

View File

@@ -0,0 +1,7 @@
/**
* chat-conversation router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::chat-conversation.chat-conversation');

View 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;
},
})
);

View 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
}
}
}

View 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}`);
}
},
})
);

View File

@@ -0,0 +1,7 @@
/**
* chat-message router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::chat-message.chat-message');

View 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;
},
})
);

View 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
);
}
}
}
},
};

View File

@@ -7,6 +7,78 @@ import { factories } from "@strapi/strapi";
export default factories.createCoreController(
"api::contact.contact",
({ 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) {
// Log de la requête entrante
console.log("GET /contacts - Requête reçue:", {
@@ -72,8 +144,8 @@ export default factories.createCoreController(
.documents("api::contact.contact")
.findFirst({
filters: {
owner: owner,
user: user,
owner: { id: owner },
user: { id: user },
},
});
@@ -148,10 +220,10 @@ export default factories.createCoreController(
filters: {
$or: [
{
user: id,
user: { id: id },
},
{
owner: id,
owner: { id: id },
},
],
},
@@ -192,5 +264,90 @@ export default factories.createCoreController(
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;
}
},
})
);

View File

@@ -0,0 +1,58 @@
// path: src/api/contact/document-middleware.ts
export default {
/**
* Déclenché à la création dun 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 à lutilisateur ${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"
},
};

View 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
},
},
],
};

View File

@@ -23,8 +23,10 @@ export default ({ strapi }: { strapi: Core.Strapi }) =>
}
try {
const groupId = ctx.request.body.data.group;
// Format tags from array of strings to array of component objects
const data = ctx.request.body.data;
delete data.group;
if (data.tags && Array.isArray(data.tags)) {
data.tags = data.tags
.filter((tag: any) => {
@@ -51,14 +53,17 @@ export default ({ strapi }: { strapi: Core.Strapi }) =>
}
// 2. Create the owner relationship
const contextType = groupId && groupId !== 0 ? "group" : "user";
const contextId = groupId && groupId !== 0 ? groupId : userId;
const ownerRelationship = await strapi.db
.query("api::event-relationship.event-relationship")
.create({
data: {
author: userId,
event: event.id,
contextType: "user",
contextId: userId,
contextType,
contextId,
relation: "owner",
metas: {
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}`
);
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);
} catch (error) {
strapi.log.error("Error in create event controller:", error);

View File

@@ -4,7 +4,7 @@ import type { Context } from "koa";
export default ({ strapi }: { strapi: Core.Strapi }) =>
async function feed(ctx: Context) {
const userId = ctx.state.user?.id;
const { contextId, contextType } = ctx.query;
if (!userId) return ctx.badRequest("userId is required");
// 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 }
);
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
start = Math.max(0, parseInt(String(_start)) || 0); // Minimum 0
} catch (error) {
@@ -100,50 +105,44 @@ export default ({ strapi }: { strapi: Core.Strapi }) =>
.map((membership) => membership.group.id)
.filter((groupId) => !groupIds.includes(groupId)); // Exclure mes propres groupes
// Construire la condition where en fonction des paramètres contextId et contextType
const whereCondition =
contextId && contextType
? {
relation: "owner",
contextType,
contextId: parseInt(String(contextId)),
}
: {
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" },
],
};
// 4⃣ Récupérer le count total pour la pagination
const totalCount = await strapi.db
.query("api::event-relationship.event-relationship")
.count({
where: {
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" },
],
},
where: whereCondition,
});
// 4⃣ Récupérer le feed avec pagination
const feed = await strapi.db
.query("api::event-relationship.event-relationship")
.findMany({
where: {
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" },
],
},
where: whereCondition,
...queryParams,
});

View File

@@ -31,7 +31,9 @@
"admin",
"owner",
"follow",
"pending"
"pending",
"invited",
"rejected"
]
}
}

View File

@@ -83,6 +83,25 @@ export default factories.createCoreController(
);
membershipMeta.success = true;
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) {
strapi.log.error(
@@ -97,5 +116,98 @@ export default factories.createCoreController(
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,
};
},
})
);

View File

@@ -0,0 +1,13 @@
/**
* Custom post routes
*/
export default {
routes: [
{
method: "POST",
path: "/group/:id/invite",
handler: "group.invite",
},
],
};

View File

@@ -4,4 +4,25 @@
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 },
});
},
}));

View 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"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* mails controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::mails.mails');

View File

@@ -0,0 +1,7 @@
/**
* mails router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::mails.mails');

View File

@@ -0,0 +1,7 @@
/**
* mails service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::mails.mails');

View File

@@ -4,26 +4,24 @@
"info": {
"singularName": "notification",
"pluralName": "notifications",
"displayName": "Notification"
"displayName": "Notification",
"description": ""
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"title": {
"type": "string"
},
"message": {
"type": "string"
},
"type": {
"type": "enumeration",
"enum": [
"info",
"success",
"warning",
"error"
"accountCreation",
"friendRequest",
"messageSent",
"postCreated"
]
},
"target_user": {
@@ -35,11 +33,20 @@
"type": "boolean",
"default": false
},
"source": {
"type": "string"
},
"payload": {
"type": "json"
},
"source": {
"type": "enumeration",
"enum": [
"system",
"user",
"group",
"choral"
]
},
"floodId": {
"type": "integer"
}
}
}

View File

@@ -4,6 +4,7 @@ interface NotificationPayload {
type?: "info" | "success" | "warning" | "error";
target_user?: number;
source?: string;
floodId?: number;
payload?: Record<string, any>;
}
@@ -14,12 +15,21 @@ interface NotificationEntity {
type: string;
target_user?: number;
source?: string;
floodId?: number;
payload?: Record<string, any>;
read: boolean;
createdAt: string;
updatedAt: string;
}
interface ActivityPayload {
userId: number;
activityMessage: string;
activityUser: string;
activityType: string;
activityAvatar?: number;
}
/**
* notification service
*/
@@ -30,22 +40,57 @@ export default factories.createCoreService(
({ strapi }: { strapi }) => ({
//Custom service for add notification.
async createNotification({
title,
message,
type = "info",
target_user,
source,
floodId,
payload,
}: 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(
"api::notification.notification",
{ data }
);
strapi.log.info(`🔔 Notification créée: ${title}`);
strapi.log.info(`🔔 Notification créée: ${message}`);
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}`
);
},
})
);

View 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"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* page controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::page.page');

View File

@@ -0,0 +1,7 @@
/**
* page router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::page.page');

View File

@@ -0,0 +1,7 @@
/**
* page service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::page.page');

View File

@@ -36,6 +36,7 @@ export default factories.createCoreController(
delete data.author;
delete data.isPublic;
delete data.comments;
ctx.request.body = { data };
const result = await super.create(ctx);

View 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
}
}
}

View 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
}
}
}

View File

@@ -14,6 +14,28 @@
},
"activityDate": {
"type": "datetime"
},
"activityAvatar": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images",
"files",
"videos",
"audios"
]
},
"activityType": {
"type": "enumeration",
"enum": [
"user",
"group",
"choral",
"message",
"post",
"contact"
]
}
}
}

View File

@@ -0,0 +1,17 @@
{
"collectionName": "components_mail_mail",
"info": {
"displayName": "mail",
"icon": "paperPlane",
"description": ""
},
"options": {},
"attributes": {
"subject": {
"type": "string"
},
"message": {
"type": "text"
}
}
}

View File

@@ -0,0 +1,17 @@
{
"collectionName": "components_user_languages",
"info": {
"displayName": "language",
"icon": "feather",
"description": ""
},
"options": {},
"attributes": {
"language": {
"type": "string"
},
"level": {
"type": "integer"
}
}
}

View 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, &quot;Segoe UI&quot;, Roboto,
&quot;Helvetica Neue&quot;, 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 quune é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 nes pas à lorigine 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 daide ? Réponds directement à cet e-mail ou consulte notre
<a href="https://www.choralsync.com/help" style="color: #0369a1"
>centre daide</a
>.
</p>
</td>
</tr>
</table>

View 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, &quot;Segoe UI&quot;, Roboto,
&quot;Helvetica Neue&quot;, 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 quune é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 nes pas à lorigine 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 daide ? Réponds directement à cet e-mail ou consulte notre
<a href="https://www.choralsync.com/help" style="color: #0369a1"
>centre daide</a
>.
</p>
</td>
</tr>
</table>

View File

@@ -73,22 +73,6 @@
"images"
]
},
"nbFollowers": {
"type": "integer",
"default": 0
},
"nbFollowing": {
"type": "integer",
"default": 0
},
"nbPosts": {
"type": "integer",
"default": 0
},
"nbSaved": {
"type": "integer",
"default": 0
},
"choralOwner": {
"type": "relation",
"relation": "oneToOne",
@@ -224,6 +208,36 @@
"relation": "oneToMany",
"target": "api::post-ownership.post-ownership",
"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"
}
}
}

View File

@@ -1,7 +1,88 @@
"use strict";
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) => {
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) => {
return strapi.plugin("users-permissions").service(name);
@@ -33,12 +114,10 @@ module.exports = (plugin) => {
throw new Error("No access_token.");
}
// Get the profile.
const profile = await getProfile(provider, query);
const email = lod.toLower(profile.email);
// We need at least the mail.
if (!email) {
throw new Error("Email was not available.");
}
@@ -49,9 +128,13 @@ module.exports = (plugin) => {
where: { email },
});
const advancedSettings = await strapi
const advancedSettings = (await strapi
.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 });
if (lod.isEmpty(user)) {
@@ -70,15 +153,13 @@ module.exports = (plugin) => {
throw new Error("Email is already taken.");
}
// Retrieve default role.
const defaultRole = await strapi.db
.query("plugin::users-permissions.role")
.findOne({ where: { type: advancedSettings.default_role } });
// Create the new user.
const newUser = {
...profile,
email, // overwrite with lowercased email
email,
provider,
role: defaultRole.id,
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 key = `${label}Image`;
if (ctx.request.files[key]) {
@@ -122,7 +440,6 @@ module.exports = (plugin) => {
plugin.controllers.user.updateMe = async (ctx) => {
const user = ctx.state.user;
// User has to be logged in to update themselves
if (!user) {
return ctx.unauthorized();
}
@@ -139,9 +456,31 @@ module.exports = (plugin) => {
"voice",
"job",
"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) {
const userWithSameUsername = await strapi
.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) {
const userWithSameEmail = await strapi
.query("plugin::users-permissions.user")
@@ -167,12 +505,59 @@ module.exports = (plugin) => {
ctx.request.body = newData;
ctx.params = { id: user.id };
const avatarId = await uploadImage(ctx, "avatar", newData.username);
if (avatarId != 0) ctx.request.body.avatar = avatarId;
const backgroundId = await uploadImage(ctx, "background", newData.username);
if (backgroundId != 0) ctx.request.body.background = backgroundId;
const keysExcludingParameterAndPrivacy = Object.keys(newData).filter(
(key) => key !== "parameter" && key !== "privacy"
);
return plugin.controllers.user.update(ctx);
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);
if (avatarId != 0) ctx.request.body.avatar = avatarId;
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({

73
stack docker/gitea.yaml Normal file
View 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

View 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 lIP 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 dappairage)
- HAMH_LOG_LEVEL=debug
# Port HTTP de linterface 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
View 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:

View File

@@ -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 {
collectionName: 'components_group_activities';
info: {
@@ -26,12 +59,31 @@ export interface GroupActivity extends Struct.ComponentSchema {
displayName: 'Activity';
};
attributes: {
activityAvatar: Schema.Attribute.Media<
'images' | 'files' | 'videos' | 'audios'
>;
activityDate: Schema.Attribute.DateTime;
activityMessage: Schema.Attribute.String;
activityType: Schema.Attribute.Enumeration<
['user', 'group', 'choral', 'message', 'post', 'contact']
>;
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 {
collectionName: 'components_social_tags';
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 {
collectionName: 'components_user_permissions';
info: {
@@ -63,8 +128,12 @@ declare module '@strapi/strapi' {
export module Public {
export interface ComponentSchemas {
'address.full-address': AddressFullAddress;
'configuration.parameter': ConfigurationParameter;
'configuration.privacy': ConfigurationPrivacy;
'group.activity': GroupActivity;
'mail.mail': MailMail;
'social.tags': SocialTags;
'user.language': UserLanguage;
'user.permissions': UserPermissions;
}
}

View File

@@ -400,7 +400,11 @@ export interface ApiAdAd extends Struct.CollectionTypeSchema {
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<'oneToMany', 'api::ad.ad'> &
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;
title: Schema.Attribute.String;
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
extends Struct.CollectionTypeSchema {
collectionName: 'choral_memberships';
@@ -1047,7 +1169,7 @@ export interface ApiGroupMembershipGroupMembership
Schema.Attribute.Private;
publishedAt: Schema.Attribute.DateTime;
role: Schema.Attribute.Enumeration<
['member', 'admin', 'owner', 'follow', 'pending']
['member', 'admin', 'owner', 'follow', 'pending', 'invited', 'rejected']
>;
updatedAt: Schema.Attribute.DateTime;
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 {
collectionName: 'messages';
info: {
@@ -1176,6 +1333,7 @@ export interface ApiNotificationNotification
extends Struct.CollectionTypeSchema {
collectionName: 'notifications';
info: {
description: '';
displayName: 'Notification';
pluralName: 'notifications';
singularName: 'notification';
@@ -1187,6 +1345,7 @@ export interface ApiNotificationNotification
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
floodId: Schema.Attribute.Integer;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<
'oneToMany',
@@ -1197,13 +1356,41 @@ export interface ApiNotificationNotification
payload: Schema.Attribute.JSON;
publishedAt: Schema.Attribute.DateTime;
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<
'oneToOne',
'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;
type: Schema.Attribute.Enumeration<['info', 'success', 'warning', 'error']>;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
@@ -1828,6 +2015,7 @@ export interface PluginUsersPermissionsUser
draftAndPublish: false;
};
attributes: {
activities: Schema.Attribute.Component<'group.activity', true>;
address: Schema.Attribute.Text;
announcements: Schema.Attribute.Relation<
'oneToMany',
@@ -1837,6 +2025,7 @@ export interface PluginUsersPermissionsUser
background: Schema.Attribute.Media<
'images' | 'files' | 'videos' | 'audios'
>;
bio: Schema.Attribute.Text;
blocked: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
board: Schema.Attribute.Relation<'oneToOne', 'api::board.board'>;
choral_memberships: Schema.Attribute.Relation<
@@ -1861,11 +2050,13 @@ export interface PluginUsersPermissionsUser
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
dob: Schema.Attribute.Date;
education: Schema.Attribute.String;
email: Schema.Attribute.Email &
Schema.Attribute.Required &
Schema.Attribute.SetMinMaxLength<{
minLength: 6;
}>;
experience: Schema.Attribute.Integer;
friends: Schema.Attribute.Relation<
'oneToMany',
'plugin::users-permissions.user'
@@ -1878,6 +2069,7 @@ export interface PluginUsersPermissionsUser
job: Schema.Attribute.Enumeration<
['choir_director', 'choir_addict', 'choir_master', 'choir_singer', 'none']
>;
languages: Schema.Attribute.JSON;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<
'oneToMany',
@@ -1885,10 +2077,7 @@ export interface PluginUsersPermissionsUser
> &
Schema.Attribute.Private;
name: Schema.Attribute.String;
nbFollowers: Schema.Attribute.Integer & Schema.Attribute.DefaultTo<0>;
nbFollowing: Schema.Attribute.Integer & Schema.Attribute.DefaultTo<0>;
nbPosts: Schema.Attribute.Integer & Schema.Attribute.DefaultTo<0>;
nbSaved: Schema.Attribute.Integer & Schema.Attribute.DefaultTo<0>;
parameter: Schema.Attribute.Component<'configuration.parameter', false>;
password: Schema.Attribute.Password &
Schema.Attribute.Private &
Schema.Attribute.SetMinMaxLength<{
@@ -1899,6 +2088,7 @@ export interface PluginUsersPermissionsUser
'oneToMany',
'api::post-ownership.post-ownership'
>;
privacy: Schema.Attribute.Component<'configuration.privacy', false>;
provider: Schema.Attribute.String;
publishedAt: Schema.Attribute.DateTime;
related_contacts: Schema.Attribute.Relation<
@@ -1912,6 +2102,7 @@ export interface PluginUsersPermissionsUser
>;
saved_posts: Schema.Attribute.Relation<'oneToMany', 'api::post.post'>;
surname: Schema.Attribute.String;
tags: Schema.Attribute.JSON;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
@@ -1943,6 +2134,9 @@ declare module '@strapi/strapi' {
'api::board-list.board-list': ApiBoardListBoardList;
'api::board.board': ApiBoardBoard;
'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-permission.choral-permission': ApiChoralPermissionChoralPermission;
'api::choral.choral': ApiChoralChoral;
@@ -1956,8 +2150,10 @@ declare module '@strapi/strapi' {
'api::group-membership.group-membership': ApiGroupMembershipGroupMembership;
'api::group.group': ApiGroupGroup;
'api::invite.invite': ApiInviteInvite;
'api::mails.mails': ApiMailsMails;
'api::message.message': ApiMessageMessage;
'api::notification.notification': ApiNotificationNotification;
'api::page.page': ApiPagePage;
'api::permissions-template.permissions-template': ApiPermissionsTemplatePermissionsTemplate;
'api::post-ownership.post-ownership': ApiPostOwnershipPostOwnership;
'api::post.post': ApiPostPost;