mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-15 05:37:22 +01:00
Establish lock to avoid race scenario in subscriptions (#14267)
* fix(subscription): establish lock to avoid race scenario * fix(lint): import syntax * fix(lint): whitespace, dependency cycle * fix(subs): skip locking on gifts and groups * fix(subs): correctly reset _subSignature * fix(sub): use findOneAndUpdate for unlock * fix(test): save newly created user for some reason Co-authored-by: SabreCat <sabe@habitica.com>
This commit is contained in:
@@ -326,6 +326,7 @@ describe('Apple Payments', () => {
|
|||||||
it('errors when a user is already subscribed', async () => {
|
it('errors when a user is already subscribed', async () => {
|
||||||
payments.createSubscription.restore();
|
payments.createSubscription.restore();
|
||||||
user = new User();
|
user = new User();
|
||||||
|
await user.save();
|
||||||
|
|
||||||
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
|
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { // eslint-disable-line import/no-cycle
|
|||||||
model as Group,
|
model as Group,
|
||||||
basicFields as basicGroupFields,
|
basicFields as basicGroupFields,
|
||||||
} from '../../models/group';
|
} from '../../models/group';
|
||||||
|
import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle
|
||||||
import {
|
import {
|
||||||
NotAuthorized,
|
NotAuthorized,
|
||||||
NotFound,
|
NotFound,
|
||||||
@@ -79,6 +80,22 @@ async function createSubscription (data) {
|
|||||||
let emailType = 'subscription-begins';
|
let emailType = 'subscription-begins';
|
||||||
let recipientIsSubscribed = recipient.isSubscribed();
|
let recipientIsSubscribed = recipient.isSubscribed();
|
||||||
|
|
||||||
|
if (data.user && !data.gift && !data.groupId) {
|
||||||
|
const unlockedUser = await User.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
_id: data.user._id,
|
||||||
|
$or: [
|
||||||
|
{ _subSignature: 'NOT_RUNNING' },
|
||||||
|
{ _subSignature: { $exists: false } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ $set: { _subSignature: 'SUB_IN_PROGRESS' } },
|
||||||
|
);
|
||||||
|
if (!unlockedUser) {
|
||||||
|
throw new NotFound('User not found or subscription already processing.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If we are buying a group subscription
|
// If we are buying a group subscription
|
||||||
if (data.groupId) {
|
if (data.groupId) {
|
||||||
const groupFields = basicGroupFields.concat(' purchased');
|
const groupFields = basicGroupFields.concat(' purchased');
|
||||||
@@ -282,10 +299,6 @@ async function createSubscription (data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group) await group.save();
|
|
||||||
if (data.user && data.user.isModified()) await data.user.save();
|
|
||||||
if (data.gift) await data.gift.member.save();
|
|
||||||
|
|
||||||
slack.sendSubscriptionNotification({
|
slack.sendSubscriptionNotification({
|
||||||
buyer: {
|
buyer: {
|
||||||
id: data.user._id,
|
id: data.user._id,
|
||||||
@@ -302,6 +315,24 @@ async function createSubscription (data) {
|
|||||||
groupId,
|
groupId,
|
||||||
autoRenews,
|
autoRenews,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (group) {
|
||||||
|
await group.save();
|
||||||
|
}
|
||||||
|
if (data.user) {
|
||||||
|
if (data.user.isModified()) {
|
||||||
|
await data.user.save();
|
||||||
|
}
|
||||||
|
if (!data.gift && !data.groupId) {
|
||||||
|
await User.findOneAndUpdate(
|
||||||
|
{ _id: data.user._id },
|
||||||
|
{ $set: { _subSignature: 'NOT_RUNNING' } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.gift) {
|
||||||
|
await data.gift.member.save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancels a subscription or group plan, setting termination to happen later
|
// Cancels a subscription or group plan, setting termination to happen later
|
||||||
|
|||||||
@@ -430,6 +430,8 @@ export default new Schema({
|
|||||||
|
|
||||||
lastCron: { $type: Date, default: Date.now },
|
lastCron: { $type: Date, default: Date.now },
|
||||||
_cronSignature: { $type: String, default: 'NOT_RUNNING' }, // Private property used to avoid double cron
|
_cronSignature: { $type: String, default: 'NOT_RUNNING' }, // Private property used to avoid double cron
|
||||||
|
// Lock property to avoid double subscription. Not strictly private because we query on it
|
||||||
|
_subSignature: { $type: String, default: 'NOT_RUNNING' },
|
||||||
|
|
||||||
// {GROUP_ID: Boolean}, represents whether they have unseen chat messages
|
// {GROUP_ID: Boolean}, represents whether they have unseen chat messages
|
||||||
newMessages: {
|
newMessages: {
|
||||||
|
|||||||
Reference in New Issue
Block a user