1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-17 18:44:45 +02:00

Server: Handle flags for accounts over limit

This commit is contained in:
Laurent Cozic 2021-08-22 13:10:29 +01:00
parent f922e9a239
commit 6e087bcb23
5 changed files with 188 additions and 2 deletions

View File

@ -2,7 +2,7 @@ import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models,
import { EmailSender, User, UserFlagType } from '../services/database/types';
import { ErrorUnprocessableEntity } from '../utils/errors';
import { betaUserDateRange, stripeConfig } from '../utils/stripe';
import { AccountType } from './UserModel';
import { accountByType, AccountType } from './UserModel';
import { failedPaymentDisableUploadInterval } from './SubscriptionModel';
import { stripePortalUrl } from '../utils/urlUtils';
@ -203,4 +203,68 @@ describe('UserModel', function() {
}
});
test('should send emails when the account is over the size limit', async function() {
const { user: user1 } = await createUserAndSession(1);
const { user: user2 } = await createUserAndSession(2);
await models().user().save({
id: user1.id,
account_type: AccountType.Basic,
total_item_size: accountByType(AccountType.Basic).max_total_item_size * 0.85,
});
await models().user().save({
id: user2.id,
account_type: AccountType.Pro,
total_item_size: accountByType(AccountType.Pro).max_total_item_size * 0.2,
});
const emailBeforeCount = (await models().email().all()).length;
await models().user().handleOversizedAccounts();
const emailAfterCount = (await models().email().all()).length;
expect(emailAfterCount).toBe(emailBeforeCount + 1);
const email = (await models().email().all()).pop();
expect(email.recipient_id).toBe(user1.id);
expect(email.subject).toContain('80%');
{
// Running it again should not send a second email
await models().user().handleOversizedAccounts();
expect((await models().email().all()).length).toBe(emailBeforeCount + 1);
}
{
// Now check that the 100% email is sent too
await models().user().save({
id: user2.id,
total_item_size: accountByType(AccountType.Pro).max_total_item_size * 1.1,
});
// User upload should be enabled at this point
expect((await models().user().load(user2.id)).can_upload).toBe(1);
const emailBeforeCount = (await models().email().all()).length;
await models().user().handleOversizedAccounts();
const emailAfterCount = (await models().email().all()).length;
// User upload should be disabled
expect((await models().user().load(user2.id)).can_upload).toBe(0);
expect(emailAfterCount).toBe(emailBeforeCount + 1);
const email = (await models().email().all()).pop();
expect(email.recipient_id).toBe(user2.id);
expect(email.subject).toContain('100%');
// Running it again should not send a second email
await models().user().handleOversizedAccounts();
expect((await models().email().all()).length).toBe(emailBeforeCount + 1);
}
});
});

View File

