diff --git a/Dockerfile b/Dockerfile
index 74a75c0..7c1ecc0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,6 +7,7 @@ ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
# On les transforme en variables d'environnement pour le processus de build
ENV NODE_ENV=${NODE_ENV}
+ARG URL=https://back.harmonylab.ovh
ENV URL=${URL}
# Strapi v4/v5 utilise aussi souvent celle-ci pour l'admin
ENV STRAPI_ADMIN_BACKEND_URL=${URL}
diff --git a/package.json b/package.json
index 8f79647..7546284 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "harmony-back",
- "version": "0.13.10",
+ "version": "0.13.11",
"private": true,
"description": "A Strapi application",
"scripts": {
diff --git a/src/email-templates/register.html b/src/email-templates/register.html
new file mode 100644
index 0000000..7cc7125
--- /dev/null
+++ b/src/email-templates/register.html
@@ -0,0 +1,222 @@
+
+
+
+
+
+ Confirmation d'e-mail - ChoralSync
+
+
+
+
+
+
+
+
+
+
+ |
+
+ ChoralSync
+
+
+ Bienvenue — plus qu’une étape
+
+ |
+
+
+
+
+
+
+ Bonjour {{USER_NAME}},
+
+
+ Merci de t'être inscrit. Clique sur le bouton ci-dessous pour
+ confirmer ton adresse e-mail et activer ton compte.
+
+
+
+
+
+
+
+
+ Si le bouton ne fonctionne pas, copie-colle ce lien dans ton
+ navigateur :
+
+
+
+ |
+
+
+
+
+ |
+
+ Si tu n'es pas à l'origine de cette inscription, ignore cet
+ e-mail.
+
+
+ © ChoralSync 2026 •
+ www.choralsync.com
+
+ |
+
+
+
+
+
+
+ |
+ Besoin d'aide ? Réponds directement à cet e-mail ou consulte
+ notre
+ centre d'aide.
+ |
+
+
+ |
+
+
+
+
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 8452950..0154a1f 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-05-04T16:37:25.949Z"
+ "x-generation-date": "2026-05-05T11:54:41.084Z"
},
"x-strapi-config": {
"plugins": [
diff --git a/src/extensions/users-permissions/strapi-server.ts b/src/extensions/users-permissions/strapi-server.ts
index 304127d..1bbb3e9 100644
--- a/src/extensions/users-permissions/strapi-server.ts
+++ b/src/extensions/users-permissions/strapi-server.ts
@@ -1,7 +1,7 @@
"use strict";
const lod = require("lodash");
const utils = require("@strapi/utils");
-const { concat, compact, isArray, toNumber, getOr } = require("lodash/fp");
+const { toNumber, getOr } = require("lodash/fp");
const cryptoLib = require("crypto");
const bcrypt = require("bcryptjs");
const fs = require("fs").promises;
@@ -9,16 +9,60 @@ const path = require("path");
module.exports = (plugin) => {
const rawProviders = plugin.services.providers({ strapi });
- const { ApplicationError, ValidationError, ForbiddenError } = utils.errors;
+ const { ApplicationError, ValidationError } = utils.errors;
const USER_MODEL_UID = "plugin::users-permissions.user";
- const sanitizeUser = (user, ctx) => {
+ const sanitizeUser = async (user, ctx) => {
const { auth } = ctx.state;
const userSchema = strapi.getModel("plugin::users-permissions.user");
return strapi.contentAPI.sanitize.output(user, userSchema, { auth });
};
+ const getUserStats = async (userId) => {
+ 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" },
+ }),
+ ]);
+
+ return {
+ contacts: contactsCount,
+ groups: groupsCount,
+ posts: postsCount,
+ events: eventsCount,
+ followers: followersCount,
+ following: followingCount,
+ };
+ };
+
const ensureHashedPasswords = async (values) => {
const attributes = strapi.getModel(USER_MODEL_UID).attributes;
@@ -78,7 +122,7 @@ module.exports = (plugin) => {
to: user.email,
subject: onCreateUser?.subject ?? "Confirme ton adresse e-mail",
html,
- from: "ChoralSync ",
+ from: "ChoralSync ",
});
return { ok: true };
@@ -111,7 +155,7 @@ module.exports = (plugin) => {
query.access_token || query.code || query.oauth_token;
if (!accessToken) {
- throw new Error("No access_token.");
+ throw new ApplicationError("No access_token.");
}
const profile = await getProfile(provider, query);
@@ -119,7 +163,7 @@ module.exports = (plugin) => {
const email = lod.toLower(profile.email);
if (!email) {
- throw new Error("Email was not available.");
+ throw new ApplicationError("Email was not available.");
}
const users = await strapi.db
@@ -142,7 +186,7 @@ module.exports = (plugin) => {
}
if (lod.isEmpty(user) && !advancedSettings.allow_register) {
- throw new Error("Register action is actually not available.");
+ throw new ApplicationError("Register action is actually not available.");
}
if (!lod.isEmpty(user)) {
@@ -150,7 +194,7 @@ module.exports = (plugin) => {
}
if (users.length && advancedSettings.unique_email) {
- throw new Error("Email is already taken.");
+ throw new ApplicationError("Email is already taken.");
}
const defaultRole = await strapi.db
@@ -174,8 +218,6 @@ 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")
@@ -206,58 +248,11 @@ module.exports = (plugin) => {
);
}
- const user = ctx.state.user;
+ const stats = await getUserStats(ctx.state.user.id);
- 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,
- },
+ stats,
};
return result;
@@ -279,10 +274,6 @@ module.exports = (plugin) => {
// 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(
@@ -291,6 +282,7 @@ module.exports = (plugin) => {
{
populate: {
post_ownerships: {
+ filters: { contextType: "user", relation: "owner" },
populate: {
post: {
populate: {
@@ -300,6 +292,7 @@ module.exports = (plugin) => {
},
},
contacts: {
+ filters: { state: "accepted" },
populate: {
owner: {
populate: {
@@ -314,6 +307,7 @@ module.exports = (plugin) => {
},
},
group_memberships: {
+ filters: { role: { $in: ["member", "admin", "owner"] } },
populate: {
group: {
populate: {
@@ -326,27 +320,6 @@ module.exports = (plugin) => {
},
},
);
- // 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")
@@ -356,90 +329,28 @@ module.exports = (plugin) => {
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,
+
+ const stats = await getUserStats(user.id);
+
+ const sanitizedPopulatedUser = await sanitizeUser(populatedUser, ctx) as object;
+
+ // Fusionne les données originales avec les relations sécurisées
+ user = {
+ ...user,
+ ...sanitizedPopulatedUser,
+ event_relationships: eventRelationships || [],
+ stats
};
} catch (err) {
console.error("Erreur populate relations user:", err);
// fallback : retourne juste l'utilisateur original
+ user.stats = await getUserStats(user.id);
}
ctx.body = user;
return ctx;
};
- const uploadImage = async (
- ctx,
- label: string,
- username: string,
- userId: number,
- ) => {
- const key = `${label}Image`;
- if (ctx.request.files[key]) {
- const files = Array.isArray(ctx.request.files[key])
- ? ctx.request.files[key][0]
- : ctx.request.files[key];
- const extension = files.originalFilename.match(/\.[0-9a-z]+$/i);
- const payload = {
- fileInfo: {
- caption: "undefined",
- alternativeText: username || "",
- name: `${username}_avatar${extension}`,
- },
- path: `user/${userId}`,
- };
- const asset = await strapi.services["plugin::upload.upload"].upload({
- data: payload,
- files,
- });
- return asset[0].id;
- }
-
- return 0;
- };
+ // Removed unused uploadImage function
plugin.services.providers = providers;
@@ -450,13 +361,13 @@ module.exports = (plugin) => {
return ctx.unauthorized();
}
- let data;
- if (ctx.request.body.data && typeof ctx.request.body.data === 'string') {
- data = JSON.parse(ctx.request.body.data);
- } else if (ctx.request.body.data && typeof ctx.request.body.data === 'object') {
- data = ctx.request.body.data;
- } else {
- data = ctx.request.body;
+ let data = ctx.request.body.data || ctx.request.body;
+ if (typeof data === "string") {
+ try {
+ data = JSON.parse(data);
+ } catch (err) {
+ throw new ValidationError("Invalid JSON format in request body data");
+ }
}
const newData = lod.pick(data, [
@@ -557,10 +468,14 @@ module.exports = (plugin) => {
?.createNotification({
title: "Bienvenue !",
message: `Ton compte est maintenant activé.`,
- type: "success",
+ type: "accountCreation",
target_user: result.id,
source: "system",
- payload: { email: result.email },
+ payload: {
+ email: result.email,
+ url: "/app/user",
+ description: "Penses à remplir ton profil",
+ },
});
strapi.log.info(