Skip to content

User Membership Integration + Volunteer#13

Open
dokicaaa wants to merge 7 commits into
mainfrom
feat/user-membership
Open

User Membership Integration + Volunteer#13
dokicaaa wants to merge 7 commits into
mainfrom
feat/user-membership

Conversation

@dokicaaa

@dokicaaa dokicaaa commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

-Changed user model to include Membership
-Volunteer request endpoint for sending email to base42 -> Admin manually sets usertype to volunteer via Strapi Dashboard (Open for automation ideas)
-Endpoints for checking current Membership type for each user via JWT

⚠️ NEEDS INTEGRATION WITH STRAPI.

UPDATE:

✅ STRIPE INTEGRATED WITH TEST API KEY AND TESTED

  • Fix: Yearly tier defaulting to monthly — added session-level metadata.tier
    to checkout creation so webhook can reliably read the tier. Added fallback
    to retrieve subscription from Stripe if metadata is missing.
  • Fix: invoice.payment_succeeded crash — subscription ID was undefined,
    causing 500. Added null check with fallback to invoice.lines.data[0].subscription
    and skip if still missing.
  • Fix: getTierForPriceId return type narrowed to 'monthly' | 'yearly'
    (was string, broke TS build).
  • New: customer.subscription.created handler — creates membership from
    subscription object as safety net if checkout.session.completed is missed.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Stripe-backed memberships and volunteer application support to the Strapi CMS, plus an event-request approval flow that creates/publishes linked events and tracks approval status.

Changes:

  • Introduces a new membership content-type with Stripe checkout, portal, and webhook handling.
  • Extends the users-permissions user model with userType, stripeCustomerId, and memberships, plus new /volunteer/apply and /me endpoints.
  • Updates event-request handling to create a draft event on submission and support approval/publish workflows with status tracking.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
cms/types/generated/contentTypes.d.ts Updates generated Strapi type declarations for new/changed content-types and user fields.
cms/src/middlewares/stripe-webhook.ts Adds a middleware to preserve Stripe webhook raw body for signature verification.
cms/src/index.ts Adds a documents middleware to sync event-request status (and notify) on event publish.
cms/src/extensions/users-permissions/strapi-server.ts Extends user schema (stripe/customer + memberships + userType) and registers new controllers.
cms/src/extensions/users-permissions/routes/custom-routes.ts Adds a custom users-permissions route for volunteer applications.
cms/src/extensions/users-permissions/controllers/user.ts Implements me and volunteerApply endpoints and related validation/email logic.
cms/src/api/membership/services/membership.ts Adds the core service for the new membership content-type.
cms/src/api/membership/routes/membership.ts Adds membership routes for “me”, checkout, portal, and Stripe webhook.
cms/src/api/membership/controllers/membership.ts Implements Stripe checkout/portal creation and webhook handlers to create/update memberships.
cms/src/api/membership/content-types/membership/schema.json Defines the new Membership collection type schema.
cms/src/api/event-request/routes/event-request.ts Adds an approve route for event-requests.
cms/src/api/event-request/controllers/form.ts Creates a draft Event on event-request submission and links it back to the request.
cms/src/api/event-request/controllers/event-request.ts Implements event-request approval (publishing the linked event + updating request status).
cms/src/api/event-request/content-types/event-request/schema.json Adds status and a one-to-one event relation to the event-request schema.
cms/config/middlewares.ts Registers the global Stripe webhook raw-body middleware.
cms/.env.example Documents required Stripe environment variables.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cms/src/api/membership/content-types/membership/schema.json
Comment thread cms/src/api/membership/controllers/membership.ts
Comment thread cms/src/api/membership/controllers/membership.ts
Comment thread cms/src/api/membership/controllers/membership.ts
Comment thread cms/src/middlewares/stripe-webhook.ts
Comment thread cms/src/extensions/users-permissions/controllers/user.ts Outdated
Comment thread cms/src/extensions/users-permissions/controllers/user.ts
Comment thread cms/src/api/event-request/controllers/event-request.ts Outdated
Comment thread cms/src/api/event-request/controllers/event-request.ts Outdated

