RocketChat/Rocket.Chat

View on GitHub
apps/meteor/ee/app/license/server/startup.ts

Summary

Maintainability
C
1 day
Test Coverage
import { api } from '@rocket.chat/core-services';
import type { LicenseLimitKind } from '@rocket.chat/core-typings';
import { applyLicense, applyLicenseOrRemove, License } from '@rocket.chat/license';
import { Subscriptions, Users, Settings, LivechatVisitors } from '@rocket.chat/models';
import { wrapExceptions } from '@rocket.chat/tools';
import moment from 'moment';

import { syncWorkspace } from '../../../../app/cloud/server/functions/syncWorkspace';
import { notifyOnSettingChangedById } from '../../../../app/lib/server/lib/notifyListener';
import { settings } from '../../../../app/settings/server';
import { callbacks } from '../../../../lib/callbacks';
import { getAppCount } from './lib/getAppCount';

export const startLicense = async () => {
    settings.watch<string>('Site_Url', (value) => {
        if (value) {
            void License.setWorkspaceUrl(value);
        }
    });

    License.onValidateLicense(async () => {
        (await Settings.updateValueById('Enterprise_License', License.encryptedLicense)).modifiedCount &&
            void notifyOnSettingChangedById('Enterprise_License');

        (await Settings.updateValueById('Enterprise_License_Status', 'Valid')).modifiedCount &&
            void notifyOnSettingChangedById('Enterprise_License_Status');
    });

    License.onInvalidateLicense(async () => {
        (await Settings.updateValueById('Enterprise_License_Status', 'Invalid')).modifiedCount &&
            void notifyOnSettingChangedById('Enterprise_License_Status');
    });

    License.onRemoveLicense(async () => {
        (await Settings.updateValueById('Enterprise_License', '')).modifiedCount &&
            void notifyOnSettingChangedById('Enterprise_License_Status');

        (await Settings.updateValueById('Enterprise_License_Status', 'Invalid')).modifiedCount &&
            void notifyOnSettingChangedById('Enterprise_License_Status');
    });

    /**
     * This is a debounced function that will sync the workspace data to the cloud.
     * it caches the context, waits for a second and then syncs the data.
     */

    const syncByTriggerDebounced = (() => {
        let timeout: NodeJS.Timeout | undefined;
        const contexts: Set<string> = new Set();
        return async (context: string) => {
            contexts.add(context);
            if (timeout) {
                clearTimeout(timeout);
            }

            timeout = setTimeout(() => {
                timeout = undefined;
                void syncByTrigger([...contexts]);
                contexts.clear();
            }, 1000);
        };
    })();

    const syncByTrigger = async (contexts: string[]) => {
        if (!License.encryptedLicense) {
            return;
        }

        const existingData = wrapExceptions(() => JSON.parse(settings.get<string>('Enterprise_License_Data'))).catch(() => ({})) ?? {};

        const date = new Date();

        const day = date.getDate();
        const month = date.getMonth() + 1;
        const year = date.getFullYear();

        const period = `${year}-${month}-${day}`;

        const [, , signed] = License.encryptedLicense.split('.');

        // Check if this sync has already been done. Based on License, behavior.

        if ([...contexts.values()].every((context) => existingData.signed === signed && existingData[context] === period)) {
            return;
        }

        const obj = Object.fromEntries(contexts.map((context) => [context, period]));

        (
            await Settings.updateValueById(
                'Enterprise_License_Data',
                JSON.stringify({
                    ...(existingData.signed === signed && existingData),
                    ...existingData,
                    ...obj,
                    signed,
                }),
            )
        ).modifiedCount && void notifyOnSettingChangedById('Enterprise_License_Data');

        try {
            await syncWorkspace();
        } catch (error) {
            console.error(error);
        }
    };

    // When settings are loaded, apply the current license if there is one.
    settings.onReady(async () => {
        if (!(await applyLicense(settings.get<string>('Enterprise_License') ?? '', false))) {
            // License from the envvar is always treated as new, because it would have been saved on the setting if it was already in use.
            if (process.env.ROCKETCHAT_LICENSE && !License.hasValidLicense()) {
                await applyLicense(process.env.ROCKETCHAT_LICENSE, true);
            }
        }

        // After the current license is already loaded, watch the setting value to react to new licenses being applied.
        settings.change<string>('Enterprise_License', (license) => applyLicenseOrRemove(license, true));

        callbacks.add('workspaceLicenseRemoved', () => License.remove());

        callbacks.add('workspaceLicenseChanged', (updatedLicense) => applyLicense(updatedLicense, true));

        License.onInstall(async () => void api.broadcast('license.actions', {} as Record<Partial<LicenseLimitKind>, boolean>));

        License.onInvalidate(async () => void api.broadcast('license.actions', {} as Record<Partial<LicenseLimitKind>, boolean>));

        License.onBehaviorTriggered('prevent_action', (context) => syncByTriggerDebounced(`prevent_action_${context.limit}`));

        License.onBehaviorTriggered('start_fair_policy', async (context) => syncByTriggerDebounced(`start_fair_policy_${context.limit}`));

        License.onBehaviorTriggered('disable_modules', async (context) => syncByTriggerDebounced(`disable_modules_${context.limit}`));

        License.onChange(() => api.broadcast('license.sync'));

        License.onBehaviorToggled('prevent_action', (context) => {
            if (!context.limit) {
                return;
            }
            void api.broadcast('license.actions', {
                [context.limit]: true,
            } as Record<Partial<LicenseLimitKind>, boolean>);
        });

        License.onBehaviorToggled('allow_action', (context) => {
            if (!context.limit) {
                return;
            }
            void api.broadcast('license.actions', {
                [context.limit]: false,
            } as Record<Partial<LicenseLimitKind>, boolean>);
        });
    });

    License.setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount());
    License.setLicenseLimitCounter('guestUsers', () => Users.getActiveLocalGuestCount());
    License.setLicenseLimitCounter('roomsPerGuest', async (context) => (context?.userId ? Subscriptions.countByUserId(context.userId) : 0));
    License.setLicenseLimitCounter('privateApps', () => getAppCount('private'));
    License.setLicenseLimitCounter('marketplaceApps', () => getAppCount('marketplace'));
    License.setLicenseLimitCounter('monthlyActiveContacts', () => LivechatVisitors.countVisitorsOnPeriod(moment.utc().format('YYYY-MM')));
};