Files
harmony-back/src/extensions/users-permissions/strapi-server.ts
julien vdb e9505524bf
Some checks failed
Build release Docker image / Build Docker Images (push) Failing after 8s
0.12.15 : add activity as a service
2025-12-11 17:32:27 +01:00

571 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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);
};
const getProfile = async (provider, query) => {
const accessToken = query.access_token || query.code || query.oauth_token;
const providers = await strapi
.store({ type: "plugin", name: "users-permissions", key: "grant" })
.get();
return getService("providers-registry").run({
provider,
query,
accessToken,
providers,
});
};
const providers = ({ strapi }) => {
return {
...rawProviders,
connect: async (provider, query) => {
const accessToken =
query.access_token || query.code || query.oauth_token;
if (!accessToken) {
throw new Error("No access_token.");
}
const profile = await getProfile(provider, query);
const email = lod.toLower(profile.email);
if (!email) {
throw new Error("Email was not available.");
}
const users = await strapi.db
.query("plugin::users-permissions.user")
.findMany({
where: { email },
});
const advancedSettings = (await strapi
.store({ type: "plugin", name: "users-permissions", key: "advanced" })
.get()) as {
allow_register: boolean;
unique_email?: boolean;
default_role?: string;
};
let user = lod.find(users, { provider });
if (lod.isEmpty(user)) {
user = lod.find(users);
}
if (lod.isEmpty(user) && !advancedSettings.allow_register) {
throw new Error("Register action is actually not available.");
}
if (!lod.isEmpty(user)) {
return user;
}
if (users.length && advancedSettings.unique_email) {
throw new Error("Email is already taken.");
}
const defaultRole = await strapi.db
.query("plugin::users-permissions.role")
.findOne({ where: { type: advancedSettings.default_role } });
const newUser = {
...profile,
email,
provider,
role: defaultRole.id,
confirmed: true,
};
const createdUser = await strapi.db
.query("plugin::users-permissions.user")
.create({ data: newUser });
return createdUser;
},
};
};
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]) {
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}`,
},
};
const asset = await strapi.services["plugin::upload.upload"].upload({
data: payload,
files,
});
return asset[0].id;
}
return 0;
};
plugin.services.providers = providers;
plugin.controllers.user.updateMe = async (ctx) => {
const user = ctx.state.user;
if (!user) {
return ctx.unauthorized();
}
const data = JSON.parse(ctx.request.body.data);
const newData = lod.pick(data, [
"email",
"username",
"name",
"surname",
"address",
"gender",
"voice",
"job",
"dob",
"phone",
"bio",
"experience",
"tags",
"languages",
"parameter",
"privacy",
]);
/*
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")
.findOne({ where: { username: newData.username } });
if (userWithSameUsername && userWithSameUsername.id != user.id) {
return ctx.badRequest("Username already taken");
}
}
if (newData.email) {
const userWithSameEmail = await strapi
.query("plugin::users-permissions.user")
.findOne({ where: { email: newData.email.toLowerCase() } });
if (userWithSameEmail && userWithSameEmail.id != user.id) {
return ctx.badRequest("Email already taken");
}
newData.email = newData.email.toLowerCase();
}
ctx.request.body = newData;
ctx.params = { id: user.id };
const keysExcludingParameterAndPrivacy = Object.keys(newData).filter(
(key) => key !== "parameter" && key !== "privacy"
);
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({
method: "PUT",
path: "/users/me",
handler: "user.updateMe",
});
return plugin;
};