Compare commits
16 Commits
d4e6a1192e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f7f0ee17d7 | |||
| 0648bf74bd | |||
| d469fc41e8 | |||
| 5af0f28b39 | |||
| fd14d20dd0 | |||
| 7d020c33b6 | |||
| 1a3ea9e0c1 | |||
| b0db6a5ddf | |||
| f13efb8707 | |||
| 75c90aa042 | |||
| 616b14edca | |||
| 42604b037b | |||
| 297dea3925 | |||
| 1f50e645ac | |||
| 56b913e888 | |||
| ce9d05cd5c |
71
Dockerfile
71
Dockerfile
@@ -1,58 +1,21 @@
|
|||||||
# Creating multi-stage build for production
|
# --- ÉTAPE 1 : BUILD ---
|
||||||
FROM node:18-alpine AS build
|
FROM node:20-alpine AS build-stage
|
||||||
RUN apk update && apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev vips-dev git > /dev/null 2>&1
|
|
||||||
ARG NODE_ENV=production
|
|
||||||
ENV NODE_ENV=${NODE_ENV}
|
|
||||||
|
|
||||||
WORKDIR /opt/
|
|
||||||
COPY package.json package-lock.json ./
|
|
||||||
RUN npm install -g node-gyp
|
|
||||||
RUN npm config set fetch-retry-maxtimeout 600000 -g && npm install --only=production
|
|
||||||
ENV PATH=/opt/node_modules/.bin:$PATH
|
|
||||||
WORKDIR /opt/app
|
|
||||||
COPY . .
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Creating final production image
|
|
||||||
FROM node:18-alpine
|
|
||||||
RUN apk add --no-cache vips-dev
|
|
||||||
ARG NODE_ENV=production
|
|
||||||
ENV NODE_ENV=${NODE_ENV}
|
|
||||||
WORKDIR /opt/
|
|
||||||
COPY --from=build /opt/node_modules ./node_modules
|
|
||||||
WORKDIR /opt/app# --- ÉTAPE 1 : BUILD ---
|
|
||||||
FROM node:18-alpine AS build
|
|
||||||
|
|
||||||
# Installation des dépendances système pour les modules natifs (Sharp, SWC, etc.)
|
|
||||||
RUN apk update && apk add --no-cache \
|
RUN apk update && apk add --no-cache \
|
||||||
build-base \
|
build-base gcc autoconf automake zlib-dev libpng-dev vips-dev git > /dev/null 2>&1
|
||||||
gcc \
|
|
||||||
autoconf \
|
|
||||||
automake \
|
|
||||||
zlib-dev \
|
|
||||||
libpng-dev \
|
|
||||||
vips-dev \
|
|
||||||
git > /dev/null 2>&1
|
|
||||||
|
|
||||||
ARG NODE_ENV=production
|
ARG NODE_ENV=production
|
||||||
ENV NODE_ENV=${NODE_ENV}
|
ENV NODE_ENV=${NODE_ENV}
|
||||||
|
|
||||||
WORKDIR /opt/
|
|
||||||
# On copie package.json ET yarn.lock (très important)
|
|
||||||
COPY package.json yarn.lock ./
|
|
||||||
|
|
||||||
# On installe TOUTES les dépendances (y compris devDependencies pour le build)
|
|
||||||
# --frozen-lockfile assure que l'on utilise exactement les versions du lock
|
|
||||||
RUN yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
RUN yarn install --frozen-lockfile
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# On build l'admin et le code TS
|
# On force le build de l'admin explicitement pour être sûr que le dossier existe
|
||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
|
||||||
# --- ÉTAPE 2 : RUNTIME ---
|
# --- ÉTAPE 2 : RUNTIME ---
|
||||||
FROM node:18-alpine
|
FROM node:20-alpine AS runtime-stage
|
||||||
RUN apk add --no-cache vips-dev libpng libjpeg-turbo
|
RUN apk add --no-cache vips-dev libpng libjpeg-turbo
|
||||||
|
|
||||||
ARG NODE_ENV=production
|
ARG NODE_ENV=production
|
||||||
@@ -60,24 +23,14 @@ ENV NODE_ENV=${NODE_ENV}
|
|||||||
|
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
|
|
||||||
# On récupère le dossier node_modules installé au stage précédent
|
# ASTUCE : On copie tout l'objet de build avec --chown
|
||||||
# Et le code buildé (dist, build, etc.)
|
# Cela évite de lister les dossiers un par un et de risquer un "Not Found"
|
||||||
COPY --from=build /opt/node_modules ./node_modules
|
COPY --from=build-stage --chown=node:node /opt/app ./
|
||||||
COPY --from=build /opt/app ./
|
|
||||||
|
|
||||||
# On expose le binaire de strapi dans le PATH
|
# On expose le binaire
|
||||||
ENV PATH=/opt/app/node_modules/.bin:$PATH
|
ENV PATH=/opt/app/node_modules/.bin:$PATH
|
||||||
|
|
||||||
RUN chown -R node:node /opt/app
|
|
||||||
USER node
|
USER node
|
||||||
|
|
||||||
EXPOSE 1337
|
EXPOSE 1337
|
||||||
# On utilise yarn start qui lancera strapi depuis le dossier dist
|
|
||||||
CMD ["yarn", "start"]
|
CMD ["yarn", "start"]
|
||||||
COPY --from=build /opt/app ./
|
|
||||||
ENV PATH=/opt/node_modules/.bin:$PATH
|
|
||||||
|
|
||||||
RUN chown -R node:node /opt/app
|
|
||||||
USER node
|
|
||||||
EXPOSE 1337
|
|
||||||
CMD ["npm", "run", "start"]
|
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "harmony-back",
|
"name": "harmony-back",
|
||||||
"version": "0.13.3",
|
"version": "0.13.10",
|
||||||
"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;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|||||||
@@ -100,6 +100,31 @@
|
|||||||
"relation": "oneToMany",
|
"relation": "oneToMany",
|
||||||
"target": "api::channel.channel",
|
"target": "api::channel.channel",
|
||||||
"mappedBy": "choral"
|
"mappedBy": "choral"
|
||||||
|
},
|
||||||
|
"available_tags": {
|
||||||
|
"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;
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"kind": "collectionType",
|
||||||
|
"collectionName": "form_templates",
|
||||||
|
"info": {
|
||||||
|
"singularName": "form-template",
|
||||||
|
"pluralName": "form-templates",
|
||||||
|
"displayName": "FormTemplate"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": false
|
||||||
|
},
|
||||||
|
"pluginOptions": {},
|
||||||
|
"attributes": {
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "enumeration",
|
||||||
|
"enum": [
|
||||||
|
"draft",
|
||||||
|
"published",
|
||||||
|
"archived"
|
||||||
|
],
|
||||||
|
"default": "draft"
|
||||||
|
},
|
||||||
|
"original_file": {
|
||||||
|
"allowedTypes": [
|
||||||
|
"images",
|
||||||
|
"files",
|
||||||
|
"videos",
|
||||||
|
"audios"
|
||||||
|
],
|
||||||
|
"type": "media",
|
||||||
|
"multiple": false
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
"target_tags": {
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
"choral": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "oneToOne",
|
||||||
|
"target": "api::choral.choral"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "oneToOne",
|
||||||
|
"target": "plugin::users-permissions.user"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/api/form-template/controllers/form-template.ts
Normal file
7
src/api/form-template/controllers/form-template.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* form-template controller
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi'
|
||||||
|
|
||||||
|
export default factories.createCoreController('api::form-template.form-template');
|
||||||
7
src/api/form-template/routes/form-template.ts
Normal file
7
src/api/form-template/routes/form-template.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* form-template router
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreRouter('api::form-template.form-template');
|
||||||
7
src/api/form-template/services/form-template.ts
Normal file
7
src/api/form-template/services/form-template.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* form-template service
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreService('api::form-template.form-template');
|
||||||
@@ -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;
|
||||||
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
9
src/api/message/routes/01-custom-message.ts
Normal file
9
src/api/message/routes/01-custom-message.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export default {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: "/messages/:id/react",
|
||||||
|
handler: "message.addReaction",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -264,6 +264,9 @@
|
|||||||
"relation": "oneToMany",
|
"relation": "oneToMany",
|
||||||
"target": "api::order.order",
|
"target": "api::order.order",
|
||||||
"mappedBy": "user"
|
"mappedBy": "user"
|
||||||
|
},
|
||||||
|
"smartTags": {
|
||||||
|
"type": "json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
71
types/generated/contentTypes.d.ts
vendored
71
types/generated/contentTypes.d.ts
vendored
@@ -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,8 @@ export interface ApiChoralChoral extends Struct.CollectionTypeSchema {
|
|||||||
'oneToMany',
|
'oneToMany',
|
||||||
'api::announcement.announcement'
|
'api::announcement.announcement'
|
||||||
>;
|
>;
|
||||||
|
available_roles: 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'>;
|
||||||
channels: Schema.Attribute.Relation<'oneToMany', 'api::channel.channel'>;
|
channels: Schema.Attribute.Relation<'oneToMany', 'api::channel.channel'>;
|
||||||
@@ -808,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'
|
||||||
@@ -831,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;
|
||||||
@@ -1144,6 +1160,48 @@ export interface ApiEventEvent extends Struct.CollectionTypeSchema {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiFormTemplateFormTemplate
|
||||||
|
extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'form_templates';
|
||||||
|
info: {
|
||||||
|
displayName: 'FormTemplate';
|
||||||
|
pluralName: 'form-templates';
|
||||||
|
singularName: 'form-template';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
author: Schema.Attribute.Relation<
|
||||||
|
'oneToOne',
|
||||||
|
'plugin::users-permissions.user'
|
||||||
|
>;
|
||||||
|
choral: Schema.Attribute.Relation<'oneToOne', 'api::choral.choral'>;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
description: Schema.Attribute.Text;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'api::form-template.form-template'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
original_file: Schema.Attribute.Media<
|
||||||
|
'images' | 'files' | 'videos' | 'audios'
|
||||||
|
>;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
schema: Schema.Attribute.JSON;
|
||||||
|
state: Schema.Attribute.Enumeration<['draft', 'published', 'archived']> &
|
||||||
|
Schema.Attribute.DefaultTo<'draft'>;
|
||||||
|
target_tags: Schema.Attribute.JSON;
|
||||||
|
title: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiGroupMembershipGroupMembership
|
export interface ApiGroupMembershipGroupMembership
|
||||||
extends Struct.CollectionTypeSchema {
|
extends Struct.CollectionTypeSchema {
|
||||||
collectionName: 'group_memberships';
|
collectionName: 'group_memberships';
|
||||||
@@ -1316,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'> &
|
||||||
@@ -1352,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;
|
||||||
@@ -2172,6 +2241,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;
|
||||||
@@ -2231,6 +2301,7 @@ declare module '@strapi/strapi' {
|
|||||||
'api::direct-message.direct-message': ApiDirectMessageDirectMessage;
|
'api::direct-message.direct-message': ApiDirectMessageDirectMessage;
|
||||||
'api::event-relationship.event-relationship': ApiEventRelationshipEventRelationship;
|
'api::event-relationship.event-relationship': ApiEventRelationshipEventRelationship;
|
||||||
'api::event.event': ApiEventEvent;
|
'api::event.event': ApiEventEvent;
|
||||||
|
'api::form-template.form-template': ApiFormTemplateFormTemplate;
|
||||||
'api::group-membership.group-membership': ApiGroupMembershipGroupMembership;
|
'api::group-membership.group-membership': ApiGroupMembershipGroupMembership;
|
||||||
'api::group.group': ApiGroupGroup;
|
'api::group.group': ApiGroupGroup;
|
||||||
'api::invite.invite': ApiInviteInvite;
|
'api::invite.invite': ApiInviteInvite;
|
||||||
|
|||||||
Reference in New Issue
Block a user