From a98f3ff303b2dac8e4c96947fc28ec9ef7e0d74c Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Tue, 3 Oct 2023 17:27:17 -0300 Subject: [PATCH] feat: new `licenses.info` endpoint (#30473) --- .changeset/tough-carrots-walk.md | 7 ++++ apps/meteor/ee/server/api/licenses.ts | 13 ++++++++ ee/packages/license/src/index.ts | 10 +++++- ee/packages/license/src/license.ts | 41 +++++++++++++++++++++++- packages/rest-typings/src/index.ts | 1 + packages/rest-typings/src/v1/licenses.ts | 29 ++++++++++++++++- 6 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 .changeset/tough-carrots-walk.md diff --git a/.changeset/tough-carrots-walk.md b/.changeset/tough-carrots-walk.md new file mode 100644 index 000000000000..2851e697b85e --- /dev/null +++ b/.changeset/tough-carrots-walk.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/license': patch +'@rocket.chat/meteor': patch +--- + +feat: added `licenses.info` endpoint diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index cfd657a1f0e9..ff5c3fcc3e47 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -1,5 +1,6 @@ import { License } from '@rocket.chat/license'; import { Settings, Users } from '@rocket.chat/models'; +import { isLicensesInfoProps } from '@rocket.chat/rest-typings'; import { check } from 'meteor/check'; import { API } from '../../../app/api/server/api'; @@ -22,6 +23,18 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'licenses.info', + { authRequired: true, validateParams: isLicensesInfoProps, permissionsRequired: ['view-privileged-setting'] }, + { + async get() { + const data = await License.getInfo(Boolean(this.queryParams.loadValues)); + + return API.v1.success({ data }); + }, + }, +); + API.v1.addRoute( 'licenses.add', { authRequired: true }, diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index 9dbd94db53ed..c5dbd9f9496f 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -1,4 +1,5 @@ -import type { LicenseLimitKind } from './definition/ILicenseV3'; +import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; +import type { LicenseModule } from './definition/LicenseModule'; import type { LimitContext } from './definition/LimitContext'; import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; import { onLicense } from './events/deprecated'; @@ -45,6 +46,13 @@ interface License { onInvalidateLicense: typeof onInvalidateLicense; onLimitReached: typeof onLimitReached; + getInfo: (loadCurrentValues: boolean) => Promise<{ + license: ILicenseV3 | undefined; + activeModules: LicenseModule[]; + limits: Record; + inFairPolicy: boolean; + }>; + // Deprecated: onLicense: typeof onLicense; // Deprecated: diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 2fb25b0e3b4f..a420eb2b0d57 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -9,7 +9,7 @@ import { DuplicatedLicenseError } from './errors/DuplicatedLicenseError'; import { InvalidLicenseError } from './errors/InvalidLicenseError'; import { NotReadyForValidation } from './errors/NotReadyForValidation'; import { logger } from './logger'; -import { invalidateAll, replaceModules } from './modules'; +import { getModules, invalidateAll, replaceModules } from './modules'; import { applyPendingLicense, clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense'; import { showLicense } from './showLicense'; import { replaceTags } from './tags'; @@ -227,4 +227,43 @@ export class LicenseManager extends Emitter< .some(({ max }) => max < currentValue), ); } + + public async getInfo(loadCurrentValues = false): Promise<{ + license: ILicenseV3 | undefined; + activeModules: LicenseModule[]; + limits: Record; + inFairPolicy: boolean; + }> { + const activeModules = getModules.call(this); + const license = this.getLicense(); + + // Get all limits present in the license and their current value + const limits = ( + (license && + (await Promise.all( + (['activeUsers', 'guestUsers', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts'] as LicenseLimitKind[]) + .map((limitKey) => ({ + limitKey, + max: Math.max(-1, Math.min(...Array.from(license.limits[limitKey as LicenseLimitKind] || [])?.map(({ max }) => max))), + })) + .filter(({ max }) => max >= 0 && max < Infinity) + .map(async ({ max, limitKey }) => { + return { + [limitKey as LicenseLimitKind]: { + ...(loadCurrentValues ? { value: await getCurrentValueForLicenseLimit.call(this, limitKey as LicenseLimitKind) } : {}), + max, + }, + }; + }), + ))) || + [] + ).reduce((prev, curr) => ({ ...prev, ...curr }), {}); + + return { + license, + activeModules, + limits: limits as Record, + inFairPolicy: this.inFairPolicy, + }; + } } diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 066e3248dc33..3b8197ce20bf 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -228,6 +228,7 @@ export * from './v1/invites'; export * from './v1/dm'; export * from './v1/dm/DmHistoryProps'; export * from './v1/integrations'; +export * from './v1/licenses'; export * from './v1/omnichannel'; export * from './v1/oauthapps'; export * from './v1/oauthapps/UpdateOAuthAppParamsPOST'; diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts index 96c67e2654bb..6dc935aae739 100644 --- a/packages/rest-typings/src/v1/licenses.ts +++ b/packages/rest-typings/src/v1/licenses.ts @@ -1,4 +1,4 @@ -import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/license'; +import type { ILicenseV2, ILicenseV3, LicenseLimitKind } from '@rocket.chat/license'; import Ajv from 'ajv'; const ajv = new Ajv({ @@ -22,10 +22,37 @@ const licensesAddPropsSchema = { export const isLicensesAddProps = ajv.compile(licensesAddPropsSchema); +type licensesInfoProps = { + loadValues?: boolean; +}; + +const licensesInfoPropsSchema = { + type: 'object', + properties: { + loadValues: { + type: 'boolean', + }, + }, + required: [], + additionalProperties: false, +}; + +export const isLicensesInfoProps = ajv.compile(licensesInfoPropsSchema); + export type LicensesEndpoints = { '/v1/licenses.get': { GET: () => { licenses: Array }; }; + '/v1/licenses.info': { + GET: (params: licensesInfoProps) => { + data: { + license: ILicenseV3 | undefined; + activeModules: string[]; + limits: Record; + inFairPolicy: boolean; + }; + }; + }; '/v1/licenses.add': { POST: (params: licensesAddProps) => void; };