diff --git a/.changeset/strange-ants-impress.md b/.changeset/strange-ants-impress.md new file mode 100644 index 0000000000000..b761a12b9af47 --- /dev/null +++ b/.changeset/strange-ants-impress.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/model-typings': patch +'@rocket.chat/models': patch +'@rocket.chat/meteor': patch +--- + +Makes roomsPerGuest exclude DMs when counting subscriptions, ensuring guest limits apply only to non-DM rooms as per expected behavior. diff --git a/apps/meteor/ee/app/license/server/startup.ts b/apps/meteor/ee/app/license/server/startup.ts index b81dc8191b43c..f2e5768c96397 100644 --- a/apps/meteor/ee/app/license/server/startup.ts +++ b/apps/meteor/ee/app/license/server/startup.ts @@ -107,7 +107,9 @@ export const startLicense = async () => { License.setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount()); License.setLicenseLimitCounter('guestUsers', () => Users.getActiveLocalGuestCount()); - License.setLicenseLimitCounter('roomsPerGuest', async (context) => (context?.userId ? Subscriptions.countByUserId(context.userId) : 0)); + License.setLicenseLimitCounter('roomsPerGuest', async (context) => + context?.userId ? Subscriptions.countByUserIdExceptType(context.userId, 'd') : 0, + ); License.setLicenseLimitCounter('privateApps', () => getAppCount('private')); License.setLicenseLimitCounter('marketplaceApps', () => getAppCount('marketplace')); License.setLicenseLimitCounter('monthlyActiveContacts', () => LivechatContacts.countContactsOnPeriod(moment.utc().format('YYYY-MM'))); diff --git a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts index 4172e46b594a6..3b421accbf9f5 100644 --- a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts +++ b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts @@ -8,7 +8,9 @@ callbacks.add( 'beforeAddedToRoom', async ({ user }) => { if (user.roles?.includes('guest')) { - if (await License.shouldPreventAction('roomsPerGuest', 0, { userId: user._id })) { + // extraCount = 1 checks if adding one more room would exceed the limit + // (not if they've already exceeded it, since this runs before adding them to the room) + if (await License.shouldPreventAction('roomsPerGuest', 1, { userId: user._id })) { throw new Meteor.Error('error-max-rooms-per-guest-reached', i18n.t('error-max-rooms-per-guest-reached')); } } diff --git a/ee/packages/license/src/license.spec.ts b/ee/packages/license/src/license.spec.ts index 63463a5b5b6c3..378591215c797 100644 --- a/ee/packages/license/src/license.spec.ts +++ b/ee/packages/license/src/license.spec.ts @@ -271,6 +271,32 @@ describe('Validate License Limits', () => { expect(fairUsageCallback).toHaveBeenCalledTimes(0); expect(preventActionCallback).toHaveBeenCalledTimes(0); }); + + it('should check roomsPerGuest with per-user context', async () => { + const licenseManager = await getReadyLicenseManager(); + const license = new MockedLicenseBuilder().withLimits('roomsPerGuest', [ + { + max: 3, + behavior: 'prevent_action', + }, + ]); + + await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true); + + licenseManager.setLicenseLimitCounter('roomsPerGuest', (context) => { + switch (context?.userId) { + case 'user1': + return 2; + case 'user2': + return 3; + default: + return 0; + } + }); + + await expect(licenseManager.shouldPreventAction('roomsPerGuest', 1, { userId: 'user1' })).resolves.toBe(false); + await expect(licenseManager.shouldPreventAction('roomsPerGuest', 1, { userId: 'user2' })).resolves.toBe(true); + }); }); }); diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index 7958feae702e6..d28fb1be57e53 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -329,7 +329,7 @@ export interface ISubscriptionsModel extends IBaseModel { countByRoomIdAndRoles(roomId: string, roles: string[]): Promise; countByRoomId(roomId: string, options?: CountDocumentsOptions): Promise; - countByUserId(userId: string): Promise; + countByUserIdExceptType(userId: string, typeException: ISubscription['t']): Promise; openByRoomIdAndUserId(roomId: string, userId: string): Promise; countByRoomIdAndNotUserId(rid: string, uid: string): Promise; countByRoomIdWhenUsernameExists(rid: string): Promise; diff --git a/packages/models/src/models/Subscriptions.ts b/packages/models/src/models/Subscriptions.ts index e73a4c1913421..520fcd8b3b66a 100644 --- a/packages/models/src/models/Subscriptions.ts +++ b/packages/models/src/models/Subscriptions.ts @@ -1172,8 +1172,11 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.countDocuments(query); } - countByUserId(userId: string): Promise { - const query = { 'u._id': userId }; + countByUserIdExceptType(userId: string, typeException: ISubscription['t']): Promise { + const query: Filter = { + 'u._id': userId, + 't': { $ne: typeException }, + }; return this.countDocuments(query); }