Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
181 lines
6.6 KiB
TypeScript
181 lines
6.6 KiB
TypeScript
import { api } from '@rocket.chat/core-services';
|
|
import type { LicenseLimitKind } from '@rocket.chat/core-typings';
|
|
import { applyLicense, applyLicenseOrRemove, applyNewestLicense, License } from '@rocket.chat/license';
|
|
import { Subscriptions, Users, Settings, LivechatContacts } from '@rocket.chat/models';
|
|
import { wrapExceptions } from '@rocket.chat/tools';
|
|
import moment from 'moment';
|
|
|
|
import { getAppCount } from './lib/getAppCount';
|
|
import { callbacks } from '../../../../server/lib/callbacks';
|
|
import { syncWorkspace } from '../../../../server/lib/cloud/syncWorkspace';
|
|
import { SystemLogger } from '../../../../server/lib/logger/system';
|
|
import { notifyOnSettingChangedById } from '../../../../server/lib/notifyListener';
|
|
import { settings } from '../../../../server/settings';
|
|
|
|
const applyStartupLicense = async (): Promise<void> => {
|
|
// Licenses can't be validated before the workspace URL is known; set it explicitly
|
|
// so the startup license is never left pending behind the Site_Url watcher.
|
|
await License.setWorkspaceUrl(settings.get<string>('Site_Url'));
|
|
|
|
await applyNewestLicense(License, settings.get<string>('Enterprise_License') ?? '', process.env.ROCKETCHAT_LICENSE ?? '');
|
|
};
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
License.setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount());
|
|
License.setLicenseLimitCounter('guestUsers', () => Users.getActiveLocalGuestCount());
|
|
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')));
|
|
|
|
return new Promise<void>((resolve) => {
|
|
// When settings are loaded, apply the current license if there is one.
|
|
settings.onReady(async () => {
|
|
import('./airGappedRestrictions');
|
|
await applyStartupLicense();
|
|
|
|
License.onInstall(() => {
|
|
if (License.hasOfflineLicense()) {
|
|
// startup level so it is visible at the default Log_Level, like 'License installed'
|
|
SystemLogger.startup(
|
|
'Offline license detected: outbound connections to Rocket.Chat Cloud services and the Rocket.Chat Push Gateway are disabled',
|
|
);
|
|
}
|
|
});
|
|
|
|
// 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>);
|
|
});
|
|
resolve();
|
|
});
|
|
});
|
|
};
|