How to allow content access for only Patrons

System Information
  • Strapi Version: 4.9.0
  • Operating System: Ubuntu 22.04
  • Database: Postgres
  • Node Version: v16.19.0
  • NPM Version: 8.19.3
  • Yarn Version: 1.22.19

I am building a video site where I would like to restrict Mux Assets access to patrons only. Is this doable using Strapi’s existing plugins?

I think RBAC might be the key to achieve this, but the docs are a little light on the details and I think the Registering Conditions section is outdated.

So far I have enabled the Patreon provider, and created a Patron role in users-permissions plugin. I think the next step is verifying that users logging in via the Patreon provider are supporters of my Patreon campaign.

From previous patreon integrations I have done, I know it’s possible to GET request https://www.patreon.com/api/oauth2/v2/identity and get username, e-mail, and membership details all in one request. It doesn’t look like Strapi does that. All Strapi gets is username and e-mail.

To get this membership information, I think I need to create some code which requests the user’s memberships via the Patreon api. I’m not sure where in my strapi repo I should insert this code. I’m also unsure about how to handle making a request to Patreon’s API with the necessary access token in the context of Strapi.

I think the things I need to know how to do are as follows.

  • How do I automatically assign a role to a user who is a member of my Patreon campaign?
  • How do I have Strapi store more details about a user’s Patreon memberships?
  • How do I have Strapi fetch user membership details from Patreon?
  • Is this the Strapi way?
  • Is there a better way?

Any advice is appreciated. I’ll keep updating this thread until I have it figured out.

RBAC is strapi admin only so not for the apis I would contact mux about it under there plugin since this seems to be plugin related since it is some functionality there plugin would have to have to be able to use private video’s If I am not wrong

1 Like

What I’ve come up with is to use the extensions functionality of Strapi to override the behavior of the connect function.

This retrieves the patron’s Patreon profile at time of Strapi user registration, something that happens by default thanks to users-permissions. Normally only the username and e-mail is saved to the Strapi user model, but my patch accesses more of the profile data and assigns a Strapi ‘Patron’ role if their list of benefits contains an ID that I assigned in the Strapi admin UI under a custom patreon-benefit-id single content-type.

Here is my new connect function which lives in src/extensions/users-permissions/server/services/providers.js

  const connect = async (provider, query) => {
    console.log(`provider connect q_q = ${provider}`)

    const accessToken = query.access_token || query.code || query.oauth_token;

    if (!accessToken) {
      throw new Error('No access_token.');
    }

    // Get the profile.
    const profile = await getProfile(provider, query);

    console.log(' >> profile')
    console.log(profile)

    if (!profile) {
      throw new Error('No profile')
    }

    const email = _.toLower(profile.email);

    // We need at least the mail.
    if (!email) {
      throw new Error('Email was not available.');
    }

    const users = await strapi.query('plugin::users-permissions.user').findMany({
      where: { email },
    });

    const advancedSettings = await strapi
      .store({ type: 'plugin', name: 'users-permissions', key: 'advanced' })
      .get();

    const user = _.find(users, { provider });

    if (_.isEmpty(user) && !advancedSettings.allow_register) {
      throw new Error('Register action is actually not available.');
    }


    // Retrieve default role.
    const defaultRole = await strapi
      .query('plugin::users-permissions.role')
      .findOne({ where: { type: advancedSettings.default_role } });

    const patronRole = await strapi
      .query('plugin::users-permissions.role')
      .findOne({ where: { name: 'Patron' }})

    // get the user's patron status
    const patreonBenefitId = await strapi
      .query('api::patreon-benefit-id.patreon-benefit-id')
      .findOne({ where: { id: 1 }})
    console.log(`  >> patreon Benefit Id `)
    console.log(patreonBenefitId)
    console.log(`  >> patreon-benefit-id:${patreonBenefitId.id}`)
    const isPatron = profile.benefits.includes(patreonBenefitId.benefit_id) // "Full library access" benefit

    console.log(`  >> user:`)
    console.log(user)

    // Update the user's role to match their patron status
    const selectedRole = (isPatron) ? patronRole.id : defaultRole.id


    if (!_.isEmpty(user)) {
      const updatedUser = await strapi
        .query('plugin::users-permissions.user')
        .update({
          where: { email },
          data: {
            ...user,
            role: selectedRole 
          }
        })
      return updatedUser;
    }

    if (users.length && advancedSettings.unique_email) {
      throw new Error('Email is already taken.');
    }


    // Create the new user.
    const newUser = {
      ...profile,
      email, // overwrite with lowercased email
      provider,
      role: selectedRole,
      confirmed: true,
    };

    const createdUser = await strapi
      .query('plugin::users-permissions.user')
      .create({ data: newUser });

    return createdUser;
  };

This new connect function depends on a ‘Patron’ role existing in Strapi Roles, as well as the aforementioned patreon-benefit-id single content-type with a {String} ‘benefit_id’ field.

And here’s the patreon section which lives in src/extensions/users-permissions/server/services/providers-registry.js

  async patreon({ accessToken }) {
    console.log(' >> overrriden patreon')
    const patreon = purest({
      provider: 'patreon',
      config: {
        patreon: {
          default: {
            origin: 'https://www.patreon.com',
            path: 'api/oauth2/{path}',
            headers: {
              authorization: 'Bearer {auth}',
            },
          },
        },
      },
    });

    return patreon
      .get('v2/identity')
      .auth(accessToken)
      .qs(new URLSearchParams({ 
        'include': 'memberships,memberships.currently_entitled_tiers,memberships.currently_entitled_tiers.benefits,memberships.campaign',
        'fields[user]': 'full_name,email',
        'fields[member]': 'full_name,is_follower,patron_status,currently_entitled_amount_cents,campaign_lifetime_support_cents',
        'fields[tier]': 'title',
        'fields[benefit]': 'title',
      }).toString())
      .request()
      .then(({ body }) => {
        const patreonData = body.data.attributes;

        let memberships = []
        let benefits = []
        if (body?.included !== undefined) {
          memberships = body.included
            .filter((i) => i.type === 'member')
            .filter((i) => i.attributes.patron_status === 'active_patron')
            .map((i) => i.id)

          benefits = body.included
            .filter((i) => i.type === 'benefit')
            .map((i) => i.id)
        }

        console.log(`memberships:${memberships}, benefits:${benefits}`)

        return {
          username: patreonData.full_name,
          email: patreonData.email,
          memberships: memberships,
          benefits: benefits,
        };
      });

I don’t like this solution because there is so much code unrelated to the solution in the extensions directory. I copied the whole of the users-permissions plugin into that directory and I think it’s only two files, providers.js and 'providers-registry.js` which contains changes. I would love to have less repeated code and override only two functions in the plugin, but I don’t think that’s possible.

Ideally, I wish users-permissions had this functionality built-in. Something that would allow choosing which data to save from the provider profile into the Strapi user object. If that was possible, I think the logic to programmatically add the Strapi Role could be added to a lifecyle hook like the following.

  async bootstrap({ strapi }) {
    strapi.db.lifecycles.subscribe({
      models: ['plugin::users-permissions.user'],

      async beforeCreate(event) {
        // @todo assign appropriate role
        // this is NOT currently possible because the event params only contains provider username and e-mail
      },
    })
  }

Maybe a custom Patreon plugin that handles the auth flow with a nice UI would be something I could make in the future. I’d have to think more about how it could live in harmony with users-permissions though. Or is improving users-permissions the best way? Anyway, my solution above is far from ideal but it is a solution that works so I think I’ll move onto my next challenge.