@iboshkov iboshkov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

Inline comments below cover the findings on lines this PR touches. Two more are in updateProfile in cms/src/extensions/users-permissions/controllers/user.ts, on lines outside this PR's diff, so they can't be anchored inline:

  • profilePicture validation is bypassed when the ID is sent as a number (user.ts:209): the existence/mime/size checks only run if (typeof safeData.profilePicture === 'string'), yet the declared body type is profilePicture?: number. A numeric ID (what a normal JSON client sends) skips validation entirely. There is also no ownership check, so any authenticated user can attach any media file ID in the upload library.
  • Empty strings bypass validation but still get written (user.ts:185-224): guards like if (safeData.username && ...) skip validation for "", but the value stays in safeData and is passed to the update, so {"username": ""} blanks out a username. A non-string value (array/object) throws on .trim() and surfaces as a generic 500.

Positives: webhook signature verification against the raw body is done correctly, stripeSubscriptionId is private: true so it never leaks through the API, and the tier normalization fallbacks are sensible.

function validateNameFields(fieldName: string, value: string) {
function validateNameFields(fieldName: string, value: string, ctx: any) {
if (typeof value !== 'string' || value.length === 0) {
throw new ValidationError(`${fieldName} cannot be empty`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing import: ValidationError is used but never defined in this file. It is thrown 10 times (here through line 220) but there is no import. TypeScript compilation fails with Cannot find name 'ValidationError', and if the file is ever transpiled without a type check, every validation failure throws a ReferenceError at runtime; the catch block's instanceof ValidationError then also throws, so clients get a 500 instead of a 400 for any invalid input.

import { errors } from '@strapi/utils';
const { ValidationError } = errors;

import { factories } from '@strapi/strapi';

export default factories.createCoreRouter('api::event-request.event-request'); No newline at end of file
export default {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR removes the custom approve controller method, but routes/custom-event-request.ts still registers POST /event-requests/:id/approve with handler event-request.approve. Strapi validates route handlers at startup, so the server throws "Handler not found" and won't boot. That route file needs to be deleted along with the controller method (the publish middleware in src/middlewares/event-request-approval.ts already covers the approval flow).

break;
}

const existing = await strapi.db.query(UID).findMany({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race: duplicate memberships can be created. checkout.session.completed, customer.subscription.created, and the invoice.payment_succeeded fallback all do check-then-create keyed on stripeSubscriptionId, and Stripe delivers these events near-simultaneously with no ordering guarantee. Two handlers can both pass the "already exists" check and each insert a row. Adding "unique": true to stripeSubscriptionId in the membership schema makes the second insert fail safely.

data: { status: 'cancelled', endDate: new Date() },
});

const user = await strapi.db.query(USER_UID).findOne({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cancelled subscriptions never demote the user - this block is dead code. Two issues compound:

  1. The membership was fetched above via strapi.db.query without populate: ['user'], so membership.user is undefined and this lookup finds nothing.
  2. Even if it did, the guard below checks user?.document_id, but the query engine returns the attribute as documentId (camelCase).

Net effect: the membership row is marked cancelled, but the user's userType stays member forever after cancelling.


case 'invoice.payment_failed': {
const failedInvoice = event.data.object;
const failedSubId = failedInvoice.subscription;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invoice.subscription is absent on newer Stripe API versions, so failed payments never flip the membership to pending. The invoice.payment_succeeded handler above already works around exactly this with invoice.lines?.data?.[0]?.subscription - the same fallback is needed here.

export default (config, { strapi }) => {
return async (ctx, next) => {
if (ctx.path === '/api/memberships/webhook' && ctx.method === 'POST') {
const chunks = [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unbounded body buffering on an unauthenticated path. This buffers the entire request body into memory before any signature check or size limit applies (it runs before strapi::body), so anyone can POST arbitrarily large payloads to /api/memberships/webhook. Worth capping the buffered size (Stripe events are tiny; ~1MB is a generous cap) and aborting early when exceeded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants