0.13.8 : invite by mail or contact and change mail provider on local
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 10m42s

This commit is contained in:
2026-04-07 22:15:59 +02:00
parent 5af0f28b39
commit d469fc41e8
8 changed files with 433 additions and 23 deletions

View File

@@ -3,16 +3,18 @@ export default () => ({
config: { config: {
provider: "nodemailer", provider: "nodemailer",
providerOptions: { providerOptions: {
host: "mail.harmonychoral.com", host: "smtp.zeptomail.eu",
port: 465, port: 587,
auth: { auth: {
user: "admin@harmonychoral.com", user: "emailapikey",
pass: "Apslxnap12bn23", pass: "yA6KbHsJ4lrywWtTFUc+0pSC94lm/aE/2nzks3i2fpZ1LYXp3qE71RBvd4O4c2CLjdfT5a9UbIkVJoCwvIpbfpczPIBXJpTGTuv4P2uV48xh8ciEYNYjhJivALIWFqVOeBsnDyo4QfEjWA==",
}, },
debug: true,
logger: true,
}, },
settings: { settings: {
defaultFrom: "admin@harmonychoral.com", defaultFrom: "ChoralSync <noreply@choralsync.com>",
defaultReplyTo: "admin@harmonychoral.com", defaultReplyTo: "contact@choralsync.com",
}, },
}, },
}, },

View File

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

View File

@@ -26,11 +26,7 @@
}, },
"role": { "role": {
"type": "enumeration", "type": "enumeration",
"enum": [ "enum": ["member", "admin", "owner"]
"member",
"admin",
"owner"
]
}, },
"permissions": { "permissions": {
"type": "component", "type": "component",
@@ -51,6 +47,14 @@
"pending_admin_approval", "pending_admin_approval",
"decline" "decline"
] ]
},
"invite_email": {
"type": "email",
"private": true
},
"invite_token": {
"type": "string",
"private": true
} }
} }
} }

View File

@@ -1,7 +1,97 @@
/** import { factories } from "@strapi/strapi";
* choral-membership controller 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;
},
}),
);

View File

@@ -25,6 +25,16 @@
} }
}, },
"component": "mail.mail" "component": "mail.mail"
},
"onInviteUser": {
"type": "component",
"repeatable": true,
"pluginOptions": {
"i18n": {
"localized": true
}
},
"component": "mail.mail"
} }
} }
} }

View File

@@ -1,7 +1,61 @@
/** import { factories } from "@strapi/strapi";
* mails service
*/
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 <contact@choralsync.com>",
},
{
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;
}
},
}),
);

View File

@@ -14,7 +14,7 @@
"name": "Apache 2.0", "name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html" "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": { "x-strapi-config": {
"plugins": [ "plugins": [
@@ -20365,6 +20365,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -26724,6 +26731,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -32414,6 +32428,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -37495,6 +37516,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -43178,6 +43206,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -49217,6 +49252,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -54739,6 +54781,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -60708,6 +60757,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -66793,6 +66849,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -73090,6 +73153,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -76462,6 +76532,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"locale": { "locale": {
"type": "string" "type": "string"
}, },
@@ -78977,6 +79054,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -81893,6 +81977,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -84537,6 +84628,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -90064,6 +90162,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -95641,6 +95746,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -101225,6 +101337,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -106749,6 +106868,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -112545,6 +112671,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -118404,6 +118537,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -123946,6 +124086,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -129866,6 +130013,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -135897,6 +136051,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -141410,6 +141571,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -146962,6 +147130,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -150698,6 +150873,12 @@
"onCreateUser": { "onCreateUser": {
"$ref": "#/components/schemas/MailMailComponent" "$ref": "#/components/schemas/MailMailComponent"
}, },
"onInviteUser": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MailMailComponent"
}
},
"locale": { "locale": {
"type": "string" "type": "string"
}, },
@@ -150773,6 +150954,12 @@
"onCreateUser": { "onCreateUser": {
"$ref": "#/components/schemas/MailMailComponent" "$ref": "#/components/schemas/MailMailComponent"
}, },
"onInviteUser": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MailMailComponent"
}
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -151188,6 +151375,12 @@
"onCreateUser": { "onCreateUser": {
"$ref": "#/components/schemas/MailMailComponent" "$ref": "#/components/schemas/MailMailComponent"
}, },
"onInviteUser": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MailMailComponent"
}
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -153866,6 +154059,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -159433,6 +159633,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -165078,6 +165285,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -171231,6 +171445,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -176956,6 +177177,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -182570,6 +182798,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -188130,6 +188365,13 @@
"decline" "decline"
] ]
}, },
"invite_email": {
"type": "string",
"format": "email"
},
"invite_token": {
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"

View File

@@ -723,6 +723,8 @@ export interface ApiChoralMembershipChoralMembership
createdAt: Schema.Attribute.DateTime; createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private; Schema.Attribute.Private;
invite_email: Schema.Attribute.Email & Schema.Attribute.Private;
invite_token: Schema.Attribute.String & Schema.Attribute.Private;
locale: Schema.Attribute.String & Schema.Attribute.Private; locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation< localizations: Schema.Attribute.Relation<
'oneToMany', 'oneToMany',
@@ -1369,6 +1371,12 @@ export interface ApiMailsMails extends Struct.SingleTypeSchema {
localized: true; localized: true;
}; };
}>; }>;
onInviteUser: Schema.Attribute.Component<'mail.mail', true> &
Schema.Attribute.SetPluginOptions<{
i18n: {
localized: true;
};
}>;
publishedAt: Schema.Attribute.DateTime; publishedAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime; updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &