0.11.6 : add event and export ICS

This commit is contained in:
2025-10-20 23:52:37 +02:00
parent 089f8f3ee8
commit 8e5182afaa
15 changed files with 5983 additions and 35 deletions

View File

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

View File

@@ -28,14 +28,14 @@
"enum": [ "enum": [
"concert", "concert",
"festival", "festival",
"salon", "trade show",
"symposium", "symposium",
"concours", "competition",
"masterclass", "masterclass",
"atelier vocal", "vocal_workshop",
"conférence", "conference",
"répétition", "rehearsal",
"sorties" "outings"
] ]
} }
} }

View File

@@ -0,0 +1,49 @@
{
"kind": "collectionType",
"collectionName": "event_relationships",
"info": {
"singularName": "event-relationship",
"pluralName": "event-relationships",
"displayName": "EventRelationship",
"description": ""
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"author": {
"type": "relation",
"relation": "oneToOne",
"target": "plugin::users-permissions.user"
},
"contextType": {
"type": "enumeration",
"enum": [
"user",
"group",
"choral",
"system"
]
},
"contextId": {
"type": "integer"
},
"relation": {
"type": "enumeration",
"enum": [
"owner",
"interested",
"registered"
]
},
"metas": {
"type": "json"
},
"event": {
"type": "relation",
"relation": "oneToOne",
"target": "api::event.event"
}
}
}

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,40 @@
}, },
"location": { "location": {
"type": "string" "type": "string"
},
"type": {
"type": "enumeration",
"enum": [
"concert",
"festival",
"trade show",
"symposium",
"competition",
"masterclass",
"vocal_workshop",
"conference",
"rehearsal",
"outings"
]
},
"voice": {
"type": "enumeration",
"enum": [
"soprano",
"alto",
"tenor",
"bass",
"mixed"
]
},
"description": {
"type": "text"
},
"size": {
"type": "integer"
},
"isPublic": {
"type": "boolean"
} }
} }
} }

View File

@@ -0,0 +1,93 @@
import type { Core } from "@strapi/strapi";
import type { Context } from "koa";
/**
* Controller pour gérer l'inscription d'un utilisateur à un événement
* POST /events/:eventId/apply
*/
export default ({ strapi }: { strapi: Core.Strapi }) =>
async function applyEvent(ctx: Context) {
const userId = ctx.state.user?.id;
const eventId = ctx.params.eventId;
// Validation des paramètres
if (!userId) {
return ctx.badRequest("User ID is required");
}
if (!eventId || isNaN(parseInt(eventId))) {
return ctx.badRequest("Event ID is required and must be a valid number");
}
try {
// Vérifier que l'événement existe
const event = await strapi.db.query("api::event.event").findOne({
where: { id: parseInt(eventId) },
});
if (!event) {
return ctx.notFound("Event not found");
}
// Vérifier que l'utilisateur n'est pas déjà inscrit
const existingRelationship = await strapi.db
.query("api::event-relationship.event-relationship")
.findOne({
where: {
event: { id: parseInt(eventId) },
contextType: "user",
contextId: parseInt(userId),
relation: { $in: ["registered", "interested"] },
},
});
if (existingRelationship) {
return ctx.badRequest(
"User is already registered or interested in this event"
);
}
// Créer la nouvelle relation d'inscription
const eventRelationship = await strapi.db
.query("api::event-relationship.event-relationship")
.create({
data: {
author: parseInt(userId),
event: parseInt(eventId),
contextType: "user",
contextId: parseInt(userId),
relation: "registered",
metas: {
appliedAt: new Date().toISOString(),
},
},
});
// Populer les données pour la réponse
const populatedRelationship = await strapi.db
.query("api::event-relationship.event-relationship")
.findOne({
where: { id: eventRelationship.id },
populate: {
event: {
populate: {
choral: true,
},
},
author: {
populate: {
avatar: true,
},
},
},
});
ctx.send({
data: populatedRelationship,
message: "Successfully registered for the event",
});
} catch (error) {
strapi.log.error("Error in applyEvent controller:", error);
ctx.internalServerError(`Failed to register for event: ${error.message}`);
}
};

