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
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/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..e02ad60 100644 --- a/cms/src/extensions/users-permissions/controllers/user.ts +++ b/cms/src/extensions/users-permissions/controllers/user.ts @@ -40,6 +40,48 @@ 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; + + 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.
+ `, + }); + } catch (emailError) { + strapi.log.error(`Volunteer application email failed: ${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/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