Files
DiceCloud/app/imports/api/users/Invites.js
2023-09-28 21:27:05 +02:00

201 lines
5.6 KiB
JavaScript

import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { getUserTier } from '/imports/api/users/patreon/tiers';
let Invites = new Mongo.Collection('invites');
let InviteSchema = new SimpleSchema({
inviter: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
invitee: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
index: 1,
},
inviteToken: {
type: String,
optional: true,
index: 1,
unique: 1,
},
isFunded: {
type: Boolean,
},
// The timestamp of when the invitee was confirmed
// Older invites have priority over newer ones
dateConfirmed: {
type: Date,
optional: true,
},
});
if (Meteor.isServer) {
Accounts.onLogin(function ({ user }) {
alignInvitesWithPatreonTier(user);
});
}
function alignInvitesWithPatreonTier(user) {
const tier = getUserTier(user);
let availableInvites = tier.invites;
let currentlyFundedInvites = [];
let currenltyUnfundedInvites = [];
Invites.find({
inviter: user._id
}).forEach(invite => {
if (invite.isFunded) {
currentlyFundedInvites.push(invite);
} else {
currenltyUnfundedInvites.push(invite);
}
});
// Return early if no work needs doing to skip sorting
if (currentlyFundedInvites.length === availableInvites) return;
// Sort the invites by date forwards and backwards
currentlyFundedInvites.sort((a, b) => a.dateConfirmed - b.dateConfirmed);
currenltyUnfundedInvites.sort((a, b) => b.dateConfirmed - a.dateConfirmed);
// Defund or delete excess invites
while (currentlyFundedInvites.length > availableInvites) {
let inviteToDefund = currentlyFundedInvites.pop();
if (inviteToDefund.invitee) {
Invites.update(inviteToDefund._id, { $set: { isFunded: false } });
} else {
Invites.remove(inviteToDefund._id);
}
}
// Fund unfunded invites or insert new ones
while (currentlyFundedInvites.length < availableInvites) {
if (currenltyUnfundedInvites.length) {
let inviteToFund = currenltyUnfundedInvites.pop();
currentlyFundedInvites.push(inviteToFund);
Invites.update(inviteToFund._id, { $set: { isFunded: true } });
} else {
let inviteId = Invites.insert({ inviter: user._id, isFunded: true });
currentlyFundedInvites.push({ _id: inviteId });
}
}
}
const getInviteToken = new ValidatedMethod({
name: 'invites.getToken',
validate: new SimpleSchema({
inviteId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ inviteId }) {
let invite = Invites.findOne(inviteId);
if (this.userId !== invite.inviter) {
throw new Meteor.Error('Invites.methods.getToken.denied',
'You need to be the inviter of the invite to create a token');
}
if (invite.inviteToken) {
return invite.inviteToken;
} else {
let inviteToken = Random.id(5);
Invites.update(inviteId, { $set: { inviteToken } })
return inviteToken;
}
},
});
const acceptInviteToken = new ValidatedMethod({
name: 'invites.acceptToken',
validate: new SimpleSchema({
inviteToken: {
type: String,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ inviteToken }) {
if (!this.userId) {
throw new Meteor.Error('Invites.methods.acceptToken.denied',
'You need to be the logged in to accept a token');
}
if (Meteor.isClient) return;
let invite = Invites.findOne({ inviteToken });
if (!invite) {
throw new Meteor.Error('Invites.methods.acceptToken.notFound',
'No invite could be found for this link, maybe it has already been claimed');
}
// If the invitee is already filled, fix unexpected case by deleting the token
if (invite.invitee) {
Invites.update(invite._id, {
$unset: { inviteToken: 1 }
});
throw new Meteor.Error('Invites.methods.acceptToken.alreadyAccepted',
'This invite has already been claimed');
}
if (this.userId === invite.inviter) {
throw new Meteor.Error('Invites.methods.acceptToken.ownToken',
'You can\'t accept your own invite');
}
Invites.update(invite._id, {
$set: { invitee: this.userId },
$unset: { inviteToken: 1 },
});
},
});
const revokeInvite = new ValidatedMethod({
name: 'invites.revokeInvite',
validate: new SimpleSchema({
inviteId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ inviteId }) {
if (!this.userId) {
throw new Meteor.Error('Invites.methods.revokeInvite.denied',
'You need to be the logged in to revoke a token');
}
if (Meteor.isClient) return;
let invite = Invites.findOne(inviteId);
if (!invite) {
throw new Meteor.Error('Invites.methods.revokeInvite.notFound',
'No invite could be found for this id');
}
if (this.userId !== invite.inviter) {
throw new Meteor.Error('Invites.methods.revokeInvite.denied',
'You are not the owner of this invite');
}
// If the invitee is empty, the token has already been revoked
if (!invite.invitee) {
return;
}
Invites.update(invite._id, {
$unset: { invitee: 1, dateConfirmed: 1 },
});
},
});
Invites.attachSchema(InviteSchema);
export default Invites;
export { alignInvitesWithPatreonTier, getInviteToken, acceptInviteToken, revokeInvite };