View File

@@ -2,6 +2,39 @@
* event controller * event controller
*/ */
import { factories } from '@strapi/strapi' import { factories } from "@strapi/strapi";
import fs from "fs";
import path from "path";
export default factories.createCoreController('api::event.event'); export default factories.createCoreController(
"api::event.event",
({ strapi }) => {
const controllersDir = __dirname;
const controllers: Record<string, any> = {};
fs.readdirSync(controllersDir).forEach((file) => {
// Ignorer le contrôleur principal + fichiers map
if (file.startsWith("event.") || file.endsWith(".map")) return;
// On accepte .ts et .js
const ext = path.extname(file);
if (![".ts", ".js"].includes(ext)) return;
const name = path.basename(file, ext);
const modulePath = path.join(controllersDir, file);
const module = require(modulePath);
const handler = module.default;
if (typeof handler === "function") {
controllers[name] = handler({ strapi });
}
});
console.log(Object.keys(controllers));
return {
// ✅ injection automatique des customs
...controllers,
} as any;
}
);

View File

@@ -0,0 +1,278 @@
import type { Core } from "@strapi/strapi";
import type { Context } from "koa";
export default ({ strapi }: { strapi: Core.Strapi }) =>
async function feed(ctx: Context) {
const userId = ctx.state.user?.id;
if (!userId) return ctx.badRequest("userId is required");
// Récupérer et valider la query avec les fonctions de Strapi
const contentType = strapi.contentType(
"api::event-relationship.event-relationship"
);
let limit: number;
let start: number;
try {
// Sanitize la query
await strapi.contentAPI.validate.query(ctx.query, contentType, {
auth: ctx.state.auth,
});
const sanitizedQueryParams = await strapi.contentAPI.sanitize.query(
ctx.query,
contentType,
{ auth: ctx.state.auth }
);
const { _limit = 20, _start = 0 } = sanitizedQueryParams;
limit = Math.max(1, Math.min(parseInt(String(_limit)) || 20, 100)); // Entre 1 et 100
start = Math.max(0, parseInt(String(_start)) || 0); // Minimum 0
} catch (error) {
return ctx.badRequest(`Invalid query parameters: ${error.message}`);
}
const queryParams = {
populate: {
event: {
populate: {
choral: true,
},
},
author: {
populate: {
avatar: true,
},
},
},
sort: { createdAt: "desc" },
pagination: {
start,
limit,
},
};
// 1⃣ Récupérer les groupes de l'utilisateur
const groups = await strapi.db
.query("api::group-membership.group-membership")
.findMany({
where: { user: { id: parseInt(userId) } },
populate: ["group"],
});
const groupIds = groups.map((g) => g.group.id);
// 2⃣ Récupérer les amis (contacts acceptés) et les follows
const friendsContacts = await strapi.db
.query("api::contact.contact")
.findMany({
where: {
$or: [
{ owner: { id: parseInt(userId) } },
{ user: { id: parseInt(userId) } },
],
state: "accepted",
},
populate: ["owner", "user"],
});
const friendIds = friendsContacts.map((c) =>
c.owner.id !== parseInt(userId) ? c.owner.id : c.user.id
);
// Récupérer les contacts suivis (follow)
const followContacts = await strapi.db
.query("api::contact.contact")
.findMany({
where: {
owner: { id: parseInt(userId) },
state: "follow",
},
populate: ["user"],
});
const followIds = followContacts.map((c) => c.user.id);
// 3⃣ Récupérer les groupes où mes amis sont membres (en excluant mes propres groupes)
const friendsGroupMemberships = await strapi.db
.query("api::group-membership.group-membership")
.findMany({
where: { user: { id: friendIds } },
populate: ["group"],
});
const friendsGroupIds = friendsGroupMemberships
.map((membership) => membership.group.id)
.filter((groupId) => !groupIds.includes(groupId)); // Exclure mes propres groupes
// 4⃣ Récupérer le count total pour la pagination
const totalCount = await strapi.db
.query("api::event-relationship.event-relationship")
.count({
where: {
relation: "owner",
$or: [
// EventRelationships de l'utilisateur courant
{ author: { id: parseInt(userId) } },
// EventRelationships des amis
{ author: { id: friendIds } },
// EventRelationships des contacts suivis
{ author: { id: followIds } },
// EventRelationships des groupes de l'utilisateur
{ contextType: "group", contextId: groupIds },
// EventRelationships des groupes des amis
{ contextType: "group", contextId: friendsGroupIds },
// EventRelationships système
{ contextType: "system" },
],
},
});
// 4⃣ Récupérer le feed avec pagination
const feed = await strapi.db
.query("api::event-relationship.event-relationship")
.findMany({
where: {
relation: "owner",
$or: [
// EventRelationships de l'utilisateur courant
{ author: { id: parseInt(userId) } },
// EventRelationships des amis
{ author: { id: friendIds } },
// EventRelationships des contacts suivis
{ author: { id: followIds } },
// EventRelationships des groupes de l'utilisateur
{ contextType: "group", contextId: groupIds },
// EventRelationships des groupes des amis
{ contextType: "group", contextId: friendsGroupIds },
// EventRelationships système
{ contextType: "system" },
],
},
...queryParams,
});
// 5⃣ Récupérer tous les groupes mentionnés dans le feed pour les populer
const allGroupIds = [...new Set([...groupIds, ...friendsGroupIds])];
const allGroups = await strapi.db.query("api::group.group").findMany({
where: { id: allGroupIds },
});
// Créer un map pour un accès rapide aux groupes par ID
const groupsMap = new Map(allGroups.map((group) => [group.id, group]));
// 6⃣ Récupérer les participants (registered) pour chaque événement du feed
const eventIds = feed.filter((er) => er.event?.id).map((er) => er.event.id);
const registeredRelationships = await strapi.db
.query("api::event-relationship.event-relationship")
.findMany({
where: {
event: { id: eventIds },
relation: { $in: ["registered", "interested", "pending"] },
contextType: "user",
},
populate: ["event"],
});
// Extraire les userIds et récupérer les Users complètement populés
const userIds = [
...new Set(registeredRelationships.map((er) => er.contextId)),
];
const users = await strapi.db
.query("plugin::users-permissions.user")
.findMany({
where: { id: userIds },
populate: {
avatar: true,
},
});
// Créer une map des users par ID pour accès rapide
const usersMap = new Map(users.map((user) => [user.id, user]));
// Grouper les participants par eventId
const participantsByEventId = new Map<number, any[]>();
registeredRelationships.forEach((er) => {
const eventId = er.event?.id;
if (eventId) {
if (!participantsByEventId.has(eventId)) {
participantsByEventId.set(eventId, []);
}
const user = usersMap.get(er.contextId);
if (user) {
participantsByEventId.get(eventId)!.push({
relation: er.relation,
createdAt: er.createdAt,
user: user,
});
}
}
});
// 7⃣ Enrichir le feed avec les propriétés friend, member, contactFollow, group et registered participants
const enrichedFeed = feed.map((eventRelationship) => {
const authorId = eventRelationship.author?.id;
const contextType = eventRelationship.contextType;
const contextId = eventRelationship.contextId;
const eventId = eventRelationship.event?.id;
// Vérifier si l'auteur est un ami
const isFriend = authorId ? friendIds.includes(authorId) : false;
// Vérifier si l'auteur est un contact suivi
const isContactFollow = authorId ? followIds.includes(authorId) : false;
// Vérifier si je suis membre du groupe (seulement pour les events de groupe)
const isMember =
contextType === "group" && contextId
? groupIds.includes(contextId)
: false;
// Ajouter l'objet group si contextType est "group"
const group =
contextType === "group" && contextId ? groupsMap.get(contextId) : null;
// Récupérer les participants enregistrés pour cet événement
const registered = eventId
? participantsByEventId.get(eventId) || []
: [];
return {
...eventRelationship,
friend: isFriend,
contactFollow: isContactFollow,
member: isMember,
...(group && { group }), // Ajouter group seulement s'il existe
registered, // Ajouter les participants enregistrés
};
});
// Trier par createdAt (le plus récent en premier)
const sortedFeed = enrichedFeed.sort((a, b) => {
const dateA = a.event?.createdAt
? new Date(a.event.createdAt).getTime()
: 0;
const dateB = b.event?.createdAt
? new Date(b.event.createdAt).getTime()
: 0;
return dateB - dateA; // Ordre décroissant (plus récent en premier)
});
// Calculer les métadonnées de pagination comme Strapi
const pageSize = limit;
const page = Math.floor(start / limit) + 1;
const pageCount = Math.ceil(totalCount / limit);
// Retourner avec les métadonnées de pagination
ctx.send({
data: sortedFeed,
meta: {
pagination: {
start,
limit: pageSize,
total: totalCount,
page,
pageSize,
pageCount,
},
},
});
};

