Compare commits

...

17 Commits

Author SHA1 Message Date
0648bf74bd 0.13.9 : change smartTags name
Some checks failed
Build release Docker image / Build Docker Images (push) Failing after 10m34s
2026-04-13 21:56:07 +02:00
d469fc41e8 0.13.8 : invite by mail or contact and change mail provider on local
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 10m42s
2026-04-07 22:15:59 +02:00
5af0f28b39 Fix controller to remove upload
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 5m43s
2026-04-06 16:42:54 +02:00
fd14d20dd0 fix
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 5m27s
2026-04-06 00:52:32 +02:00
7d020c33b6 Fix add logo 2026-04-06 00:52:13 +02:00
1a3ea9e0c1 0.13.7 : add logo to choral
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 16m39s
2026-04-06 00:10:27 +02:00
b0db6a5ddf 0.13.6 : change choral-membership
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 9m43s
2026-03-15 19:37:44 +01:00
f13efb8707 0.13.5 : add roles
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 10m33s
2026-03-15 10:48:07 +01:00
75c90aa042 Change schema
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 5m24s
2026-03-11 18:01:09 +01:00
616b14edca 0.13.4 : add form-template
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 10m31s
2026-03-08 16:07:54 +01:00
42604b037b Fix quick dockerfile
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 5m29s
2026-03-05 19:52:07 +01:00
297dea3925 Dokerfile optim speed
Some checks failed
Build release Docker image / Build Docker Images (push) Failing after 6m55s
2026-03-05 19:42:05 +01:00
1f50e645ac Fix dockerfile
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 18m57s
2026-03-05 19:21:14 +01:00
56b913e888 Fix node version
Some checks failed
Build release Docker image / Build Docker Images (push) Failing after 4m43s
2026-03-05 19:14:48 +01:00
ce9d05cd5c Fix dockerfile
Some checks failed
Build release Docker image / Build Docker Images (push) Failing after 2m25s
2026-03-05 18:58:28 +01:00
d4e6a1192e Fix build
Some checks failed
Build release Docker image / Build Docker Images (push) Failing after 2m26s
2026-03-05 18:53:07 +01:00
ba0c076f70 Fix build 2026-03-05 18:52:55 +01:00
20 changed files with 25571 additions and 21513 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
# .dockerignore
node_modules
dist
.tmp
.cache
.env
build

View File

@@ -1,30 +1,36 @@
# 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 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 ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV} 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 WORKDIR /opt/app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . . COPY . .
RUN npm run build
# Creating final production image # On force le build de l'admin explicitement pour être sûr que le dossier existe
FROM node:18-alpine RUN yarn build
RUN apk add --no-cache vips-dev
# --- ÉTAPE 2 : RUNTIME ---
FROM node:20-alpine AS runtime-stage
RUN apk add --no-cache vips-dev libpng libjpeg-turbo
ARG NODE_ENV=production ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV} ENV NODE_ENV=${NODE_ENV}
WORKDIR /opt/
COPY --from=build /opt/node_modules ./node_modules
WORKDIR /opt/app
COPY --from=build /opt/app ./
ENV PATH=/opt/node_modules/.bin:$PATH
RUN chown -R node:node /opt/app WORKDIR /opt/app
# ASTUCE : On copie tout l'objet de build avec --chown
# Cela évite de lister les dossiers un par un et de risquer un "Not Found"
COPY --from=build-stage --chown=node:node /opt/app ./
# On expose le binaire
ENV PATH=/opt/app/node_modules/.bin:$PATH
USER node USER node
EXPOSE 1337 EXPOSE 1337
CMD ["npm", "run", "start"]
CMD ["yarn", "start"]

View File

@@ -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",
}, },
}, },
}, },

21472
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "harmony-back", "name": "harmony-back",
"version": "0.13.3", "version": "0.13.9",
"private": true, "private": true,
"description": "A Strapi application", "description": "A Strapi application",
"scripts": { "scripts": {

View File

@@ -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
} }
} }
} }

View File

@@ -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
} }
} }
} }

View File

@@ -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;
},
}),
);

View File

@@ -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
} }
} }
} }

View File

@@ -33,5 +33,5 @@ export default factories.createCoreController(
const result = await super.create(ctx); const result = await super.create(ctx);
return result; return result;
}, },
}) }),
); );

View File

@@ -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"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* form-template controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::form-template.form-template');

View File

@@ -0,0 +1,7 @@
/**
* form-template router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::form-template.form-template');

View File

@@ -0,0 +1,7 @@
/**
* form-template service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::form-template.form-template');

View File

@@ -25,6 +25,16 @@
} }
}, },
"component": "mail.mail" "component": "mail.mail"
},
"onInviteUser": {
"type": "component",
"repeatable": true,
"pluginOptions": {
"i18n": {
"localized": true
}
},
"component": "mail.mail"
} }
} }
} }

View File

@@ -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;
}
},
}),
);

View File

@@ -264,6 +264,9 @@
"relation": "oneToMany", "relation": "oneToMany",
"target": "api::order.order", "target": "api::order.order",
"mappedBy": "user" "mappedBy": "user"
},
"smartTags": {
"type": "json"
} }
} }
} }

View File

@@ -723,15 +723,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 +806,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 +817,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 +843,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 +1157,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 +1371,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'> &
@@ -2172,6 +2233,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 +2293,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;

12533
yarn.lock Normal file

File diff suppressed because it is too large Load Diff