From a263672e04c136fb2cfbd686adb2dd3f6052f23d Mon Sep 17 00:00:00 2001 From: dokicaaa Date: Sat, 9 May 2026 00:58:28 +0200 Subject: [PATCH 1/3] Automated events creation for submited event request --- .../content-types/event-request/schema.json | 10 ++++ .../controllers/event-request.ts | 52 ++++++++++++++++++- cms/src/api/event-request/controllers/form.ts | 18 +++++++ .../api/event-request/routes/event-request.ts | 16 ++++-- cms/src/index.ts | 50 ++++++++++++++++++ 5 files changed, 142 insertions(+), 4 deletions(-) diff --git a/cms/src/api/event-request/content-types/event-request/schema.json b/cms/src/api/event-request/content-types/event-request/schema.json index ce40fa7..2ed9e75 100644 --- a/cms/src/api/event-request/content-types/event-request/schema.json +++ b/cms/src/api/event-request/content-types/event-request/schema.json @@ -56,6 +56,16 @@ }, "physicalPresence": { "type": "boolean" + }, + "status": { + "type": "enumeration", + "enum": ["pending", "approved"], + "default": "pending" + }, + "event": { + "type": "relation", + "relation": "oneToOne", + "target": "api::event.event" } } } diff --git a/cms/src/api/event-request/controllers/event-request.ts b/cms/src/api/event-request/controllers/event-request.ts index 1dd3f47..f30461f 100644 --- a/cms/src/api/event-request/controllers/event-request.ts +++ b/cms/src/api/event-request/controllers/event-request.ts @@ -4,4 +4,54 @@ import { factories } from '@strapi/strapi' -export default factories.createCoreController('api::event-request.event-request'); +export default factories.createCoreController('api::event-request.event-request', ({ strapi }) => ({ + async approve(ctx) { + const { id } = ctx.params; + + const request: any = await strapi.documents('api::event-request.event-request').findOne({ + documentId: id, + populate: ['event' as any], + }); + + if (!request) { + return ctx.notFound('Event request not found'); + } + + if (!request.event) { + return ctx.badRequest('No linked draft event found'); + } + + if (request.status === 'approved') { + return ctx.badRequest('Event request already approved'); + } + + try { + await strapi.documents('api::event.event').publish({ + documentId: request.event.documentId, + }); + + await strapi.documents('api::event-request.event-request').update({ + documentId: id, + data: { status: 'approved' } as any, + }); + + await strapi.plugins['email'].services.email.send({ + to: request.initiatorEmail, + from: 'hello@42.mk', + replyTo: 'hello@42.mk', + subject: 'Your event has been approved! - 42.mk', + html: ` +

Great news! Your event request "${request.eventName}" has been approved and is now published.

+

You can view it on our platform.

+

Best regards,
42.mk Team

+ `, + }); + + ctx.body = { message: 'Event request approved' }; + } catch (error) { + strapi.log.error('Error approving event request:', error); + ctx.status = 500; + ctx.body = { error: { message: 'Failed to approve event request' } }; + } + }, +})); diff --git a/cms/src/api/event-request/controllers/form.ts b/cms/src/api/event-request/controllers/form.ts index 37d43e1..2588f24 100644 --- a/cms/src/api/event-request/controllers/form.ts +++ b/cms/src/api/event-request/controllers/form.ts @@ -52,6 +52,24 @@ export default { data: eventRequest, }); + const startDateTime = `${eventRequest.eventDate}T${eventRequest.eventStart}`; + + const draftEvent = await strapi.documents("api::event.event").create({ + data: { + title: eventRequest.eventName, + description: `${eventRequest.eventPurpose}\n\n${eventRequest.eventTheme}`, + start: startDateTime, + summary: eventRequest.eventAgenda, + tags: eventRequest.eventTheme ? [{ tagName: eventRequest.eventTheme }] : [], + }, + status: "draft", + }); + + await strapi.documents("api::event-request.event-request").update({ + documentId: res.documentId, + data: { event: draftEvent.documentId } as any, + }); + const requestCopy = `

Organizing entity: ${eventRequest.organizingEntity}

Initiator name: ${eventRequest.initiatorName}

Initiator email: ${eventRequest.initiatorEmail}

diff --git a/cms/src/api/event-request/routes/event-request.ts b/cms/src/api/event-request/routes/event-request.ts index 4582ebe..46da162 100644 --- a/cms/src/api/event-request/routes/event-request.ts +++ b/cms/src/api/event-request/routes/event-request.ts @@ -2,6 +2,16 @@ * event-request router */ -import { factories } from '@strapi/strapi'; - -export default factories.createCoreRouter('api::event-request.event-request'); +export default { + routes: [ + { + method: 'POST', + path: '/event-requests/:id/approve', + handler: 'event-request.approve', + config: { + policies: [], + middlewares: [], + }, + }, + ], +}; diff --git a/cms/src/index.ts b/cms/src/index.ts index 23b030e..5bef8d8 100644 --- a/cms/src/index.ts +++ b/cms/src/index.ts @@ -5,6 +5,55 @@ import { eventNotificationMiddleware } from './middlewares/event-notifications'; const FIREBASE_FCM_CREDENTIALS_PATH = './base42-mobile-app-fcm-firebase-adminsdk.json'; +const eventRequestApprovalMiddleware = () => { + return async (context, next) => { + if (context.uid !== 'api::event.event') { + return await next(); + } + + const result = await next(); + + if (context.action === 'publish' && result?.documentId) { + try { + const request = await strapi.db.query('api::event-request.event-request').findOne({ + where: { + event: { documentId: result.documentId }, + }, + }); + + if (request && request.status !== 'approved') { + await strapi.documents('api::event-request.event-request').update({ + documentId: request.documentId, + data: { status: 'approved' } as any, + }); + + const reqDetails = await strapi.db.query('api::event-request.event-request').findOne({ + where: { documentId: request.documentId }, + }); + + if (reqDetails?.initiatorEmail) { + await strapi.plugins['email'].services.email.send({ + to: reqDetails.initiatorEmail, + from: 'hello@42.mk', + replyTo: 'hello@42.mk', + subject: 'Your event has been approved! - 42.mk', + html: ` +