View File

@@ -0,0 +1,65 @@
import type { Core } from "@strapi/strapi";
import type { Context } from "koa";
/**
* Controller pour gérer la désinscription d'un utilisateur d'un événement
* POST /events/:eventId/unapply
*/
export default ({ strapi }: { strapi: Core.Strapi }) =>
async function unapplyEvent(ctx: Context) {
const userId = ctx.state.user?.id;
const eventId = ctx.params.eventId;
// Validation des paramètres
if (!userId) {
return ctx.badRequest("User ID is required");
}
if (!eventId || isNaN(parseInt(eventId))) {
return ctx.badRequest("Event ID is required and must be a valid number");
}
try {
// Vérifier que l'événement existe
const event = await strapi.db.query("api::event.event").findOne({
where: { id: parseInt(eventId) },
});
if (!event) {
return ctx.notFound("Event not found");
}
// Vérifier que l'utilisateur est inscrit
const existingRelationship = await strapi.db
.query("api::event-relationship.event-relationship")
.findOne({
where: {
event: { id: parseInt(eventId) },
contextType: "user",
contextId: parseInt(userId),
relation: { $in: ["registered", "interested"] },
},
});
if (!existingRelationship) {
return ctx.badRequest("User is not registered for this event");
}
// Supprimer la relation d'inscription
const deletedRelationship = await strapi.db
.query("api::event-relationship.event-relationship")
.delete({
where: { id: existingRelationship.id },
});
ctx.send({
data: deletedRelationship,
message: "Successfully unregistered from the event",
});
} catch (error) {
strapi.log.error("Error in unapplyEvent controller:", error);
ctx.internalServerError(
`Failed to unregister from event: ${error.message}`
);
}
};

