Lastest objects for post, invite and add cron GNEWS

This commit is contained in:
2025-09-26 13:22:07 +02:00
parent b6d18c2681
commit a84fdbed9a
21 changed files with 16320 additions and 5273 deletions

71
config/cron-tasks.ts Normal file
View File

@@ -0,0 +1,71 @@
interface GNewsArticle {
title: string;
description: string;
url: string;
image: string;
publishedAt: string;
source: {
name: string;
url: string;
};
}
type GNewsResponse = {
articles: GNewsArticle[];
};
export default {
/**
* Simple example.
* Every monday at 1am.
*/
GNEWS: {
task: async ({ strapi }) => {
// Add your own logic here (e.g. send a queue of email, create a database backup, etc.).
const API_KEY = "b3760544f6efb233f19d32fe78cc1300";
const keywords = ["chorale"].join("+");
const url = `https://gnews.io/api/v4/search?q=${keywords}&lang=fr&max=10&token=${API_KEY}`;
try {
const response = await fetch(url);
const data = (await response.json()) as GNewsResponse;
const articles: GNewsArticle[] = data.articles;
for (const article of articles) {
const alreadyExists = await strapi.db
.query("api::post.post")
.findOne({
where: { url: article.url },
});
if (!alreadyExists) {
await strapi.db.query("api::post.post").create({
data: {
title: article.title,
content: article.description,
url: article.url,
imageUrl: article.image,
source: article.source.name,
published_at: article.publishedAt,
//is_auto_generated: true,
category: "photo",
author: 14,
},
});
strapi.log.info(`🆕 Article créé : ${article.title}`);
} else {
strapi.log.info(`🔁 Article déjà existant : ${article.title}`);
}
}
} catch (error) {
strapi.log.error("❌ Erreur lors du fetch GNews :", error);
}
},
options: {
rule: "0 0 8 * * *",
},
},
};

View File

@@ -1,7 +1,13 @@
import cronTasks from "./cron-tasks";
export default ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
host: env("HOST", "0.0.0.0"),
port: env.int("PORT", 1337),
app: {
keys: env.array('APP_KEYS'),
keys: env.array("APP_KEYS"),
},
cron: {
enabled: true,
tasks: cronTasks,
},
});

View File

@@ -0,0 +1,36 @@
{
"kind": "collectionType",
"collectionName": "group_memberships",
"info": {
"singularName": "group-membership",
"pluralName": "group-memberships",
"displayName": "GroupMembership",
"description": ""
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"user": {
"type": "relation",
"relation": "manyToOne",
"target": "plugin::users-permissions.user",
"inversedBy": "group_memberships"
},
"group": {
"type": "relation",
"relation": "manyToOne",
"target": "api::group.group",
"inversedBy": "memberships"
},
"role": {
"type": "enumeration",
"enum": [
"member",
"admin",
"owner"
]
}
}
}

View File

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

View File

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

View File

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

View File

@@ -4,40 +4,73 @@
"info": {
"singularName": "group",
"pluralName": "groups",
"displayName": "Group"
"displayName": "Group",
"description": ""
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"title": {
"name": {
"type": "string"
},
"description": {
"type": "text"
},
"banner": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images",
"files",
"videos",
"audios"
],
"type": "media",
"multiple": false
]
},
"users": {
"type": "relation",
"relation": "manyToMany",
"target": "plugin::users-permissions.user",
"inversedBy": "groups"
"tags": {
"displayName": "tags",
"type": "component",
"repeatable": true,
"component": "social.tags"
},
"owner": {
"category": {
"type": "enumeration",
"enum": [
"mixedChoir",
"womensChoir",
"mensChoir",
"childrensChoir"
]
},
"access": {
"type": "enumeration",
"enum": [
"private",
"public",
"closed"
]
},
"memberships": {
"type": "relation",
"relation": "manyToOne",
"target": "plugin::users-permissions.user",
"inversedBy": "groups"
"relation": "oneToMany",
"target": "api::group-membership.group-membership",
"mappedBy": "group"
},
"lastActivity": {
"type": "datetime"
},
"activityType": {
"type": "string"
},
"state": {
"type": "enumeration",
"enum": [
"open",
"recruiting",
"audition"
]
}
}
}

View File

