Compare commits
16 Commits
616b14edca
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 30c4c4ed3c | |||
| ca2604d0fb | |||
| 574d81ac54 | |||
| 6034c4f07b | |||
| 945ad5d095 | |||
| ce25ba2695 | |||
| f7f0ee17d7 | |||
| 0648bf74bd | |||
| d469fc41e8 | |||
| 5af0f28b39 | |||
| fd14d20dd0 | |||
| 7d020c33b6 | |||
| 1a3ea9e0c1 | |||
| b0db6a5ddf | |||
| f13efb8707 | |||
| 75c90aa042 |
@@ -5,3 +5,4 @@ dist
|
|||||||
.cache
|
.cache
|
||||||
.env
|
.env
|
||||||
build
|
build
|
||||||
|
.git
|
||||||
@@ -5,9 +5,9 @@ on: [push]
|
|||||||
env:
|
env:
|
||||||
DOCKER_IMAGE_NAME: harmony-back
|
DOCKER_IMAGE_NAME: harmony-back
|
||||||
DOCKER_REGISTRY_URL: git.harmonylab.ovh
|
DOCKER_REGISTRY_URL: git.harmonylab.ovh
|
||||||
DOCKER_REGISTRY_ORG: harmony
|
DOCKER_REGISTRY_ORG: admin
|
||||||
RELEASE_VERSION: 1.2.0
|
RELEASE_VERSION: 1.2.0
|
||||||
DOCKPLOY_WEBHOOK_URL: http://192.168.0.220:3000/api/deploy/HHWS7dR5rT-8vbXD6Zypv
|
DOCKPLOY_WEBHOOK_URL: https://dokploy.harmonylab.ovh/api/deploy/fO2GDNPJAoNicWALwiBPa
|
||||||
DISCORD_WEBHOOK_URL: https://discord.com/api/webhooks/1433240422901088339/GAvL79ESBRabkB6rvxN2DsWI74KJ_Szgp9W2_PycPIY113rMCT_9LvIv-iTLCMD9W9qH
|
DISCORD_WEBHOOK_URL: https://discord.com/api/webhooks/1433240422901088339/GAvL79ESBRabkB6rvxN2DsWI74KJ_Szgp9W2_PycPIY113rMCT_9LvIv-iTLCMD9W9qH
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ RUN apk update && apk add --no-cache \
|
|||||||
|
|
||||||
ARG NODE_ENV=production
|
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
|
||||||
|
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}
|
||||||
|
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json yarn.lock ./
|
||||||
|
|||||||
Vendored
+8
-7
@@ -3,17 +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==",
|
||||||
},
|
},
|
||||||
// ... any custom nodemailer options
|
debug: true,
|
||||||
|
logger: true,
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
defaultFrom: "admin@harmonychoral.com",
|
defaultFrom: "ChoralSync <noreply@choralsync.com>",
|
||||||
defaultReplyTo: "admin@harmonychoral.com",
|
defaultReplyTo: "contact@choralsync.com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+16
-12
@@ -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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -21,16 +23,18 @@ export default () => ({
|
|||||||
config: {
|
config: {
|
||||||
provider: "aws-s3",
|
provider: "aws-s3",
|
||||||
providerOptions: {
|
providerOptions: {
|
||||||
baseUrl: "https://container.harmonylab.ovh/harmony",
|
baseUrl: "https://container.choralsync.com",
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: "admin",
|
accessKeyId: "a6f474b04f010543022788efc626cbfe",
|
||||||
secretAccessKey: "Apslxnap12bn23",
|
secretAccessKey:
|
||||||
|
"eeeb631194378edee5145b3ef6314630b152fa186b3dab2fefdf8b48952cde21",
|
||||||
},
|
},
|
||||||
endpoint: "https://container.harmonylab.ovh",
|
endpoint:
|
||||||
|
"https://4c59d3c67191bdd08e311a5a49c3ac98.r2.cloudflarestorage.com",
|
||||||
forcePathStyle: true,
|
forcePathStyle: true,
|
||||||
region: "eu-west-3",
|
region: "weur",
|
||||||
params: {
|
params: {
|
||||||
Bucket: "harmony",
|
Bucket: "choralsync",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "harmony-back",
|
"name": "harmony-back",
|
||||||
"version": "0.13.4",
|
"version": "0.13.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A Strapi application",
|
"description": "A Strapi application",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -58,14 +58,15 @@
|
|||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
"medias": {
|
"medias": {
|
||||||
|
"type": "media",
|
||||||
|
"multiple": true,
|
||||||
|
"required": false,
|
||||||
"allowedTypes": [
|
"allowedTypes": [
|
||||||
"images",
|
"images",
|
||||||
"files",
|
"files",
|
||||||
"videos",
|
"videos",
|
||||||
"audios"
|
"audios"
|
||||||
],
|
]
|
||||||
"type": "media",
|
|
||||||
"multiple": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"singularName": "channel",
|
"singularName": "channel",
|
||||||
"pluralName": "channels",
|
"pluralName": "channels",
|
||||||
"displayName": "Channel"
|
"displayName": "Channel",
|
||||||
|
"description": ""
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"draftAndPublish": false
|
"draftAndPublish": false
|
||||||
@@ -33,6 +34,12 @@
|
|||||||
"relation": "oneToMany",
|
"relation": "oneToMany",
|
||||||
"target": "api::message.message",
|
"target": "api::message.message",
|
||||||
"mappedBy": "channel"
|
"mappedBy": "channel"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"type": "json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,29 @@
|
|||||||
"type": "component",
|
"type": "component",
|
||||||
"repeatable": true,
|
"repeatable": true,
|
||||||
"component": "user.permissions"
|
"component": "user.permissions"
|
||||||
|
},
|
||||||
|
"role_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"permission_exceptions": {
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "enumeration",
|
||||||
|
"enum": [
|
||||||
|
"active",
|
||||||
|
"pending_user_approval",
|
||||||
|
"pending_admin_approval",
|
||||||
|
"decline"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"invite_email": {
|
||||||
|
"type": "email",
|
||||||
|
"private": true
|
||||||
|
},
|
||||||
|
"invite_token": {
|
||||||
|
"type": "string",
|
||||||
|
"private": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|||||||
@@ -103,6 +103,28 @@
|
|||||||
},
|
},
|
||||||
"available_tags": {
|
"available_tags": {
|
||||||
"type": "json"
|
"type": "json"
|
||||||
|
},
|
||||||
|
"social_networks": {
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"founded": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"available_roles": {
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
"logo": {
|
||||||
|
"allowedTypes": [
|
||||||
|
"images",
|
||||||
|
"files",
|
||||||
|
"videos",
|
||||||
|
"audios"
|
||||||
|
],
|
||||||
|
"type": "media",
|
||||||
|
"multiple": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,5 +33,5 @@ export default factories.createCoreController(
|
|||||||
const result = await super.create(ctx);
|
const result = await super.create(ctx);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,6 +25,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"component": "mail.mail"
|
"component": "mail.mail"
|
||||||
|
},
|
||||||
|
"onInviteUser": {
|
||||||
|
"type": "component",
|
||||||
|
"repeatable": true,
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"component": "mail.mail"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|||||||
@@ -33,6 +33,14 @@
|
|||||||
"type": "relation",
|
"type": "relation",
|
||||||
"relation": "oneToOne",
|
"relation": "oneToOne",
|
||||||
"target": "api::choral.choral"
|
"target": "api::choral.choral"
|
||||||
|
},
|
||||||
|
"reactions": {
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
"parent_message": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "oneToOne",
|
||||||
|
"target": "api::message.message"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,134 @@
|
|||||||
|
|
||||||
import { factories } from "@strapi/strapi";
|
import { factories } from "@strapi/strapi";
|
||||||
|
|
||||||
|
interface ChannelPermissions {
|
||||||
|
[roleName: string]: {
|
||||||
|
read: boolean;
|
||||||
|
write: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PopulatedChannel {
|
||||||
|
id: any;
|
||||||
|
permissions?: any;
|
||||||
|
choral?: {
|
||||||
|
id: any;
|
||||||
|
available_roles?: any[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChoralMembership {
|
||||||
|
id: any;
|
||||||
|
role_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default factories.createCoreController(
|
export default factories.createCoreController(
|
||||||
"api::message.message",
|
"api::message.message",
|
||||||
({ strapi }) => ({
|
({ strapi }) => ({
|
||||||
async create(ctx) {
|
async create(ctx) {
|
||||||
return super.create(ctx);
|
const { channel: channelId } = ctx.request.body.data;
|
||||||
|
const user = ctx.state.user;
|
||||||
|
|
||||||
|
// 1. Récupérer les permissions du channel et son choral
|
||||||
|
const channel = (await strapi.entityService.findOne(
|
||||||
|
"api::channel.channel",
|
||||||
|
channelId,
|
||||||
|
{ populate: ["choral"] },
|
||||||
|
)) as unknown as PopulatedChannel;
|
||||||
|
|
||||||
|
if (!channel || !channel.choral) {
|
||||||
|
return ctx.badRequest("Canal ou chorale introuvable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Récupérer le membership du user pour cette chorale
|
||||||
|
const memberships = (await strapi.entityService.findMany(
|
||||||
|
"api::choral-membership.choral-membership",
|
||||||
|
{
|
||||||
|
filters: {
|
||||||
|
user: user.id,
|
||||||
|
choral: channel.choral.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)) as unknown as ChoralMembership[];
|
||||||
|
|
||||||
|
const membership = memberships?.[0];
|
||||||
|
const userRoleId = membership?.role_id;
|
||||||
|
|
||||||
|
// Résolution du nom du rôle à partir des available_roles de la chorale
|
||||||
|
const availableRoles = channel.choral.available_roles || [];
|
||||||
|
const userRoleName = availableRoles.find(
|
||||||
|
(r) => String(r.id) === String(userRoleId),
|
||||||
|
)?.name;
|
||||||
|
|
||||||
|
// 3. Vérification "Write" (on vérifie le flag write pour le rôle spécifique)
|
||||||
|
const permissions = channel.permissions as unknown as ChannelPermissions;
|
||||||
|
const canWrite =
|
||||||
|
permissions?.[userRoleName]?.write === true ||
|
||||||
|
userRoleId === "admin" ||
|
||||||
|
userRoleName === "Admin";
|
||||||
|
|
||||||
|
if (!canWrite) {
|
||||||
|
return ctx.forbidden(
|
||||||
|
"Désolé, tu n'as pas le droit d'écrire dans ce salon.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Procéder à la création si OK
|
||||||
|
return await super.create(ctx);
|
||||||
},
|
},
|
||||||
})
|
|
||||||
|
async addReaction(ctx) {
|
||||||
|
const { id } = ctx.params;
|
||||||
|
const { reaction } = ctx.request.body;
|
||||||
|
const user = ctx.state.user;
|
||||||
|
|
||||||
|
if (!reaction) {
|
||||||
|
return ctx.badRequest("La réaction est requise.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Récupérer le message
|
||||||
|
const message = await strapi.entityService.findOne(
|
||||||
|
"api::message.message",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return ctx.notFound("Message introuvable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Gérer les réactions (toggle logic)
|
||||||
|
let reactions = (message.reactions as any) || {};
|
||||||
|
if (typeof reactions !== "object") {
|
||||||
|
reactions = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reactions[reaction]) {
|
||||||
|
reactions[reaction] = [user.id];
|
||||||
|
} else {
|
||||||
|
const index = reactions[reaction].indexOf(user.id);
|
||||||
|
if (index > -1) {
|
||||||
|
// Si l'utilisateur a déjà réagi, on retire sa réaction
|
||||||
|
reactions[reaction].splice(index, 1);
|
||||||
|
// Si plus personne n'a réagi avec cet emoji, on retire l'entrée
|
||||||
|
if (reactions[reaction].length === 0) {
|
||||||
|
delete reactions[reaction];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Sinon on ajoute l'utilisateur
|
||||||
|
reactions[reaction].push(user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Mettre à jour le message
|
||||||
|
const updatedMessage = await strapi.entityService.update(
|
||||||
|
"api::message.message",
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
data: { reactions },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return updatedMessage;
|
||||||
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export default {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: "/messages/:id/react",
|
||||||
|
handler: "message.addReaction",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -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:
|
||||||
|
"Inter",
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
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 qu’une é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>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
|||||||
|
module.exports = (plugin) => {
|
||||||
|
// 1. LE SECRET : On ajoute le champ 'path' au schéma officiel de Strapi.
|
||||||
|
// Strapi va ainsi créer la colonne en BDD et arrêter de supprimer notre variable.
|
||||||
|
plugin.contentTypes.file.schema.attributes.path = {
|
||||||
|
type: "string",
|
||||||
|
configurable: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalServiceFactory = plugin.services.upload;
|
||||||
|
|
||||||
|
plugin.services.upload = ({ strapi }) => {
|
||||||
|
const service = originalServiceFactory({ strapi });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...service,
|
||||||
|
|
||||||
|
// 2. Surcharge de l'Uploader
|
||||||
|
async upload(fileData, usage) {
|
||||||
|
const ctx = strapi.requestContext.get();
|
||||||
|
// On récupère le custom path envoyé par ton client Next.js
|
||||||
|
const customPath = ctx?.request?.body?.path;
|
||||||
|
|
||||||
|
if (customPath) {
|
||||||
|
// Pour que le provider S3 l'utilise tout de suite (R2)
|
||||||
|
fileData.path = customPath;
|
||||||
|
|
||||||
|
// Pour que Strapi l'enregistre dans notre nouvelle colonne BDD
|
||||||
|
if (!fileData.fileInfo) fileData.fileInfo = {};
|
||||||
|
fileData.fileInfo.path = customPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Le service original va maintenant sauvegarder le path en BDD !
|
||||||
|
return service.upload(fileData, usage);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Note : Plus besoin de surcharger `remove` (l'équivalent de delete) !
|
||||||
|
// Comme la colonne 'path' est maintenant en BDD, le service original
|
||||||
|
// récupère l'info et la passe au provider S3 qui supprime le bon dossier.
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return plugin;
|
||||||
|
};
|
||||||
@@ -265,7 +265,7 @@
|
|||||||
"target": "api::order.order",
|
"target": "api::order.order",
|
||||||
"mappedBy": "user"
|
"mappedBy": "user"
|
||||||
},
|
},
|
||||||
"admin_tags": {
|
"smartTags": {
|
||||||
"type": "json"
|
"type": "json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -26,7 +70,7 @@ module.exports = (plugin) => {
|
|||||||
if (attributes[key] && attributes[key].type === "password") {
|
if (attributes[key] && attributes[key].type === "password") {
|
||||||
// Check if a custom encryption.rounds has been set on the password attribute
|
// Check if a custom encryption.rounds has been set on the password attribute
|
||||||
const rounds = toNumber(
|
const rounds = toNumber(
|
||||||
getOr(10, "encryption.rounds", attributes[key])
|
getOr(10, "encryption.rounds", attributes[key]),
|
||||||
);
|
);
|
||||||
values[key] = await bcrypt.hash(values[key], rounds);
|
values[key] = await bcrypt.hash(values[key], rounds);
|
||||||
}
|
}
|
||||||
@@ -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")
|
||||||
@@ -186,14 +228,14 @@ module.exports = (plugin) => {
|
|||||||
|
|
||||||
if (fullUser.contacts && Array.isArray(fullUser.contacts)) {
|
if (fullUser.contacts && Array.isArray(fullUser.contacts)) {
|
||||||
fullUser.contacts = fullUser.contacts.filter(
|
fullUser.contacts = fullUser.contacts.filter(
|
||||||
(contact) => contact.state === "accepted"
|
(contact) => contact.state === "accepted",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullUser.post_ownerships && Array.isArray(fullUser.post_ownerships)) {
|
if (fullUser.post_ownerships && Array.isArray(fullUser.post_ownerships)) {
|
||||||
fullUser.post_ownerships = fullUser.post_ownerships.filter(
|
fullUser.post_ownerships = fullUser.post_ownerships.filter(
|
||||||
(ownership) =>
|
(ownership) =>
|
||||||
ownership.contextType === "user" && ownership.relation === "owner"
|
ownership.contextType === "user" && ownership.relation === "owner",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,62 +244,15 @@ module.exports = (plugin) => {
|
|||||||
Array.isArray(fullUser.group_memberships)
|
Array.isArray(fullUser.group_memberships)
|
||||||
) {
|
) {
|
||||||
fullUser.group_memberships = fullUser.group_memberships.filter(
|
fullUser.group_memberships = fullUser.group_memberships.filter(
|
||||||
(membership) => ["member", "admin", "owner"].includes(membership.role)
|
(membership) => ["member", "admin", "owner"].includes(membership.role),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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: {
|
||||||
@@ -324,29 +318,8 @@ module.exports = (plugin) => {
|
|||||||
},
|
},
|
||||||
activities: 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
|
const eventRelationships = await strapi.db
|
||||||
.query("api::event-relationship.event-relationship")
|
.query("api::event-relationship.event-relationship")
|
||||||
@@ -356,84 +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 (ctx, label: string, username: string) => {
|
// Removed unused uploadImage function
|
||||||
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.services.providers = providers;
|
||||||
|
|
||||||
@@ -444,7 +361,14 @@ module.exports = (plugin) => {
|
|||||||
return ctx.unauthorized();
|
return ctx.unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = JSON.parse(ctx.request.body.data);
|
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, [
|
const newData = lod.pick(data, [
|
||||||
"email",
|
"email",
|
||||||
@@ -463,6 +387,7 @@ module.exports = (plugin) => {
|
|||||||
"languages",
|
"languages",
|
||||||
"parameter",
|
"parameter",
|
||||||
"privacy",
|
"privacy",
|
||||||
|
"avatar",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -506,7 +431,7 @@ module.exports = (plugin) => {
|
|||||||
ctx.params = { id: user.id };
|
ctx.params = { id: user.id };
|
||||||
|
|
||||||
const keysExcludingParameterAndPrivacy = Object.keys(newData).filter(
|
const keysExcludingParameterAndPrivacy = Object.keys(newData).filter(
|
||||||
(key) => key !== "parameter" && key !== "privacy"
|
(key) => key !== "parameter" && key !== "privacy",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (keysExcludingParameterAndPrivacy.length === 0) {
|
if (keysExcludingParameterAndPrivacy.length === 0) {
|
||||||
@@ -520,9 +445,6 @@ module.exports = (plugin) => {
|
|||||||
return ctx;
|
return ctx;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const avatarId = await uploadImage(ctx, "avatar", newData.username);
|
|
||||||
if (avatarId != 0) ctx.request.body.avatar = avatarId;
|
|
||||||
|
|
||||||
return plugin.controllers.user.update(ctx);
|
return plugin.controllers.user.update(ctx);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -533,14 +455,12 @@ module.exports = (plugin) => {
|
|||||||
|
|
||||||
await sendConfirmationEmail(result);
|
await sendConfirmationEmail(result);
|
||||||
|
|
||||||
await strapi
|
await strapi.service("api::notification.notification")?.addActivity({
|
||||||
.service("api::notification.notification")
|
userId: result.id,
|
||||||
?.addActivity({
|
activityMessage: `Bienvenue ${result.username}, Ton compte est maintenant activé.`,
|
||||||
userId: result.id,
|
activityUser: result.username,
|
||||||
activityMessage: `Bienvenue ${result.username}, Ton compte est maintenant activé.`,
|
activityType: "user",
|
||||||
activityUser: result.username,
|
});
|
||||||
activityType: "user",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Appel du service de notification
|
// Appel du service de notification
|
||||||
await strapi
|
await strapi
|
||||||
@@ -548,14 +468,18 @@ 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(
|
||||||
`🔔 Notification envoyée pour l'utilisateur ${result.id}`
|
`🔔 Notification envoyée pour l'utilisateur ${result.id}`,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+28
-1
@@ -560,6 +560,7 @@ export interface ApiBoardBoard extends Struct.CollectionTypeSchema {
|
|||||||
export interface ApiChannelChannel extends Struct.CollectionTypeSchema {
|
export interface ApiChannelChannel extends Struct.CollectionTypeSchema {
|
||||||
collectionName: 'channels';
|
collectionName: 'channels';
|
||||||
info: {
|
info: {
|
||||||
|
description: '';
|
||||||
displayName: 'Channel';
|
displayName: 'Channel';
|
||||||
pluralName: 'channels';
|
pluralName: 'channels';
|
||||||
singularName: 'channel';
|
singularName: 'channel';
|
||||||
@@ -572,6 +573,7 @@ export interface ApiChannelChannel extends Struct.CollectionTypeSchema {
|
|||||||
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;
|
||||||
|
description: Schema.Attribute.Text;
|
||||||
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
localizations: Schema.Attribute.Relation<
|
localizations: Schema.Attribute.Relation<
|
||||||
'oneToMany',
|
'oneToMany',
|
||||||
@@ -580,6 +582,7 @@ export interface ApiChannelChannel extends Struct.CollectionTypeSchema {
|
|||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
messages: Schema.Attribute.Relation<'oneToMany', 'api::message.message'>;
|
messages: Schema.Attribute.Relation<'oneToMany', 'api::message.message'>;
|
||||||
name: Schema.Attribute.String;
|
name: Schema.Attribute.String;
|
||||||
|
permissions: Schema.Attribute.JSON;
|
||||||
publishedAt: Schema.Attribute.DateTime;
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
type: Schema.Attribute.Enumeration<['TEXT', 'AUDIO', 'VIDEO']>;
|
type: Schema.Attribute.Enumeration<['TEXT', 'AUDIO', 'VIDEO']>;
|
||||||
updatedAt: Schema.Attribute.DateTime;
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
@@ -723,15 +726,22 @@ 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',
|
||||||
'api::choral-membership.choral-membership'
|
'api::choral-membership.choral-membership'
|
||||||
> &
|
> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
|
permission_exceptions: Schema.Attribute.JSON;
|
||||||
permissions: Schema.Attribute.Component<'user.permissions', true>;
|
permissions: Schema.Attribute.Component<'user.permissions', true>;
|
||||||
publishedAt: Schema.Attribute.DateTime;
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
role: Schema.Attribute.Enumeration<['member', 'admin', 'owner']>;
|
role: Schema.Attribute.Enumeration<['member', 'admin', 'owner']>;
|
||||||
|
role_id: Schema.Attribute.String;
|
||||||
|
state: Schema.Attribute.Enumeration<
|
||||||
|
['active', 'pending_user_approval', 'pending_admin_approval', 'decline']
|
||||||
|
>;
|
||||||
updatedAt: Schema.Attribute.DateTime;
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
@@ -799,6 +809,7 @@ export interface ApiChoralChoral extends Struct.CollectionTypeSchema {
|
|||||||
'oneToMany',
|
'oneToMany',
|
||||||
'api::announcement.announcement'
|
'api::announcement.announcement'
|
||||||
>;
|
>;
|
||||||
|
available_roles: Schema.Attribute.JSON;
|
||||||
available_tags: Schema.Attribute.JSON;
|
available_tags: Schema.Attribute.JSON;
|
||||||
boards: Schema.Attribute.Relation<'oneToMany', 'api::board.board'>;
|
boards: Schema.Attribute.Relation<'oneToMany', 'api::board.board'>;
|
||||||
calendar: Schema.Attribute.Relation<'oneToMany', 'api::event.event'>;
|
calendar: Schema.Attribute.Relation<'oneToMany', 'api::event.event'>;
|
||||||
@@ -809,13 +820,16 @@ export interface ApiChoralChoral extends Struct.CollectionTypeSchema {
|
|||||||
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;
|
||||||
|
description: Schema.Attribute.Text;
|
||||||
email: Schema.Attribute.Email;
|
email: Schema.Attribute.Email;
|
||||||
|
founded: Schema.Attribute.Integer;
|
||||||
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
localizations: Schema.Attribute.Relation<
|
localizations: Schema.Attribute.Relation<
|
||||||
'oneToMany',
|
'oneToMany',
|
||||||
'api::choral.choral'
|
'api::choral.choral'
|
||||||
> &
|
> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
|
logo: Schema.Attribute.Media<'images' | 'files' | 'videos' | 'audios'>;
|
||||||
memberships: Schema.Attribute.Relation<
|
memberships: Schema.Attribute.Relation<
|
||||||
'oneToMany',
|
'oneToMany',
|
||||||
'api::choral-membership.choral-membership'
|
'api::choral-membership.choral-membership'
|
||||||
@@ -832,6 +846,7 @@ export interface ApiChoralChoral extends Struct.CollectionTypeSchema {
|
|||||||
phoneNumber: Schema.Attribute.String;
|
phoneNumber: Schema.Attribute.String;
|
||||||
postal: Schema.Attribute.Integer;
|
postal: Schema.Attribute.Integer;
|
||||||
publishedAt: Schema.Attribute.DateTime;
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
social_networks: Schema.Attribute.JSON;
|
||||||
updatedAt: Schema.Attribute.DateTime;
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
@@ -1359,6 +1374,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'> &
|
||||||
@@ -1395,7 +1416,12 @@ export interface ApiMessageMessage extends Struct.CollectionTypeSchema {
|
|||||||
'api::message.message'
|
'api::message.message'
|
||||||
> &
|
> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
|
parent_message: Schema.Attribute.Relation<
|
||||||
|
'oneToOne',
|
||||||
|
'api::message.message'
|
||||||
|
>;
|
||||||
publishedAt: Schema.Attribute.DateTime;
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
reactions: Schema.Attribute.JSON;
|
||||||
updatedAt: Schema.Attribute.DateTime;
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
@@ -1945,6 +1971,7 @@ export interface PluginUploadFile extends Struct.CollectionTypeSchema {
|
|||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
mime: Schema.Attribute.String & Schema.Attribute.Required;
|
mime: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
name: Schema.Attribute.String & Schema.Attribute.Required;
|
name: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
path: Schema.Attribute.String;
|
||||||
previewUrl: Schema.Attribute.String;
|
previewUrl: Schema.Attribute.String;
|
||||||
provider: Schema.Attribute.String & Schema.Attribute.Required;
|
provider: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
provider_metadata: Schema.Attribute.JSON;
|
provider_metadata: Schema.Attribute.JSON;
|
||||||
@@ -2122,7 +2149,6 @@ export interface PluginUsersPermissionsUser
|
|||||||
attributes: {
|
attributes: {
|
||||||
activities: Schema.Attribute.Component<'group.activity', true>;
|
activities: Schema.Attribute.Component<'group.activity', true>;
|
||||||
address: Schema.Attribute.Text;
|
address: Schema.Attribute.Text;
|
||||||
admin_tags: Schema.Attribute.JSON;
|
|
||||||
announcements: Schema.Attribute.Relation<
|
announcements: Schema.Attribute.Relation<
|
||||||
'oneToMany',
|
'oneToMany',
|
||||||
'api::announcement.announcement'
|
'api::announcement.announcement'
|
||||||
@@ -2216,6 +2242,7 @@ export interface PluginUsersPermissionsUser
|
|||||||
'plugin::users-permissions.role'
|
'plugin::users-permissions.role'
|
||||||
>;
|
>;
|
||||||
saved_posts: Schema.Attribute.Relation<'oneToMany', 'api::post.post'>;
|
saved_posts: Schema.Attribute.Relation<'oneToMany', 'api::post.post'>;
|
||||||
|
smartTags: Schema.Attribute.JSON;
|
||||||
subscriptionPlan: Schema.Attribute.Enumeration<['free', 'pro', 'premium']> &
|
subscriptionPlan: Schema.Attribute.Enumeration<['free', 'pro', 'premium']> &
|
||||||
Schema.Attribute.DefaultTo<'free'>;
|
Schema.Attribute.DefaultTo<'free'>;
|
||||||
surname: Schema.Attribute.String;
|
surname: Schema.Attribute.String;
|
||||||
|
|||||||
Reference in New Issue
Block a user