Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 3 additions & 16 deletions apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,36 +49,25 @@ const fetchCloudWorkspaceLicensePayload = async ({ token }: { token: string }):
return payload;
};

export async function getWorkspaceLicense(): Promise<{ updated: boolean; license: string }> {
export async function getWorkspaceLicense() {
const currentLicense = await Settings.findOne('Cloud_Workspace_License');
// it should never happen, since even if the license is not found, it will return an empty settings
if (!currentLicense?._updatedAt) {
throw new CloudWorkspaceLicenseError('Failed to retrieve current license');
}

const fromCurrentLicense = async () => {
const license = currentLicense?.value as string | undefined;
if (license) {
await callbacks.run('workspaceLicenseChanged', license);
}

return { updated: false, license: license ?? '' };
};

try {
const token = await getWorkspaceAccessToken();
if (!token) {
return fromCurrentLicense();
return;
}

const payload = await fetchCloudWorkspaceLicensePayload({ token });

if (currentLicense.value && Date.parse(payload.updatedAt) <= currentLicense._updatedAt.getTime()) {
return fromCurrentLicense();
return;
}

await Settings.updateValueById('Cloud_Workspace_License', payload.license);

await callbacks.run('workspaceLicenseChanged', payload.license);

return { updated: true, license: payload.license };
Expand All @@ -88,7 +77,5 @@ export async function getWorkspaceLicense(): Promise<{ updated: boolean; license
url: '/license',
err,
});

return fromCurrentLicense();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Cloud, Serialized } from '@rocket.chat/core-typings';
import { DuplicatedLicenseError } from '@rocket.chat/license';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import { v, compile } from 'suretype';

Expand Down Expand Up @@ -74,14 +75,41 @@ export async function syncCloudData() {

await callbacks.run('workspaceLicenseChanged', license);

SystemLogger.info({
msg: 'Synced with Rocket.Chat Cloud',
function: 'syncCloudData',
});

return true;
} catch (err) {
/**
* If some of CloudWorkspaceAccessError and CloudWorkspaceRegistrationError happens, makes no sense to run the legacySyncWorkspace
* because it will fail too.
* The DuplicatedLicenseError license error is also ignored because it is not a problem. the Cloud is allowed to send the same license twice.
*/
switch (true) {
case err instanceof CloudWorkspaceAccessError:
case err instanceof CloudWorkspaceRegistrationError:
case err instanceof DuplicatedLicenseError:
SystemLogger.info({
msg: 'Failed to sync with Rocket.Chat Cloud',
function: 'syncCloudData',
err,
});
return;
}

SystemLogger.error({
msg: 'Failed to sync with Rocket.Chat Cloud',
url: '/sync',
err,
});
}

SystemLogger.info({
msg: 'Falling back to legacy sync',
function: 'syncCloudData',
});

await legacySyncWorkspace();
}
11 changes: 3 additions & 8 deletions apps/meteor/client/hooks/useIsEnterprise.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import type { OperationResult } from '@rocket.chat/rest-typings';
import { useEndpoint } from '@rocket.chat/ui-contexts';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';

export const useIsEnterprise = (): UseQueryResult<OperationResult<'GET', '/v1/licenses.isEnterprise'>> => {
const isEnterpriseEdition = useEndpoint('GET', '/v1/licenses.isEnterprise');
import { useLicenseBase } from './useLicense';

return useQuery(['licenses', 'isEnterprise'], () => isEnterpriseEdition(), {
keepPreviousData: true,
staleTime: Infinity,
});
export const useIsEnterprise = (): UseQueryResult<OperationResult<'GET', '/v1/licenses.isEnterprise'>> => {
return useLicenseBase({ select: (data) => ({ isEnterprise: Boolean(data?.license.license) }) });
};
37 changes: 19 additions & 18 deletions apps/meteor/client/hooks/useLicense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { QueryClient, UseQueryResult } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';

type LicenseDataType = Awaited<OperationResult<'GET', '/v1/licenses.info'>>['license'];
type LicenseDataType = Serialized<Awaited<OperationResult<'GET', '/v1/licenses.info'>>>;

type LicenseParams = {
loadValues?: boolean;
Expand All @@ -18,12 +18,18 @@ const invalidateQueryClientLicenses = (() => {
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = undefined;
queryClient.invalidateQueries(['licenses', 'getLicenses']);
queryClient.invalidateQueries(['licenses']);
}, 5000);
};
})();