@ -16,6 +16,9 @@ import { betaStartSubUrl, betaUserDateRange, betaUserTrialPeriodDays, isBetaUser
import endOfBetaTemplate from '../views/emails/endOfBetaTemplate';
import Logger from '@joplin/lib/Logger';
import paymentFailedUploadDisabledTemplate from '../views/emails/paymentFailedUploadDisabledTemplate';
import oversizedAccount1 from '../views/emails/oversizedAccount1';
import oversizedAccount2 from '../views/emails/oversizedAccount2';
import dayjs = require('dayjs');
const logger = Logger.create('UserModel');
@ -188,7 +191,7 @@ export default class UserModel extends BaseModel<User> {
const maxItemSize = getMaxItemSize(user);
const maxSize = maxItemSize * (itemIsEncrypted(item) ? 2.2 : 1);
if (maxSize && itemSize > maxSize) {
throw new ErrorPayloadTooLarge(_('Cannot save %s "%s" because it is larger than than the allowed limit (%s)',
throw new ErrorPayloadTooLarge(_('Cannot save %s "%s" because it is larger than the allowed limit (%s)',
isNote ? _('note') : _('attachment'),
itemTitle ? itemTitle : item.name,
formatBytes(maxItemSize)
@ -369,6 +372,67 @@ export default class UserModel extends BaseModel<User> {
});
}
public async handleOversizedAccounts() {
const alertLimit1 = 0.8;
const alertLimitMax = 1;
const basicAccount = accountByType(AccountType.Basic);
const proAccount = accountByType(AccountType.Pro);
const users: User[] = await this
.db(this.tableName)
.select(['id', 'total_item_size', 'max_total_item_size', 'account_type', 'email', 'full_name'])
.where(function() {
void this.whereRaw('total_item_size > ? AND account_type = ?', [alertLimit1 * basicAccount.max_total_item_size, AccountType.Basic])
.orWhereRaw('total_item_size > ? AND account_type = ?', [alertLimit1 * proAccount.max_total_item_size, AccountType.Pro]);
})
// Users who are disabled or who cannot upload already received the
// notification.
.andWhere('enabled', '=', 1)
.andWhere('can_upload', '=', 1);
const makeEmailKey = (user: User, alertLimit: number): string => {
return [
'oversizedAccount',
user.account_type,
alertLimit * 100,
// Also add the month/date to the key so that we don't send more than one email a month
dayjs(Date.now()).format('MMYY'),
].join('::');
};
await this.withTransaction(async () => {
for (const user of users) {
const maxTotalItemSize = getMaxTotalItemSize(user);
const account = accountByType(user.account_type);
if (user.total_item_size > maxTotalItemSize * alertLimitMax) {
await this.models().email().push({
...oversizedAccount2({
percentLimit: alertLimitMax * 100,
url: this.baseUrl,
}),
...this.userEmailDetails(user),
sender_id: EmailSender.Support,
key: makeEmailKey(user, alertLimitMax),
});
await this.models().userFlag().add(user.id, UserFlagType.AccountOverLimit);
} else if (maxTotalItemSize > account.max_total_item_size * alertLimit1) {
await this.models().email().push({
...oversizedAccount1({
percentLimit: alertLimit1 * 100,
url: this.baseUrl,
}),
...this.userEmailDetails(user),
sender_id: EmailSender.Support,
key: makeEmailKey(user, alertLimit1),
});
}
}
});
}
private formatValues(user: User): User {
const output: User = { ...user };
if ('email' in output) output.email = user.email.trim().toLowerCase();

View File

@ -33,6 +33,10 @@ export default class CronService extends BaseService {
cron.schedule('0 13 * * *', async () => {
await runCronTask('handleFailedPaymentSubscriptions', async () => this.models.user().handleFailedPaymentSubscriptions());
});
cron.schedule('0 14 * * *', async () => {
await runCronTask('handleOversizedAccounts', async () => this.models.user().handleOversizedAccounts());
});
}
}

View File

@ -0,0 +1,28 @@
import config from '../../config';
import { EmailSubjectBody } from '../../models/EmailModel';
interface TemplateView {
percentLimit: number;
url: string;
}
export default function(view: TemplateView): EmailSubjectBody {
return {
subject: `Your ${config().appName} account is over ${view.percentLimit}% full`,
body: `
Your ${config().appName} account is over ${view.percentLimit}% full.
Please consider deleting notes or attachments before it reaches its limit.
Once the account is full it will no longer be possible to upload new notes to it.
If you have Pro account and would like to request more space, please contact us by replying to this email.
You may access your account by following this URL:
${view.url}
`.trim(),
};
}

View File

@ -0,0 +1,26 @@
import config from '../../config';
import { EmailSubjectBody } from '../../models/EmailModel';
interface TemplateView {
percentLimit: number;
url: string;
}
export default function(view: TemplateView): EmailSubjectBody {
return {
subject: `Your ${config().appName} account is over ${view.percentLimit}% full`,
body: `
Your ${config().appName} account is over ${view.percentLimit}% full, and as a result it is not longer possible to upload new notes to it.
Please consider deleting notes or attachments so as to go below the limit.
If you have Pro account and would like to request more space, please contact us by replying to this email.
You may access your account by following this URL:
${view.url}
`.trim(),
};
}