@@ -1,7 +1,84 @@
/**
* group controller
*/
// /src/api/group/controllers/group.ts
import { factories } from "@strapi/strapi";
import { factories } from '@strapi/strapi'
export default factories.createCoreController(
"api::group.group",
({ strapi }) => ({
async create(ctx) {
// 1) Prépare le payload + upload banner
const body = ctx.request.body as any;
const data =
typeof body?.data === "string"
? JSON.parse(body.data)
: body?.data || {};
export default factories.createCoreController('api::group.group');
const bannerInput = (ctx.request.files as any)?.banner;
if (bannerInput) {
const file = Array.isArray(bannerInput) ? bannerInput[0] : bannerInput;
const uploaded = await strapi
.plugin("upload")
.service("upload")
.upload({
data: {
fileInfo: {
alternativeText: data?.name || "banner",
caption: "banner",
name: file.originalFilename || "banner",
},
},
files: file,
});
if (uploaded?.[0]?.id) {
data.banner = uploaded[0].id;
}
}
// 2) Crée le group via core controller
ctx.request.body = { data };
const response = await super.create(ctx); // { data: { id, attributes }, meta: {} }
// 3) Tente de créer le GroupMembership (owner) pour le user courant
const membershipMeta: {
success: boolean;
membershipId?: number;
error?: string;
} = { success: false };
try {
const userId = ctx.state.user?.id;
const groupId = response?.data?.id;
if (!userId) {
membershipMeta.error = "Unauthenticated user";
} else if (!groupId) {
membershipMeta.error = "Missing group id";
} else {
const membership = await strapi.entityService.create(
"api::group-membership.group-membership",
{
data: {
user: userId,
group: groupId,
role: "owner",
},
}
);
membershipMeta.success = true;
membershipMeta.membershipId = membership?.id as number;
}
} catch (err: any) {
strapi.log.error(
"[group.create] Erreur création GroupMembership:",
err
);
membershipMeta.error = err?.message || "Unknown error";
}
// 4) Ajoute le résultat du membership dans la meta sans casser la création du group
response.meta = { ...(response.meta || {}), membership: membershipMeta };
return response;
},
})
);

View File

@@ -0,0 +1,45 @@
{
"kind": "collectionType",
"collectionName": "invites",
"info": {
"singularName": "invite",
"pluralName": "invites",
"displayName": "Invite",
"description": ""
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"invitedUser": {
"type": "relation",
"relation": "oneToOne",
"target": "plugin::users-permissions.user"
},
"name": {
"type": "string"
},
"description": {
"type": "text"
},
"invitedBy": {
"type": "relation",
"relation": "oneToOne",
"target": "plugin::users-permissions.user"
},
"type": {
"type": "enumeration",
"enum": [
"group",
"choral"
]
},
"members": {
"type": "integer"
},
"invitedDate": {
"type": "datetime"
}
}
}

View File

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

View File

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

View File

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

View File

@@ -52,6 +52,18 @@
},
"shares": {
"type": "integer"
},
"title": {
"type": "string"
},
"url": {
"type": "string"
},
"imageUrl": {
"type": "string"
},
"source": {
"type": "string"
}
}
}

View File