export const useLicense = (params?: LicenseParams): UseQueryResult<Serialized<LicenseDataType>> => {
export const useLicenseBase = <TData = LicenseDataType>({
params,
select,
}: {
params?: LicenseParams;
select: (data: LicenseDataType) => TData;
}) => {
const uid = useUserId();

const getLicenses = useEndpoint('GET', '/v1/licenses.info');
Expand All @@ -34,32 +40,27 @@ export const useLicense = (params?: LicenseParams): UseQueryResult<Serialized<Li

useEffect(() => notify('license', () => invalidateQueries()), [notify, invalidateQueries]);

return useQuery(['licenses', 'getLicenses', params?.loadValues], () => getLicenses({ ...params }), {
return useQuery(['licenses', 'getLicenses', params], () => getLicenses({ ...params }), {
staleTime: Infinity,
keepPreviousData: true,
select: (data) => data.license,
select,
enabled: !!uid,
});
};

export const useLicenseName = (params?: LicenseParams) => {
const getLicenses = useEndpoint('GET', '/v1/licenses.info');

const invalidateQueries = useInvalidateLicense();

const notify = useSingleStream('notify-all');
export const useLicense = (params?: LicenseParams) => {
return useLicenseBase({ params, select: (data) => data.license });
};

useEffect(() => notify('license', () => invalidateQueries()), [notify, invalidateQueries]);
export const useHasLicense = (): UseQueryResult<boolean> => {
return useLicenseBase({ select: (data) => Boolean(data.license) });
};

return useQuery(['licenses', 'getLicenses', params?.loadValues], () => getLicenses({ ...params }), {
staleTime: Infinity,
keepPreviousData: true,
select: (data) => data.license.tags?.map((tag) => tag.name).join(' ') ?? 'Community',
});
export const useLicenseName = (params?: LicenseParams) => {
return useLicenseBase({ params, select: (data) => data?.license.tags?.map((tag) => tag.name).join(' ') ?? 'Community' });
};

export const useInvalidateLicense = () => {
const queryClient = useQueryClient();

return () => invalidateQueryClientLicenses(queryClient);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { useAuditItems } from './useAuditItems';
it('should return an empty array if doesn`t have license', async () => {
const { result, waitFor } = renderHook(() => useAuditItems(), {
wrapper: mockAppRoot()
.withMethod('license:getModules', () => [])
.withEndpoint('GET', '/v1/licenses.info', () => ({
// @ts-expect-error: just for testing
license: {
activeModules: [],
},
}))
.withJohnDoe()
.withPermission('can-audit')
.withPermission('can-audit-log')
Expand All @@ -21,6 +26,16 @@ it('should return an empty array if doesn`t have license', async () => {
it('should return an empty array if have license and not have permissions', async () => {
const { result, waitFor } = renderHook(() => useAuditItems(), {
wrapper: mockAppRoot()
.withEndpoint('GET', '/v1/licenses.info', () => ({
license: {
license: {
// @ts-expect-error: just for testing
grantedModules: [{ module: 'auditing' }],
},
// @ts-expect-error: just for testing
activeModules: ['auditing'],
},
}))
.withMethod('license:getModules', () => ['auditing'])
.withJohnDoe()
.build(),
Expand All @@ -34,7 +49,16 @@ it('should return an empty array if have license and not have permissions', asyn
it('should return auditItems if have license and permissions', async () => {
const { result, waitFor } = renderHook(() => useAuditItems(), {
wrapper: mockAppRoot()
.withMethod('license:getModules', () => ['auditing'])
.withEndpoint('GET', '/v1/licenses.info', () => ({
license: {
license: {
// @ts-expect-error: just for testing
grantedModules: [{ module: 'auditing' }],
},
// @ts-expect-error: just for testing
activeModules: ['auditing'],
},
}))
.withJohnDoe()
.withPermission('can-audit')
.withPermission('can-audit-log')
Expand All @@ -59,7 +83,16 @@ it('should return auditItems if have license and permissions', async () => {
it('should return auditMessages item if have license and can-audit permission', async () => {
const { result, waitFor } = renderHook(() => useAuditItems(), {
wrapper: mockAppRoot()
.withMethod('license:getModules', () => ['auditing'])
.withEndpoint('GET', '/v1/licenses.info', () => ({
license: {
license: {
// @ts-expect-error: just for testing
grantedModules: [{ module: 'auditing' }],
},
// @ts-expect-error: just for testing
activeModules: ['auditing'],
},
}))
.withJohnDoe()
.withPermission('can-audit')
.build(),
Expand All @@ -77,7 +110,16 @@ it('should return auditMessages item if have license and can-audit permission',
it('should return audiLogs item if have license and can-audit-log permission', async () => {
const { result, waitFor } = renderHook(() => useAuditItems(), {
wrapper: mockAppRoot()
.withMethod('license:getModules', () => ['auditing'])
.withEndpoint('GET', '/v1/licenses.info', () => ({
license: {
license: {
// @ts-expect-error: just for testing
grantedModules: [{ module: 'auditing' }],
},
// @ts-expect-error: just for testing
activeModules: ['auditing'],
},
}))
.withJohnDoe()
.withPermission('can-audit-log')
.build(),
Expand Down
17 changes: 7 additions & 10 deletions apps/meteor/ee/client/hooks/useHasLicenseModule.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import type { LicenseModule } from '@rocket.chat/license';
import { useMethod, useUserId } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';

export const useHasLicenseModule = (licenseName: LicenseModule): 'loading' | boolean => {
const method = useMethod('license:getModules');
const uid = useUserId();

const features = useQuery(['ee.features'], method, {
enabled: !!uid,
});
import { useLicenseBase } from '../../../client/hooks/useLicense';

return features.data?.includes(licenseName) ?? 'loading';
export const useHasLicenseModule = (licenseName: LicenseModule): 'loading' | boolean => {
return (
useLicenseBase({
select: (data) => data.license.activeModules.includes(licenseName),
}).data ?? 'loading'
);
};
23 changes: 10 additions & 13 deletions apps/meteor/ee/server/startup/engagementDashboard.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { License } from '@rocket.chat/license';
import { Meteor } from 'meteor/meteor';

License.onToggledFeature('engagement-dashboard', {
up: () =>
Meteor.startup(async () => {
const { prepareAnalytics, attachCallbacks } = await import('../lib/engagementDashboard/startup');
await prepareAnalytics();
attachCallbacks();
await import('../api/engagementDashboard');
}),
down: () =>
Meteor.startup(async () => {
const { detachCallbacks } = await import('../lib/engagementDashboard/startup');
detachCallbacks();
}),
up: async () => {
const { prepareAnalytics, attachCallbacks } = await import('../lib/engagementDashboard/startup');
await prepareAnalytics();
attachCallbacks();
await import('../api/engagementDashboard');
},
down: async () => {
const { detachCallbacks } = await import('../lib/engagementDashboard/startup');
detachCallbacks();
},
});
2 changes: 1 addition & 1 deletion apps/meteor/server/models/raw/Analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class AnalyticsRaw extends BaseRaw<IAnalytic> implements IAnalyticsModel
}

protected modelIndexes(): IndexDescription[] {
return [{ key: { date: 1 } }, { key: { 'room._id': 1, 'date': 1 }, unique: true }];
return [{ key: { date: 1 } }, { key: { 'room._id': 1, 'date': 1 }, unique: true, partialFilterExpression: { type: 'rooms' } }];
}

saveMessageSent({ room, date }: { room: IRoom; date: IAnalytic['date'] }): Promise<Document | UpdateResult> {
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/server/startup/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ import './v299';
import './v300';
import './v301';
import './v303';
import './v304';
import './xrun';
11 changes: 11 additions & 0 deletions apps/meteor/server/startup/migrations/v304.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Analytics } from '@rocket.chat/models';

import { addMigration } from '../../lib/migrations';

addMigration({
version: 304,
name: 'Drop wrong index from analytics collection',
async up() {
await Analytics.col.dropIndex('room._id_1_date_1');
},
});
12 changes: 10 additions & 2 deletions ee/packages/license/src/events/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,26 @@ import { logger } from '../logger';
export function moduleValidated(this: LicenseManager, module: LicenseModule) {
try {
this.emit('module', { module, valid: true });
} catch (error) {
logger.error({ msg: `Error running module (valid: true) event: ${module}`, error });
}
try {
this.emit(`valid:${module}`);
} catch (error) {
logger.error({ msg: 'Error running module added event', error });
logger.error({ msg: `Error running module added event: ${module}`, error });
}
}

export function moduleRemoved(this: LicenseManager, module: LicenseModule) {
try {
this.emit('module', { module, valid: false });
} catch (error) {
logger.error({ msg: `Error running module (valid: false) event: ${module}`, error });
}
try {
this.emit(`invalid:${module}`);
} catch (error) {
logger.error({ msg: 'Error running module removed event', error });
logger.error({ msg: `Error running module removed event: ${module}`, error });
}
}

Expand Down
1 change: 1 addition & 0 deletions ee/packages/license/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { getTags } from './tags';
import { getCurrentValueForLicenseLimit, setLicenseLimitCounter } from './validation/getCurrentValueForLicenseLimit';
import { validateFormat } from './validation/validateFormat';

export { DuplicatedLicenseError } from './errors/DuplicatedLicenseError';
export * from './definition/ILicenseTag';
export * from './definition/ILicenseV2';
export * from './definition/ILicenseV3';
Expand Down
Loading