Great news! Your event request "${reqDetails.eventName}" has been approved and is now published.

+

You can view it on our platform.

+

Best regards,
42.mk Team

+ `, + }); + } + } + } catch (error) { + strapi.log.error('[Event Request Approval] Error syncing status after publish:', error); + } + } + + return result; + }; +}; + export default { /** * An asynchronous register function that runs before @@ -42,6 +91,7 @@ export default { }); strapi.documents.use(eventNotificationMiddleware()); + strapi.documents.use(eventRequestApprovalMiddleware()); }, /** From 8fcbb3548417bdcbbb5e3304cafc0b7f52c77671 Mon Sep 17 00:00:00 2001 From: dokicaaa Date: Tue, 30 Jun 2026 15:15:48 +0200 Subject: [PATCH 2/3] Added Membership Support --- .../content-types/membership/schema.json | 42 +++++++ .../api/membership/controllers/membership.ts | 22 ++++ cms/src/api/membership/routes/membership.ts | 12 ++ cms/src/api/membership/services/membership.ts | 3 + .../users-permissions/controllers/user.ts | 44 ++++++++ .../users-permissions/routes/custom-routes.ts | 8 ++ .../users-permissions/strapi-server.ts | 20 ++++ cms/types/generated/contentTypes.d.ts | 106 ++++++++++++++++-- 8 files changed, 249 insertions(+), 8 deletions(-) create mode 100644 cms/src/api/membership/content-types/membership/schema.json create mode 100644 cms/src/api/membership/controllers/membership.ts create mode 100644 cms/src/api/membership/routes/membership.ts create mode 100644 cms/src/api/membership/services/membership.ts diff --git a/cms/src/api/membership/content-types/membership/schema.json b/cms/src/api/membership/content-types/membership/schema.json new file mode 100644 index 0000000..ed73f0a --- /dev/null +++ b/cms/src/api/membership/content-types/membership/schema.json @@ -0,0 +1,42 @@ +{ + "kind": "collectionType", + "collectionName": "memberships", + "info": { + "singularName": "membership", + "pluralName": "memberships", + "displayName": "Membership", + "description": "Tracks user membership subscriptions" + }, + "options": { + "draftAndPublish": false + }, + "pluginOptions": {}, + "attributes": { + "user": { + "type": "relation", + "relation": "manyToOne", + "target": "plugin::users-permissions.user", + "inversedBy": "memberships" + }, + "tier": { + "type": "enumeration", + "enum": ["monthly", "yearly"], + "required": true + }, + "status": { + "type": "enumeration", + "enum": ["active", "inactive", "cancelled", "pending"], + "default": "pending" + }, + "startDate": { + "type": "datetime" + }, + "endDate": { + "type": "datetime" + }, + "stripeSubscriptionId": { + "type": "string", + "private": true + } + } +} diff --git a/cms/src/api/membership/controllers/membership.ts b/cms/src/api/membership/controllers/membership.ts new file mode 100644 index 0000000..b8fbd5f --- /dev/null +++ b/cms/src/api/membership/controllers/membership.ts @@ -0,0 +1,22 @@ +import { factories } from '@strapi/strapi'; + +const UID = 'api::membership.membership'; + +export default factories.createCoreController(UID, ({ strapi }) => ({ + async me(ctx) { + if (!ctx.state.user) return ctx.unauthorized(); + + const memberships = await strapi.documents(UID).findMany({ + filters: { user: { documentId: ctx.state.user.documentId } }, + sort: { startDate: 'desc' }, + populate: [], + }); + + const active = memberships.find((m: any) => m.status === 'active'); + + ctx.body = { + memberships, + active: active || null, + }; + }, +})); diff --git a/cms/src/api/membership/routes/membership.ts b/cms/src/api/membership/routes/membership.ts new file mode 100644 index 0000000..19e1d99 --- /dev/null +++ b/cms/src/api/membership/routes/membership.ts @@ -0,0 +1,12 @@ +export default { + routes: [ + { + method: 'GET', + path: '/memberships/me', + handler: 'membership.me', + config: { + prefix: '', + }, + }, + ], +}; diff --git a/cms/src/api/membership/services/membership.ts b/cms/src/api/membership/services/membership.ts new file mode 100644 index 0000000..96b82e6 --- /dev/null +++ b/cms/src/api/membership/services/membership.ts @@ -0,0 +1,3 @@ +import { factories } from '@strapi/strapi'; + +export default factories.createCoreService('api::membership.membership'); diff --git a/cms/src/extensions/users-permissions/controllers/user.ts b/cms/src/extensions/users-permissions/controllers/user.ts index dcb771c..580d305 100644 --- a/cms/src/extensions/users-permissions/controllers/user.ts +++ b/cms/src/extensions/users-permissions/controllers/user.ts @@ -40,6 +40,50 @@ function validateProfilePictureId(id: any, ctx: any) { } export default { + async me(ctx) { + if (!ctx.state.user) return ctx.unauthorized(); + + const user = await strapi.documents('plugin::users-permissions.user').findOne({ + documentId: ctx.state.user.documentId, + populate: ['role', 'profilePicture', 'memberships'], + }); + + if (!user) return ctx.notFound(); + + const { password, resetPasswordToken, confirmationToken, ...safeUser } = user; + ctx.body = safeUser; + }, + + async volunteerApply(ctx) { + if (!ctx.state.user) return ctx.unauthorized(); + + const user = ctx.state.user; + strapi.log.info(`Volunteer application received from user: ${user.id} (${user.username}, ${user.email})`); + + try { + await strapi.plugins['email'].services.email.send({ + to: 'hello@42.mk', + from: 'hello@42.mk', + replyTo: user.email, + subject: `New volunteer application from ${user.username}`, + html: ` +