@@ -2,6 +2,97 @@
* post controller
*/
import { factories } from '@strapi/strapi'
import { factories } from "@strapi/strapi";
export default factories.createCoreController('api::post.post');
export default factories.createCoreController(
"api::post.post",
({ strapi }) => ({
async create(ctx) {
const data = JSON.parse(ctx.request.body.data);
if (ctx.request.files.media) {
const files = Array.isArray(ctx.request.files.media)
? ctx.request.files.media[0]
: ctx.request.files.media;
const extension = files.originalFilename.match(/\.[0-9a-z]+$/i);
const payload = {
fileInfo: {
caption: "undefined",
alternativeText: data.name || "",
name: `${data.name}_media${extension}`,
},
};
const asset = await strapi.services["plugin::upload.upload"].upload({
data: payload,
files,
});
data.media = asset[0].id;
}
ctx.request.body = { data };
const result = await super.create(ctx);
return result;
},
async savePost(ctx) {
console.log("🎯 savePost called with params:", ctx.params);
console.log("🎯 Request method:", ctx.request.method);
console.log("🎯 Request URL:", ctx.request.url);
const user = ctx.state.user;
if (!user) {
console.log("❌ User not authenticated");
return ctx.unauthorized("You must be logged in to save posts");
}
console.log("✅ User authenticated:", user.id);
const postId = parseInt(ctx.params.id);
if (isNaN(postId)) {
return ctx.badRequest("Invalid post ID");
}
// Verify the post exists
const post = await strapi.entityService.findOne("api::post.post", postId);
if (!post) {
return ctx.notFound("Post not found");
}
// Get current user with saved_posts
const currentUser: any = await strapi.entityService.findOne(
"plugin::users-permissions.user",
user.id,
{ populate: ["saved_posts"] }
);
// Initialize saved_posts array if it doesn't exist
const currentSavedPosts = currentUser.saved_posts || [];
// Check if post is already saved to avoid duplicates
if (currentSavedPosts.some((savedPost: any) => savedPost.id === postId)) {
console.log("⚠️ Post already saved");
return ctx.badRequest("Post already saved");
}
// Add the post to saved_posts
const updatedSavedPosts = [...currentSavedPosts, postId];
console.log("🔄 Updating user with new saved posts:", updatedSavedPosts);
// Update the user
const updatedUser = await strapi.entityService.update(
"plugin::users-permissions.user",
user.id,
{
data: {
saved_posts: updatedSavedPosts as any,
},
}
);
return ctx.send({
message: "Post saved successfully",
savedPostsCount: updatedSavedPosts.length,
});
},
})
);

View File

@@ -0,0 +1,13 @@
/**
* Custom post routes
*/
export default {
routes: [
{
method: "POST",
path: "/posts/:id/save",
handler: "post.savePost",
},
],
};

View File

@@ -2,6 +2,6 @@
* post router
*/
import { factories } from '@strapi/strapi';
import { factories } from "@strapi/strapi";
export default factories.createCoreRouter('api::post.post');
export default factories.createCoreRouter("api::post.post");

View File

@@ -0,0 +1,13 @@
{
"collectionName": "components_social_tags",
"info": {
"displayName": "tags",
"icon": "emotionUnhappy"
},
"options": {},
"attributes": {
"tag": {
"type": "string"
}
}
}

View File

@@ -188,16 +188,21 @@
"target": "api::post.post",
"mappedBy": "author"
},
"groups": {
"type": "relation",
"relation": "oneToMany",
"target": "api::group.group",
"mappedBy": "owner"
},
"friends": {
"type": "relation",
"relation": "oneToMany",
"target": "plugin::users-permissions.user"
},
"group_memberships": {
"type": "relation",
"relation": "oneToMany",
"target": "api::group-membership.group-membership",
"mappedBy": "user"
},
"saved_posts": {
"type": "relation",
"relation": "oneToMany",
"target": "api::post.post"
}
}
}

View File

@@ -19,6 +19,17 @@ export interface AddressFullAddress extends Struct.ComponentSchema {
};
}
export interface SocialTags extends Struct.ComponentSchema {
collectionName: 'components_social_tags';
info: {
displayName: 'tags';
icon: 'emotionUnhappy';
};
attributes: {
tag: Schema.Attribute.String;
};
}
export interface UserPermissions extends Struct.ComponentSchema {
collectionName: 'components_user_permissions';
info: {
@@ -39,6 +50,7 @@ declare module '@strapi/strapi' {
export module Public {
export interface ComponentSchemas {
'address.full-address': AddressFullAddress;
'social.tags': SocialTags;
'user.permissions': UserPermissions;
}
}

View File

@@ -915,9 +915,45 @@ export interface ApiEventEvent extends Struct.CollectionTypeSchema {
};
}
export interface ApiGroupMembershipGroupMembership
extends Struct.CollectionTypeSchema {
collectionName: 'group_memberships';
info: {
description: '';
displayName: 'GroupMembership';
pluralName: 'group-memberships';
singularName: 'group-membership';
};
options: {
draftAndPublish: false;
};
attributes: {
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
group: Schema.Attribute.Relation<'manyToOne', 'api::group.group'>;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<
'oneToMany',
'api::group-membership.group-membership'
> &
Schema.Attribute.Private;
publishedAt: Schema.Attribute.DateTime;
role: Schema.Attribute.Enumeration<['member', 'admin', 'owner']>;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
user: Schema.Attribute.Relation<
'manyToOne',
'plugin::users-permissions.user'
>;
};
}
export interface ApiGroupGroup extends Struct.CollectionTypeSchema {
collectionName: 'groups';
info: {
description: '';
displayName: 'Group';
pluralName: 'groups';
singularName: 'group';
@@ -926,27 +962,72 @@ export interface ApiGroupGroup extends Struct.CollectionTypeSchema {
draftAndPublish: false;
};
attributes: {
access: Schema.Attribute.Enumeration<['private', 'public', 'closed']>;
activityType: Schema.Attribute.String;
banner: Schema.Attribute.Media<'images' | 'files' | 'videos' | 'audios'>;
category: Schema.Attribute.Enumeration<
['mixedChoir', 'womensChoir', 'mensChoir', 'childrensChoir']
>;
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
description: Schema.Attribute.Text;
lastActivity: Schema.Attribute.DateTime;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<'oneToMany', 'api::group.group'> &
Schema.Attribute.Private;
owner: Schema.Attribute.Relation<
'manyToOne',
'plugin::users-permissions.user'
memberships: Schema.Attribute.Relation<
'oneToMany',
'api::group-membership.group-membership'
>;
name: Schema.Attribute.String;
publishedAt: Schema.Attribute.DateTime;
title: Schema.Attribute.String;
state: Schema.Attribute.Enumeration<['open', 'recruiting', 'audition']>;
tags: Schema.Attribute.Component<'social.tags', true>;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
users: Schema.Attribute.Relation<
'manyToMany',
};
}
export interface ApiInviteInvite extends Struct.CollectionTypeSchema {
collectionName: 'invites';
info: {
description: '';
displayName: 'Invite';
pluralName: 'invites';
singularName: 'invite';
};
options: {
draftAndPublish: false;
};
attributes: {
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
description: Schema.Attribute.Text;
invitedBy: Schema.Attribute.Relation<
'oneToOne',
'plugin::users-permissions.user'
>;
invitedDate: Schema.Attribute.DateTime;
invitedUser: Schema.Attribute.Relation<
'oneToOne',
'plugin::users-permissions.user'
>;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<
'oneToMany',
'api::invite.invite'
> &
Schema.Attribute.Private;
members: Schema.Attribute.Integer;
name: Schema.Attribute.String;
publishedAt: Schema.Attribute.DateTime;
type: Schema.Attribute.Enumeration<['group', 'choral']>;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
};
}
@@ -1041,6 +1122,7 @@ export interface ApiPostPost extends Struct.CollectionTypeSchema {
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
imageUrl: Schema.Attribute.String;
likes: Schema.Attribute.Relation<
'oneToMany',
'plugin::users-permissions.user'
@@ -1054,9 +1136,12 @@ export interface ApiPostPost extends Struct.CollectionTypeSchema {
>;
publishedAt: Schema.Attribute.DateTime;
shares: Schema.Attribute.Integer;
source: Schema.Attribute.String;
title: Schema.Attribute.String;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
url: Schema.Attribute.String;
};
}
@@ -1591,7 +1676,10 @@ export interface PluginUsersPermissionsUser
'plugin::users-permissions.user'
>;
gender: Schema.Attribute.Enumeration<['men', 'women', 'none']>;
groups: Schema.Attribute.Relation<'oneToMany', 'api::group.group'>;
group_memberships: Schema.Attribute.Relation<
'oneToMany',
'api::group-membership.group-membership'
>;
job: Schema.Attribute.Enumeration<
['choir_director', 'choir_addict', 'choir_master', 'choir_singer', 'none']
>;
@@ -1619,6 +1707,7 @@ export interface PluginUsersPermissionsUser
'manyToOne',
'plugin::users-permissions.role'
>;
saved_posts: Schema.Attribute.Relation<'oneToMany', 'api::post.post'>;
surname: Schema.Attribute.String;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
@@ -1659,7 +1748,9 @@ declare module '@strapi/strapi' {
'api::direct-message.direct-message': ApiDirectMessageDirectMessage;
'api::event-other.event-other': ApiEventOtherEventOther;
'api::event.event': ApiEventEvent;
'api::group-membership.group-membership': ApiGroupMembershipGroupMembership;
'api::group.group': ApiGroupGroup;
'api::invite.invite': ApiInviteInvite;
'api::message.message': ApiMessageMessage;
'api::permissions-template.permissions-template': ApiPermissionsTemplatePermissionsTemplate;
'api::post.post': ApiPostPost;