0.13.11 : change register template an improve in user extension
Build release Docker image / Build Docker Images (push) Successful in 8m20s

This commit is contained in:
2026-05-05 13:55:21 +02:00
parent ca2604d0fb
commit 30c4c4ed3c
5 changed files with 308 additions and 170 deletions
+1
View File
@@ -7,6 +7,7 @@ ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV} ENV NODE_ENV=${NODE_ENV}
# On les transforme en variables d'environnement pour le processus de build # On les transforme en variables d'environnement pour le processus de build
ENV NODE_ENV=${NODE_ENV} ENV NODE_ENV=${NODE_ENV}
ARG URL=https://back.harmonylab.ovh
ENV URL=${URL} ENV URL=${URL}
# Strapi v4/v5 utilise aussi souvent celle-ci pour l'admin # Strapi v4/v5 utilise aussi souvent celle-ci pour l'admin
ENV STRAPI_ADMIN_BACKEND_URL=${URL} ENV STRAPI_ADMIN_BACKEND_URL=${URL}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "harmony-back", "name": "harmony-back",
"version": "0.13.10", "version": "0.13.11",
"private": true, "private": true,
"description": "A Strapi application", "description": "A Strapi application",
"scripts": { "scripts": {
+222
View File
@@ -0,0 +1,222 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Confirmation d'e-mail - ChoralSync</title>
<!--[if mso]>
<style type="text/css">
body,
table,
td,
a {
font-family: Arial, Helvetica, sans-serif !important;
}
</style>
<![endif]-->
</head>
<body
style="
margin: 0;
padding: 0;
background-color: #f9fafb;
font-family:
&quot;Inter&quot;,
-apple-system,
BlinkMacSystemFont,
&quot;Segoe UI&quot;,
Roboto,
Helvetica,
Arial,
sans-serif;
"
>
<table
width="100%"
border="0"
cellspacing="0"
cellpadding="0"
style="background-color: #f9fafb; padding: 40px 20px"
>
<tr>
<td align="center">
<!-- Main Container -->
<table
width="100%"
border="0"
cellspacing="0"
cellpadding="0"
style="
max-width: 650px;
background-color: #ffffff;
border-radius: 16px;
border: 1px solid #e2e8f0;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
"
>
<!-- Header -->
<tr>
<td style="padding: 32px 32px 16px 32px">
<div
style="
font-size: 24px;
font-weight: 700;
color: #111827;
letter-spacing: -0.025em;
"
>
ChoralSync
</div>
<div
style="
font-size: 14px;
color: #6b7280;
font-weight: 500;
margin-top: 4px;
"
>
Bienvenue — plus quune étape
</div>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 0 32px 32px 32px">
<h1
style="
font-size: 24px;
font-weight: 700;
color: #111827;
margin-top: 32px;
margin-bottom: 16px;
"
>
Bonjour {{USER_NAME}},
</h1>
<p
style="
font-size: 18px;
line-height: 1.6;
color: #4b5563;
margin-bottom: 32px;
"
>
Merci de t'être inscrit. Clique sur le bouton ci-dessous pour
confirmer ton adresse e-mail et activer ton compte.
</p>
<!-- CTA Button -->
<table
border="0"
cellspacing="0"
cellpadding="0"
style="margin-bottom: 40px"
>
<tr>
<td
align="center"
bgcolor="#0b1c30"
style="border-radius: 12px"
>
<a
href="{{CONFIRM_URL}}"
target="_blank"
style="
display: inline-block;
padding: 16px 32px;
font-size: 18px;
font-weight: 600;
color: #ffffff;
text-decoration: none;
border-radius: 12px;
"
>
Confirmer mon e-mail →
</a>
</td>
</tr>
</table>
<!-- Fallback Link -->
<div style="margin-top: 32px">
<p
style="font-size: 14px; color: #6b7280; margin-bottom: 12px"
>
Si le bouton ne fonctionne pas, copie-colle ce lien dans ton
navigateur :
</p>
<div
style="
background-color: #f1f5f9;
border-radius: 12px;
padding: 16px;
border: 1px solid #e2e8f0;
"
>
<a
href="{{CONFIRM_URL}}"
style="
color: #0ea5e9;
font-size: 14px;
text-decoration: underline;
word-break: break-all;
font-weight: 500;
"
>
{{CONFIRM_URL}}
</a>
</div>
</div>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="padding: 24px 32px; border-top: 1px solid #f1f5f9">
<p style="font-size: 14px; color: #9ca3af; margin: 0 0 8px 0">
Si tu n'es pas à l'origine de cette inscription, ignore cet
e-mail.
</p>
<p style="font-size: 14px; color: #9ca3af; margin: 0">
© ChoralSync 2026 •
<a
href="https://www.choralsync.com"
style="color: #9ca3af; text-decoration: underline"
>www.choralsync.com</a
>
</p>
</td>
</tr>
</table>
<!-- External Footer -->
<table
width="100%"
border="0"
cellspacing="0"
cellpadding="0"
style="max-width: 650px; margin-top: 32px"
>
<tr>
<td align="center" style="font-size: 14px; color: #9ca3af">
Besoin d'aide ? Réponds directement à cet e-mail ou consulte
notre
<a
href="#"
style="
color: #0ea5e9;
font-weight: 500;
text-decoration: underline;
"
>centre d'aide</a
>.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
@@ -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-05-04T16:37:25.949Z" "x-generation-date": "2026-05-05T11:54:41.084Z"
}, },
"x-strapi-config": { "x-strapi-config": {
"plugins": [ "plugins": [
+83 -168
View File
@@ -1,7 +1,7 @@
"use strict"; "use strict";
const lod = require("lodash"); const lod = require("lodash");
const utils = require("@strapi/utils"); 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 cryptoLib = require("crypto");
const bcrypt = require("bcryptjs"); const bcrypt = require("bcryptjs");
const fs = require("fs").promises; const fs = require("fs").promises;
@@ -9,16 +9,60 @@ const path = require("path");
module.exports = (plugin) => { module.exports = (plugin) => {
const rawProviders = plugin.services.providers({ strapi }); const rawProviders = plugin.services.providers({ strapi });
const { ApplicationError, ValidationError, ForbiddenError } = utils.errors; const { ApplicationError, ValidationError } = utils.errors;
const USER_MODEL_UID = "plugin::users-permissions.user"; const USER_MODEL_UID = "plugin::users-permissions.user";
const sanitizeUser = (user, ctx) => { const sanitizeUser = async (user, ctx) => {
const { auth } = ctx.state; const { auth } = ctx.state;
const userSchema = strapi.getModel("plugin::users-permissions.user"); const userSchema = strapi.getModel("plugin::users-permissions.user");
return strapi.contentAPI.sanitize.output(user, userSchema, { auth }); 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 ensureHashedPasswords = async (values) => {
const attributes = strapi.getModel(USER_MODEL_UID).attributes; const attributes = strapi.getModel(USER_MODEL_UID).attributes;
@@ -78,7 +122,7 @@ module.exports = (plugin) => {
to: user.email, to: user.email,
subject: onCreateUser?.subject ?? "Confirme ton adresse e-mail", subject: onCreateUser?.subject ?? "Confirme ton adresse e-mail",
html, html,
from: "ChoralSync <admin@harmonychoral.com>", from: "ChoralSync <contact@choralsync.com>",
}); });
return { ok: true }; return { ok: true };
@@ -111,7 +155,7 @@ module.exports = (plugin) => {
query.access_token || query.code || query.oauth_token; query.access_token || query.code || query.oauth_token;
if (!accessToken) { if (!accessToken) {
throw new Error("No access_token."); throw new ApplicationError("No access_token.");
} }
const profile = await getProfile(provider, query); const profile = await getProfile(provider, query);
@@ -119,7 +163,7 @@ module.exports = (plugin) => {
const email = lod.toLower(profile.email); const email = lod.toLower(profile.email);
if (!email) { if (!email) {
throw new Error("Email was not available."); throw new ApplicationError("Email was not available.");
} }
const users = await strapi.db const users = await strapi.db
@@ -142,7 +186,7 @@ module.exports = (plugin) => {
} }
if (lod.isEmpty(user) && !advancedSettings.allow_register) { 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)) { if (!lod.isEmpty(user)) {
@@ -150,7 +194,7 @@ module.exports = (plugin) => {
} }
if (users.length && advancedSettings.unique_email) { 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 const defaultRole = await strapi.db
@@ -174,8 +218,6 @@ module.exports = (plugin) => {
}; };
}; };
const originalMe = plugin.controllers.user.me;
plugin.controllers.user.me = async (ctx) => { plugin.controllers.user.me = async (ctx) => {
const fullUser = await strapi.db const fullUser = await strapi.db
.query("plugin::users-permissions.user") .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 = { const result = {
...JSON.parse(JSON.stringify(fullUser)), ...JSON.parse(JSON.stringify(fullUser)),
stats: { stats,
contacts: contactsCount,
groups: groupsCount,
posts: postsCount,
events: eventsCount,
followers: followersCount,
following: followingCount,
},
}; };
return result; return result;
@@ -279,10 +274,6 @@ module.exports = (plugin) => {
// 4️⃣ Ajoute un champ calculé // 4️⃣ Ajoute un champ calculé
user.profileCompleted = Boolean(user.username && user.surname); 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 // 5️⃣ Refetch avec relations peuplées
try { try {
const populatedUser = await strapi.entityService.findOne( const populatedUser = await strapi.entityService.findOne(
@@ -291,6 +282,7 @@ module.exports = (plugin) => {
{ {
populate: { populate: {
post_ownerships: { post_ownerships: {
filters: { contextType: "user", relation: "owner" },
populate: { populate: {
post: { post: {
populate: { populate: {
@@ -300,6 +292,7 @@ module.exports = (plugin) => {
}, },
}, },
contacts: { contacts: {
filters: { state: "accepted" },
populate: { populate: {
owner: { owner: {
populate: { populate: {
@@ -314,6 +307,7 @@ module.exports = (plugin) => {
}, },
}, },
group_memberships: { group_memberships: {
filters: { role: { $in: ["member", "admin", "owner"] } },
populate: { populate: {
group: { group: {
populate: { 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 const eventRelationships = await strapi.db
.query("api::event-relationship.event-relationship") .query("api::event-relationship.event-relationship")
@@ -356,90 +329,28 @@ module.exports = (plugin) => {
event: true, event: true,
}, },
}); });
user.event_relationships = eventRelationships || [];
const [ const stats = await getUserStats(user.id);
contactsCount,
groupsCount, const sanitizedPopulatedUser = await sanitizeUser(populatedUser, ctx) as object;
postsCount,
eventsCount, // Fusionne les données originales avec les relations sécurisées
followersCount, user = {
followingCount, ...user,
] = await Promise.all([ ...sanitizedPopulatedUser,
strapi.db.query("api::contact.contact").count({ event_relationships: eventRelationships || [],
where: { stats
$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) { } catch (err) {
console.error("Erreur populate relations user:", err); console.error("Erreur populate relations user:", err);
// fallback : retourne juste l'utilisateur original // fallback : retourne juste l'utilisateur original
user.stats = await getUserStats(user.id);
} }
ctx.body = user; ctx.body = user;
return ctx; return ctx;
}; };
const uploadImage = async ( // Removed unused uploadImage function
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;
};
plugin.services.providers = providers; plugin.services.providers = providers;
@@ -450,13 +361,13 @@ module.exports = (plugin) => {
return ctx.unauthorized(); return ctx.unauthorized();
} }
let data; let data = ctx.request.body.data || ctx.request.body;
if (ctx.request.body.data && typeof ctx.request.body.data === 'string') { if (typeof data === "string") {
data = JSON.parse(ctx.request.body.data); try {
} else if (ctx.request.body.data && typeof ctx.request.body.data === 'object') { data = JSON.parse(data);
data = ctx.request.body.data; } catch (err) {
} else { throw new ValidationError("Invalid JSON format in request body data");
data = ctx.request.body; }
} }
const newData = lod.pick(data, [ const newData = lod.pick(data, [
@@ -557,10 +468,14 @@ module.exports = (plugin) => {
?.createNotification({ ?.createNotification({
title: "Bienvenue !", title: "Bienvenue !",
message: `Ton compte est maintenant activé.`, message: `Ton compte est maintenant activé.`,
type: "success", type: "accountCreation",
target_user: result.id, target_user: result.id,
source: "system", source: "system",
payload: { email: result.email }, payload: {
email: result.email,
url: "/app/user",
description: "Penses à remplir ton profil",
},
}); });
strapi.log.info( strapi.log.info(