Skip to content
3 changes: 2 additions & 1 deletion client/admin/sidebar/AdminSidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useAtLeastOnePermission } from '../../contexts/AuthorizationContext';
import Sidebar from '../../components/basic/Sidebar';
import SettingsProvider from '../../providers/SettingsProvider';
import { itemsSubscription } from '../sidebarItems';
import PlanTag from '../../components/basic/PlanTag';

const AdminSidebarPages = React.memo(({ currentPath }) => {
const items = useSubscription(itemsSubscription);
Expand Down Expand Up @@ -134,7 +135,7 @@ export default React.memo(function AdminSidebar() {
// TODO: uplift this provider
return <SettingsProvider privileged>
<Sidebar>
<Sidebar.Header onClose={closeAdminFlex} title={t('Administration')}/>
<Sidebar.Header onClose={closeAdminFlex} title={<>{t('Administration')} <PlanTag/></>}/>
<Sidebar.Content>
<AdminSidebarPages currentPath={currentPath}/>
{canViewSettings && <AdminSidebarSettings currentPath={currentPath}/>}
Expand Down
22 changes: 22 additions & 0 deletions client/components/basic/PlanTag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { useEffect, useState } from 'react';
import { Tag } from '@rocket.chat/fuselage';
import { useSafely } from '@rocket.chat/fuselage-hooks';

import { useMethod } from '../../contexts/ServerContext';

function PlanTag() {
const [plans, setPlans] = useSafely(useState([]));

const getTags = useMethod('license:getTags');

useEffect(() => {
(async () => {
const tags = await getTags();
setPlans([process.env.NODE_ENV === 'development' && { name: 'development', color: 'primary-600' }, ...tags].filter(Boolean).map((plan) => ({ plan: plan.name, background: plan.color })));
})();
}, []);

return plans.map(({ plan, background }) => <Tag key={plan} backgroundColor={background} marginInline='x4' color='#fff' textTransform='capitalize'>{plan}</Tag>);
}

export default PlanTag;
4 changes: 2 additions & 2 deletions ee/app/license/server/bundles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const bundles: IBundle = {
],
};

const getBundleFromModule = (moduleName: string): string|undefined => {
export const getBundleFromModule = (moduleName: string): string|undefined => {
const match = moduleName.match(/(.*):\*$/);
if (!match) {
return;
Expand Down Expand Up @@ -43,7 +43,7 @@ export function getBundleModules(moduleName: string): string[] {
}

const bundle = getBundleFromModule(moduleName);
if (!bundle) {
if (!bundle || !bundles[bundle]) {
return [];
}

Expand Down
8 changes: 8 additions & 0 deletions ee/app/license/server/getTagColor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function getTagColor(tag: string): string {
switch (tag) {
case 'bronze': return '#BD5A0B';
case 'silver': return '#9EA2A8';
case 'gold': return '#F3BE08';
default: return '#4411DD';
}
}
57 changes: 50 additions & 7 deletions ee/app/license/server/license.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { EventEmitter } from 'events';

import { Users } from '../../../../app/models/server';
import { resetEnterprisePermissions } from '../../authorization/server/resetEnterprisePermissions';
import { addRoleRestrictions } from '../../authorization/lib/addRoleRestrictions';
import { resetEnterprisePermissions } from '../../authorization/server/resetEnterprisePermissions';
import { getBundleModules, isBundle, getBundleFromModule } from './bundles';
import decrypt from './decrypt';
import { getBundleModules, isBundle } from './bundles';
import { getTagColor } from './getTagColor';

const EnterpriseLicenses = new EventEmitter();

interface ILicenseTag {
name: string;
color: string;
}

export interface ILicense {
url: string;
expiry: string;
maxActiveUsers: number;
modules: string[];
maxGuestUsers: number;
maxRoomsPerGuest: number;
tag?: ILicenseTag;
}

export interface IValidLicense {
Expand All @@ -30,13 +37,15 @@ class LicenseClass {

private licenses: IValidLicense[] = [];

private tags = new Set<ILicenseTag>();

private modules = new Set<string>();

_validateExpiration(expiration: string): boolean {
private _validateExpiration(expiration: string): boolean {
return new Date() > new Date(expiration);
}

_validateURL(licenseURL: string, url: string): boolean {
private _validateURL(licenseURL: string, url: string): boolean {
licenseURL = licenseURL
.replace(/\./g, '\\.') // convert dots to literal
.replace(/\*/g, '.*'); // convert * to .*
Expand All @@ -45,7 +54,7 @@ class LicenseClass {
return !!regex.exec(url);
}

_validModules(licenseModules: string[]): void {
private _validModules(licenseModules: string[]): void {
licenseModules.forEach((licenseModule) => {
const modules = isBundle(licenseModule)
? getBundleModules(licenseModule)
Expand All @@ -58,7 +67,7 @@ class LicenseClass {
});
}

_invalidModules(licenseModules: string[]): void {
private _invalidModules(licenseModules: string[]): void {
licenseModules.forEach((licenseModule) => {
const modules = isBundle(licenseModule)
? getBundleModules(licenseModule)
Expand All @@ -68,10 +77,34 @@ class LicenseClass {
});
}

_hasValidNumberOfActiveUsers(maxActiveUsers: number): boolean {
private _hasValidNumberOfActiveUsers(maxActiveUsers: number): boolean {
return Users.getActiveLocalUserCount() <= maxActiveUsers;
}

private _addTags(license: ILicense): void {
// if no tag present, it means it is an old license, so try check for bundles and use them as tags
if (typeof license.tag === 'undefined') {
license.modules
.filter(isBundle)
.map(getBundleFromModule)
.forEach((tag) => tag && this._addTag({ name: tag, color: getTagColor(tag) }));
return;
}

this._addTag(license.tag);
}

private _addTag(tag: ILicenseTag): void {
// make sure to not add duplicated tag names
for (const addedTag of this.tags) {
if (addedTag.name.toLowerCase() === tag.name.toLowerCase()) {
return;
}
}

this.tags.add(tag);
}

addLicense(license: ILicense): void {
this.licenses.push({
valid: undefined,
Expand Down Expand Up @@ -99,6 +132,10 @@ class LicenseClass {
return [...this.modules];
}

getTags(): ILicenseTag[] {
return [...this.tags];
}

setURL(url: string): void {
this.url = url.replace(/\/$/, '').replace(/^https?:\/\/(.*)$/, '$1');

Expand Down Expand Up @@ -138,6 +175,8 @@ class LicenseClass {

this._validModules(license.modules);

this._addTags(license);

console.log('#### License validated:', license.modules.join(', '));

item.valid = true;
Expand Down Expand Up @@ -220,6 +259,10 @@ export function getModules(): string[] {
return License.getModules();
}

export function getTags(): ILicenseTag[] {
return License.getTags();
}

export function onLicense(feature: string, cb: (...args: any[]) => void): void {
if (hasLicense(feature)) {
return cb();
Expand Down
7 changes: 5 additions & 2 deletions ee/app/license/server/methods.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { hasLicense, getModules, isEnterprise } from './license';
import { getModules, getTags, hasLicense, isEnterprise } from './license';

Meteor.methods({
'license:hasLicense'(feature: string) {
Expand All @@ -12,6 +12,9 @@ Meteor.methods({
'license:getModules'() {
return getModules();
},
'license:getTags'() {
return getTags();
},
'license:isEnterprise'() {
return isEnterprise();
},
Expand Down