View File

@@ -0,0 +1,23 @@
/**
* Custom event routes
*/
export default {
routes: [
{
method: "GET",
path: "/events/feed",
handler: "event.feed",
},
{
method: "POST",
path: "/events/:eventId/apply",
handler: "event.applyEvent",
},
{
method: "POST",
path: "/events/:eventId/unapply",
handler: "event.unapplyEvent",
},
],
};

View File

@@ -148,7 +148,7 @@ export default factories.createCoreController(
}); });
const followIds = followContacts.map((c) => c.user.id); const followIds = followContacts.map((c) => c.user.id);
// Récupérer les contacts suivis (follow) // Récupérer les contacts bloqués (blocked)
const blockedContacts = await strapi.db const blockedContacts = await strapi.db
.query("api::contact.contact") .query("api::contact.contact")
.findMany({ .findMany({

View File

@@ -947,14 +947,14 @@ export interface ApiEventOtherEventOther extends Struct.CollectionTypeSchema {
[ [
'concert', 'concert',
'festival', 'festival',
'salon', 'trade show',
'symposium', 'symposium',
'concours', 'competition',
'masterclass', 'masterclass',
'atelier vocal', 'vocal_workshop',
'conf\u00E9rence', 'conference',
'r\u00E9p\u00E9tition', 'rehearsal',
'sorties', 'outings',
] ]
>; >;
updatedAt: Schema.Attribute.DateTime; updatedAt: Schema.Attribute.DateTime;
@@ -963,6 +963,48 @@ export interface ApiEventOtherEventOther extends Struct.CollectionTypeSchema {
}; };
} }
export interface ApiEventRelationshipEventRelationship
extends Struct.CollectionTypeSchema {
collectionName: 'event_relationships';
info: {
description: '';
displayName: 'EventRelationship';
pluralName: 'event-relationships';
singularName: 'event-relationship';
};
options: {
draftAndPublish: false;
};
attributes: {
author: Schema.Attribute.Relation<
'oneToOne',
'plugin::users-permissions.user'
>;
contextId: Schema.Attribute.Integer;
contextType: Schema.Attribute.Enumeration<
['user', 'group', 'choral', 'system']
>;
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
event: Schema.Attribute.Relation<'oneToOne', 'api::event.event'>;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<
'oneToMany',
'api::event-relationship.event-relationship'
> &
Schema.Attribute.Private;
metas: Schema.Attribute.JSON;
publishedAt: Schema.Attribute.DateTime;
relation: Schema.Attribute.Enumeration<
['owner', 'interested', 'registered']
>;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
};
}
export interface ApiEventEvent extends Struct.CollectionTypeSchema { export interface ApiEventEvent extends Struct.CollectionTypeSchema {
collectionName: 'events'; collectionName: 'events';
info: { info: {
@@ -981,17 +1023,37 @@ export interface ApiEventEvent 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;
end: Schema.Attribute.DateTime; end: Schema.Attribute.DateTime;
isPublic: Schema.Attribute.Boolean;
locale: Schema.Attribute.String & Schema.Attribute.Private; locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<'oneToMany', 'api::event.event'> & localizations: Schema.Attribute.Relation<'oneToMany', 'api::event.event'> &
Schema.Attribute.Private; Schema.Attribute.Private;
location: Schema.Attribute.String; location: Schema.Attribute.String;
publishedAt: Schema.Attribute.DateTime; publishedAt: Schema.Attribute.DateTime;
size: Schema.Attribute.Integer;
start: Schema.Attribute.DateTime; start: Schema.Attribute.DateTime;
title: Schema.Attribute.String; title: Schema.Attribute.String;
type: Schema.Attribute.Enumeration<
[
'concert',
'festival',
'trade show',
'symposium',
'competition',
'masterclass',
'vocal_workshop',
'conference',
'rehearsal',
'outings',
]
>;
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;
voice: Schema.Attribute.Enumeration<
['soprano', 'alto', 'tenor', 'bass', 'mixed']
>;
}; };
} }
@@ -1886,6 +1948,7 @@ declare module '@strapi/strapi' {
'api::conversation.conversation': ApiConversationConversation; 'api::conversation.conversation': ApiConversationConversation;
'api::direct-message.direct-message': ApiDirectMessageDirectMessage; 'api::direct-message.direct-message': ApiDirectMessageDirectMessage;
'api::event-other.event-other': ApiEventOtherEventOther; 'api::event-other.event-other': ApiEventOtherEventOther;
'api::event-relationship.event-relationship': ApiEventRelationshipEventRelationship;
'api::event.event': ApiEventEvent; 'api::event.event': ApiEventEvent;
'api::group-membership.group-membership': ApiGroupMembershipGroupMembership; 'api::group-membership.group-membership': ApiGroupMembershipGroupMembership;
'api::group.group': ApiGroupGroup; 'api::group.group': ApiGroupGroup;