A new volunteer application has been submitted.

+

Username: ${user.username}

+

Email: ${user.email}

+

First name: ${user.firstName || 'N/A'}

+

Last name: ${user.lastName || 'N/A'}

+
+

You can review and update this user's type in the Strapi admin dashboard.

+ `, + }); + strapi.log.info(`Volunteer application email sent successfully for user: ${user.id}`); + } catch (emailError) { + strapi.log.error(`Failed to send volunteer application email for user ${user.id}: ${emailError}`); + } + + ctx.body = { ok: true, message: 'Volunteer application submitted successfully' }; + }, + async updateProfile(ctx) { if (!ctx.state.user) return ctx.unauthorized(); diff --git a/cms/src/extensions/users-permissions/routes/custom-routes.ts b/cms/src/extensions/users-permissions/routes/custom-routes.ts index 0e99165..53bd058 100644 --- a/cms/src/extensions/users-permissions/routes/custom-routes.ts +++ b/cms/src/extensions/users-permissions/routes/custom-routes.ts @@ -17,5 +17,13 @@ export default { prefix: '', }, }, + { + method: 'POST', + path: '/volunteer/apply', + handler: 'user.volunteerApply', + config: { + prefix: '', + }, + }, ], } diff --git a/cms/src/extensions/users-permissions/strapi-server.ts b/cms/src/extensions/users-permissions/strapi-server.ts index a6aa100..8f439db 100644 --- a/cms/src/extensions/users-permissions/strapi-server.ts +++ b/cms/src/extensions/users-permissions/strapi-server.ts @@ -30,8 +30,28 @@ module.exports = (plugin) => { mappedBy: 'user', }; + userAttributes.stripeCustomerId = { + type: 'string', + private: true, + }; + + userAttributes.userType = { + type: 'enumeration', + enum: ['user', 'volunteer', 'member'], + default: 'user', + }; + + userAttributes.memberships = { + type: 'relation', + relation: 'oneToMany', + target: 'api::membership.membership', + mappedBy: 'user', + }; + plugin.controllers.user.updateProfile = userController.updateProfile; plugin.controllers.user.saveFcmToken = userController.saveFcmToken; + plugin.controllers.user.me = userController.me; + plugin.controllers.user.volunteerApply = userController.volunteerApply; plugin.routes['content-api'].routes.push(...customRoutes.routes); diff --git a/cms/types/generated/contentTypes.d.ts b/cms/types/generated/contentTypes.d.ts index 718bee2..db6dc65 100644 --- a/cms/types/generated/contentTypes.d.ts +++ b/cms/types/generated/contentTypes.d.ts @@ -446,28 +446,26 @@ export interface ApiEventRequestEventRequest createdAt: Schema.Attribute.DateTime; createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & Schema.Attribute.Private; + event: Schema.Attribute.Relation<'oneToOne', 'api::event.event'>; eventAgenda: Schema.Attribute.RichText; eventDate: Schema.Attribute.Date; eventEnd: Schema.Attribute.Time; - eventName: Schema.Attribute.String; - eventPurpose: Schema.Attribute.String; eventStart: Schema.Attribute.Time; - eventTheme: Schema.Attribute.String; - eventType: Schema.Attribute.String; expectedGuests: Schema.Attribute.Integer; initiatorEmail: Schema.Attribute.String; - initiatorName: Schema.Attribute.String; - initiatorPhoneNumber: Schema.Attribute.String; locale: Schema.Attribute.String & Schema.Attribute.Private; localizations: Schema.Attribute.Relation< 'oneToMany', 'api::event-request.event-request' > & Schema.Attribute.Private; - organization: Schema.Attribute.String; organizingEntity: Schema.Attribute.String; - physicalPresence: Schema.Attribute.Boolean; publishedAt: Schema.Attribute.DateTime; + space: Schema.Attribute.Enumeration< + ['events-hall', 'workshop-area', 'electronics-area', 'full-space'] + >; + status: Schema.Attribute.Enumeration<['pending', 'approved']> & + Schema.Attribute.DefaultTo<'pending'>; updatedAt: Schema.Attribute.DateTime; updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & Schema.Attribute.Private; @@ -660,6 +658,47 @@ export interface ApiHomeHome extends Struct.SingleTypeSchema { }; } +export interface ApiMembershipMembership extends Struct.CollectionTypeSchema { + collectionName: 'memberships'; + info: { + description: 'Tracks user membership subscriptions'; + displayName: 'Membership'; + pluralName: 'memberships'; + singularName: 'membership'; + }; + options: { + draftAndPublish: false; + }; + attributes: { + createdAt: Schema.Attribute.DateTime; + createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & + Schema.Attribute.Private; + endDate: Schema.Attribute.DateTime; + locale: Schema.Attribute.String & Schema.Attribute.Private; + localizations: Schema.Attribute.Relation< + 'oneToMany', + 'api::membership.membership' + > & + Schema.Attribute.Private; + publishedAt: Schema.Attribute.DateTime; + startDate: Schema.Attribute.DateTime; + status: Schema.Attribute.Enumeration< + ['active', 'inactive', 'cancelled', 'pending'] + > & + Schema.Attribute.DefaultTo<'pending'>; + stripeSubscriptionId: Schema.Attribute.String & Schema.Attribute.Private; + tier: Schema.Attribute.Enumeration<['monthly', 'yearly']> & + Schema.Attribute.Required; + 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 ApiPartnerPartner extends Struct.CollectionTypeSchema { collectionName: 'partners'; info: { @@ -723,6 +762,48 @@ export interface ApiPartnerPartner extends Struct.CollectionTypeSchema { }; } +export interface ApiProjectProject extends Struct.CollectionTypeSchema { + collectionName: 'projects'; + info: { + description: 'Cached GitHub repositories for mobile app consumption'; + displayName: 'Project'; + pluralName: 'projects'; + singularName: 'project'; + }; + options: { + draftAndPublish: true; + }; + attributes: { + commit_activity: Schema.Attribute.JSON; + contributors: Schema.Attribute.JSON; + createdAt: Schema.Attribute.DateTime; + createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & + Schema.Attribute.Private; + description: Schema.Attribute.Text; + github_repo_id: Schema.Attribute.String & + Schema.Attribute.Required & + Schema.Attribute.Unique; + help_wanted_count: Schema.Attribute.Integer & Schema.Attribute.DefaultTo<0>; + language: Schema.Attribute.String; + last_synced_at: Schema.Attribute.DateTime; + locale: Schema.Attribute.String & Schema.Attribute.Private; + localizations: Schema.Attribute.Relation< + 'oneToMany', + 'api::project.project' + > & + Schema.Attribute.Private; + name: Schema.Attribute.String & Schema.Attribute.Required; + owner_login: Schema.Attribute.String & Schema.Attribute.Required; + pr_count: Schema.Attribute.Integer & Schema.Attribute.DefaultTo<0>; + publishedAt: Schema.Attribute.DateTime; + pushed_at: Schema.Attribute.DateTime; + stargazers_count: Schema.Attribute.Integer & Schema.Attribute.DefaultTo<0>; + updatedAt: Schema.Attribute.DateTime; + updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & + Schema.Attribute.Private; + }; +} + export interface ApiUserEventUserEvent extends Struct.CollectionTypeSchema { collectionName: 'user_events'; info: { @@ -1238,6 +1319,10 @@ export interface PluginUsersPermissionsUser 'plugin::users-permissions.user' > & Schema.Attribute.Private; + memberships: Schema.Attribute.Relation< + 'oneToMany', + 'api::membership.membership' + >; password: Schema.Attribute.Password & Schema.Attribute.Private & Schema.Attribute.SetMinMaxLength<{ @@ -1251,6 +1336,7 @@ export interface PluginUsersPermissionsUser 'manyToOne', 'plugin::users-permissions.role' >; + stripeCustomerId: Schema.Attribute.String & Schema.Attribute.Private; updatedAt: Schema.Attribute.DateTime; updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & Schema.Attribute.Private; @@ -1264,6 +1350,8 @@ export interface PluginUsersPermissionsUser Schema.Attribute.SetMinMaxLength<{ minLength: 3; }>; + userType: Schema.Attribute.Enumeration<['user', 'volunteer', 'member']> & + Schema.Attribute.DefaultTo<'user'>; }; } @@ -1282,7 +1370,9 @@ declare module '@strapi/strapi' { 'api::event.event': ApiEventEvent; 'api::gallery.gallery': ApiGalleryGallery; 'api::home.home': ApiHomeHome; + 'api::membership.membership': ApiMembershipMembership; 'api::partner.partner': ApiPartnerPartner; + 'api::project.project': ApiProjectProject; 'api::user-event.user-event': ApiUserEventUserEvent; 'plugin::content-releases.release': PluginContentReleasesRelease; 'plugin::content-releases.release-action': PluginContentReleasesReleaseAction; From 252f867fce1bf33a2bf3f9810c8bfdbbc4c636b1 Mon Sep 17 00:00:00 2001 From: dokicaaa Date: Tue, 30 Jun 2026 15:23:42 +0200 Subject: [PATCH 3/3] Removed dev logs --- cms/src/extensions/users-permissions/controllers/user.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cms/src/extensions/users-permissions/controllers/user.ts b/cms/src/extensions/users-permissions/controllers/user.ts index 580d305..e02ad60 100644 --- a/cms/src/extensions/users-permissions/controllers/user.ts +++ b/cms/src/extensions/users-permissions/controllers/user.ts @@ -58,7 +58,6 @@ export default { if (!ctx.state.user) return ctx.unauthorized(); const user = ctx.state.user; - strapi.log.info(`Volunteer application received from user: ${user.id} (${user.username}, ${user.email})`); try { await strapi.plugins['email'].services.email.send({ @@ -76,9 +75,8 @@ export default {

You can review and update this user's type in the Strapi admin dashboard.

`, }); - strapi.log.info(`Volunteer application email sent successfully for user: ${user.id}`); } catch (emailError) { - strapi.log.error(`Failed to send volunteer application email for user ${user.id}: ${emailError}`); + strapi.log.error(`Volunteer application email failed: ${emailError}`); } ctx.body = { ok: true, message: 'Volunteer application submitted successfully' };