diff --git a/config/plugins.ts b/config/plugins.ts index 586b049..821679b 100644 --- a/config/plugins.ts +++ b/config/plugins.ts @@ -3,16 +3,18 @@ export default () => ({ config: { provider: "nodemailer", providerOptions: { - host: "mail.harmonychoral.com", - port: 465, + host: "smtp.zeptomail.eu", + port: 587, auth: { - user: "admin@harmonychoral.com", - pass: "Apslxnap12bn23", + user: "emailapikey", + pass: "yA6KbHsJ4lrywWtTFUc+0pSC94lm/aE/2nzks3i2fpZ1LYXp3qE71RBvd4O4c2CLjdfT5a9UbIkVJoCwvIpbfpczPIBXJpTGTuv4P2uV48xh8ciEYNYjhJivALIWFqVOeBsnDyo4QfEjWA==", }, + debug: true, + logger: true, }, settings: { - defaultFrom: "admin@harmonychoral.com", - defaultReplyTo: "admin@harmonychoral.com", + defaultFrom: "ChoralSync ", + defaultReplyTo: "contact@choralsync.com", }, }, }, diff --git a/package.json b/package.json index d287a54..1e76cd1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "harmony-back", - "version": "0.13.7", + "version": "0.13.8", "private": true, "description": "A Strapi application", "scripts": { diff --git a/src/api/choral-membership/content-types/choral-membership/schema.json b/src/api/choral-membership/content-types/choral-membership/schema.json index 3e2ac92..c0a8665 100644 --- a/src/api/choral-membership/content-types/choral-membership/schema.json +++ b/src/api/choral-membership/content-types/choral-membership/schema.json @@ -26,11 +26,7 @@ }, "role": { "type": "enumeration", - "enum": [ - "member", - "admin", - "owner" - ] + "enum": ["member", "admin", "owner"] }, "permissions": { "type": "component", @@ -51,6 +47,14 @@ "pending_admin_approval", "decline" ] + }, + "invite_email": { + "type": "email", + "private": true + }, + "invite_token": { + "type": "string", + "private": true } } } diff --git a/src/api/choral-membership/controllers/choral-membership.ts b/src/api/choral-membership/controllers/choral-membership.ts index ad3a390..632b9f3 100644 --- a/src/api/choral-membership/controllers/choral-membership.ts +++ b/src/api/choral-membership/controllers/choral-membership.ts @@ -1,7 +1,97 @@ -/** - * choral-membership controller - */ +import { factories } from "@strapi/strapi"; +import crypto from "crypto"; -import { factories } from '@strapi/strapi' +export default factories.createCoreController( + "api::choral-membership.choral-membership", + ({ strapi }) => ({ + async create(ctx) { + const { data } = ctx.request.body; + const inviter = ctx.state.user; -export default factories.createCoreController('api::choral-membership.choral-membership'); + // 1. Déterminer le type d'invitation + const isExternalInvite = !!data.invite_email && !data.user; + const isInternalInvite = !!data.user; + + let token = null; + let targetEmail = null; + + // 2. Configuration commune + // On force le statut "en attente" peut importe le type d'invité + ctx.request.body.data.state = "pending_user_approval"; + if (!data.role) { + ctx.request.body.data.role = "member"; + } + + // 3. Traitement spécifique selon le cas + if (isExternalInvite) { + // Cas A : Utilisateur externe (Token requis) + token = crypto.randomBytes(32).toString("hex"); + ctx.request.body.data.invite_token = token; + ctx.request.body.data.invite_email = data.invite_email.toLowerCase(); + targetEmail = data.invite_email.toLowerCase(); + } else if (isInternalInvite) { + // Cas B : Utilisateur existant (Pas de token) + // On s'assure de nettoyer les champs d'invitation externe au cas où le front les enverrait par erreur + ctx.request.body.data.invite_token = null; + ctx.request.body.data.invite_email = null; + + // On récupère l'email de l'utilisateur existant pour lui envoyer une notification + const invitedUser = await strapi + .documents("plugin::users-permissions.user") + .findMany({ + filters: { + id: data.user, + }, + }); + if (invitedUser && invitedUser.length > 0) { + targetEmail = invitedUser[0].email; + } + } + + // 4. Exécution de la création native par Strapi + const response = await super.create(ctx); + + // 5. Post-traitement : Envoi de l'email + if (response && targetEmail) { + try { + const chorals = await strapi + .documents("api::choral.choral") + .findMany({ + filters: { + id: data.choral, + }, + }); + const choral = chorals[0]; + + if (isExternalInvite) { + // Email d'invitation avec le lien tokenisé + await strapi + .service("api::mails.mails") + .sendInvitation( + targetEmail, + inviter?.username || "Un membre", + choral?.name || "votre chorale", + token, + ); + } else if (isInternalInvite) { + // Email de notification pour un utilisateur existant + // Ici le token est 'null', on prévient le service que c'est un user interne + await strapi.service("api::mails.mails").sendInvitation( + targetEmail, + inviter?.username || "Un membre", + choral?.name || "votre chorale", + null, // On passe explicitement null + ); + } + } catch (error) { + console.error( + "Erreur lors de l'envoi de l'email d'invitation :", + error, + ); + } + } + + return response; + }, + }), +); diff --git a/src/api/mails/content-types/mails/schema.json b/src/api/mails/content-types/mails/schema.json index f1f0064..c152255 100644 --- a/src/api/mails/content-types/mails/schema.json +++ b/src/api/mails/content-types/mails/schema.json @@ -25,6 +25,16 @@ } }, "component": "mail.mail" + }, + "onInviteUser": { + "type": "component", + "repeatable": true, + "pluginOptions": { + "i18n": { + "localized": true + } + }, + "component": "mail.mail" } } } diff --git a/src/api/mails/services/mails.ts b/src/api/mails/services/mails.ts index 28e24be..c6a594f 100644 --- a/src/api/mails/services/mails.ts +++ b/src/api/mails/services/mails.ts @@ -1,7 +1,61 @@ -/** - * mails service - */ +import { factories } from "@strapi/strapi"; -import { factories } from '@strapi/strapi'; +export default factories.createCoreService( + "api::mails.mails", + ({ strapi }) => ({ + // Notre méthode personnalisée + async sendInvitation( + emailAddress: string, + inviterName: string, + choirName: string, + inviteToken: string | null, + ) { + // 1. Définir le lien de redirection selon le type d'utilisateur + // S'il y a un token, c'est un nouvel utilisateur. Sinon, c'est un membre existant. + const inviteLink = inviteToken + ? `https://www.choralsync.com/invite?token=${inviteToken}` + : `https://www.choralsync.com/app`; // Lien vers l'app pour accepter l'invitation interne -export default factories.createCoreService('api::mails.mails'); + // 2. Récupérer le single-type "mails" avec l'API Document + const mailsConfig = await strapi.documents("api::mails.mails").findFirst({ + populate: ["onInviteUser"], + }); + + if (!mailsConfig || !mailsConfig.onInviteUser) { + throw new Error( + "Le template d'email d'invitation n'est pas configuré dans le CMS.", + ); + } + + const templateConfig = mailsConfig.onInviteUser[0]; + + try { + // 3. Envoyer l'email via le moteur de template natif + await strapi.plugin("email").service("email").sendTemplatedEmail( + { + to: emailAddress, + from: "ChoralSync ", + }, + { + subject: templateConfig.subject, + html: templateConfig.message, + text: `Bonjour, Vous avez été invité(e) par <%= inviter_name %> à rejoindre <%= choir_name %>. Acceptez l'invitation en cliquant sur ce lien : <%= invite_link %>`, + }, + { + // Variables injectées dans le template (<%= variable %>) + inviter_name: inviterName, + choir_name: choirName, + invite_link: inviteLink, + }, + ); + + return { success: true }; + } catch (error) { + console.error("Erreur lors de l'envoi de l'email :", error); + // On throw l'erreur pour que le contrôleur puisse la logger, + // mais idéalement sans bloquer la création en base. + throw error; + } + }, + }), +); diff --git a/src/extensions/documentation/documentation/1.0.0/full_documentation.json b/src/extensions/documentation/documentation/1.0.0/full_documentation.json index fe47eed..f0e7c45 100644 --- a/src/extensions/documentation/documentation/1.0.0/full_documentation.json +++ b/src/extensions/documentation/documentation/1.0.0/full_documentation.json @@ -14,7 +14,7 @@ "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, - "x-generation-date": "2026-04-06T14:10:16.903Z" + "x-generation-date": "2026-04-07T20:15:21.212Z" }, "x-strapi-config": { "plugins": [ @@ -20365,6 +20365,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -26724,6 +26731,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -32414,6 +32428,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -37495,6 +37516,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -43178,6 +43206,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -49217,6 +49252,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -54739,6 +54781,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -60708,6 +60757,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -66793,6 +66849,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -73090,6 +73153,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -76462,6 +76532,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "locale": { "type": "string" }, @@ -78977,6 +79054,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -81893,6 +81977,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -84537,6 +84628,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -90064,6 +90162,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -95641,6 +95746,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -101225,6 +101337,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -106749,6 +106868,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -112545,6 +112671,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -118404,6 +118537,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -123946,6 +124086,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -129866,6 +130013,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -135897,6 +136051,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -141410,6 +141571,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -146962,6 +147130,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -150698,6 +150873,12 @@ "onCreateUser": { "$ref": "#/components/schemas/MailMailComponent" }, + "onInviteUser": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MailMailComponent" + } + }, "locale": { "type": "string" }, @@ -150773,6 +150954,12 @@ "onCreateUser": { "$ref": "#/components/schemas/MailMailComponent" }, + "onInviteUser": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MailMailComponent" + } + }, "createdAt": { "type": "string", "format": "date-time" @@ -151188,6 +151375,12 @@ "onCreateUser": { "$ref": "#/components/schemas/MailMailComponent" }, + "onInviteUser": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MailMailComponent" + } + }, "createdAt": { "type": "string", "format": "date-time" @@ -153866,6 +154059,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -159433,6 +159633,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -165078,6 +165285,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -171231,6 +171445,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -176956,6 +177177,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -182570,6 +182798,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" @@ -188130,6 +188365,13 @@ "decline" ] }, + "invite_email": { + "type": "string", + "format": "email" + }, + "invite_token": { + "type": "string" + }, "createdAt": { "type": "string", "format": "date-time" diff --git a/types/generated/contentTypes.d.ts b/types/generated/contentTypes.d.ts index b2d1d72..8520907 100644 --- a/types/generated/contentTypes.d.ts +++ b/types/generated/contentTypes.d.ts @@ -723,6 +723,8 @@ export interface ApiChoralMembershipChoralMembership createdAt: Schema.Attribute.DateTime; createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & Schema.Attribute.Private; + invite_email: Schema.Attribute.Email & Schema.Attribute.Private; + invite_token: Schema.Attribute.String & Schema.Attribute.Private; locale: Schema.Attribute.String & Schema.Attribute.Private; localizations: Schema.Attribute.Relation< 'oneToMany', @@ -1369,6 +1371,12 @@ export interface ApiMailsMails extends Struct.SingleTypeSchema { localized: true; }; }>; + onInviteUser: Schema.Attribute.Component<'mail.mail', true> & + Schema.Attribute.SetPluginOptions<{ + i18n: { + localized: true; + }; + }>; publishedAt: Schema.Attribute.DateTime; updatedAt: Schema.Attribute.DateTime; updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &