diff --git a/apps/meteor/ee/server/startup/federation.ts b/apps/meteor/ee/server/startup/federation.ts new file mode 100644 index 0000000000000..3a9d8b196cdc4 --- /dev/null +++ b/apps/meteor/ee/server/startup/federation.ts @@ -0,0 +1,78 @@ +import { api } from '@rocket.chat/core-services'; +import { FederationMatrix } from '@rocket.chat/federation-matrix'; +import { License } from '@rocket.chat/license'; +import { Logger } from '@rocket.chat/logger'; + +import { settings } from '../../../app/settings/server'; +import { registerFederationRoutes } from '../api/federation'; + +const logger = new Logger('Federation'); + +export const startFederationService = async (): Promise => { + let federationMatrixService: FederationMatrix | undefined; + + const shouldStartService = (): boolean => { + const hasLicense = License.hasModule('federation'); + const isEnabled = settings.get('Federation_Service_Enabled') === true; + return hasLicense && isEnabled; + }; + + const startService = async (): Promise => { + if (federationMatrixService) { + logger.debug('Federation-matrix service already started... skipping'); + return; + } + + logger.debug('Starting federation-matrix service'); + federationMatrixService = await FederationMatrix.create(); + + try { + api.registerService(federationMatrixService); + await registerFederationRoutes(federationMatrixService); + } catch (error) { + logger.error('Failed to start federation-matrix service:', error); + } + }; + + const stopService = async (): Promise => { + if (!federationMatrixService) { + logger.debug('Federation-matrix service not registered... skipping'); + return; + } + + logger.debug('Stopping federation-matrix service'); + + // TODO: Unregister routes + // await unregisterFederationRoutes(federationMatrixService); + + await api.destroyService(federationMatrixService); + federationMatrixService = undefined; + }; + + if (shouldStartService()) { + await startService(); + } + + void License.onLicense('federation', async () => { + logger.debug('Federation license became available'); + if (shouldStartService()) { + await startService(); + } + }); + + License.onInvalidateLicense(async () => { + logger.debug('License invalidated, checking federation module'); + if (!shouldStartService()) { + await stopService(); + } + }); + + settings.watch('Federation_Service_Enabled', async (enabled) => { + logger.debug('Federation_Service_Enabled setting changed:', enabled); + if (shouldStartService()) { + await startService(); + } else { + await stopService(); + } + }); +}; diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index 3c4f0120e17c7..8707cd409c38d 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -30,7 +30,6 @@ import { UploadService } from './upload/service'; import { UserService } from './user/service'; import { VideoConfService } from './video-conference/service'; import { VoipAsteriskService } from './voip-asterisk/service'; -import { registerFederationRoutes } from '../../ee/server/api/federation'; import { i18n } from '../lib/i18n'; export const registerServices = async (): Promise => { @@ -72,12 +71,6 @@ export const registerServices = async (): Promise => { api.registerService(new Presence()); api.registerService(new Authorization()); - // TODO: Add it to a proper place since it's EE only - const { FederationMatrix } = await import('@rocket.chat/federation-matrix'); - const federationMatrix = await FederationMatrix.create(); - api.registerService(federationMatrix); - await registerFederationRoutes(federationMatrix); - // Run EE services defined outside of the main repo // Otherwise, monolith would ignore them :( // Always register the service and manage licensing inside the service (tbd) diff --git a/apps/meteor/startRocketChat.ts b/apps/meteor/startRocketChat.ts index f0ba6f3899b14..f2579be32b736 100644 --- a/apps/meteor/startRocketChat.ts +++ b/apps/meteor/startRocketChat.ts @@ -1,5 +1,6 @@ import { startLicense } from './ee/app/license/server/startup'; import { registerEEBroker } from './ee/server'; +import { startFederationService as startFederationMatrixService } from './ee/server/startup/federation'; import { startFederationService } from './ee/server/startup/services'; const loadBeforeLicense = async () => { @@ -8,6 +9,7 @@ const loadBeforeLicense = async () => { const loadAfterLicense = async () => { await startFederationService(); + await startFederationMatrixService(); }; export const startRocketChat = async () => { diff --git a/ee/apps/federation-service/package.json b/ee/apps/federation-service/package.json index 411b00d1784a8..ba32bc0a0ab59 100644 --- a/ee/apps/federation-service/package.json +++ b/ee/apps/federation-service/package.json @@ -27,7 +27,9 @@ "@rocket.chat/emitter": "^0.31.25", "@rocket.chat/federation-matrix": "workspace:^", "@rocket.chat/http-router": "workspace:*", + "@rocket.chat/license": "workspace:^", "@rocket.chat/models": "workspace:*", + "@rocket.chat/network-broker": "workspace:^", "hono": "^3.11.0", "pino": "^8.16.0", "polka": "^0.5.2", diff --git a/ee/apps/federation-service/src/service.ts b/ee/apps/federation-service/src/service.ts index 46e69b868dff9..1c4ee6a9e0f0f 100644 --- a/ee/apps/federation-service/src/service.ts +++ b/ee/apps/federation-service/src/service.ts @@ -1,6 +1,7 @@ import 'reflect-metadata'; import { serve } from '@hono/node-server'; -import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection, Settings } from '@rocket.chat/core-services'; +import { License } from '@rocket.chat/license'; import { registerServiceModels } from '@rocket.chat/models'; import { startBroker } from '@rocket.chat/network-broker'; import { Hono } from 'hono'; @@ -10,10 +11,19 @@ import { config } from './config'; function handleHealthCheck(app: Hono) { app.get('/health', async (c) => { try { - return c.json({ status: 'ok' }); + const hasLicense = await License.hasModule('federation'); + const isEnabled = await Settings.get('Federation_Service_Enabled'); + + return c.json({ + status: 'ok', + license: hasLicense ? 'valid' : 'invalid', + settings: { + federation_enabled: isEnabled, + }, + }); } catch (err) { console.error('Service not healthy', err); - return c.json({ status: 'not healthy' }, 500); + return c.json({ status: 'not healthy', error: (err as Error).message }, 500); } }); } @@ -26,6 +36,18 @@ function handleHealthCheck(app: Hono) { api.setBroker(startBroker()); + await api.start(); + + const hasLicense = License.hasModule('federation'); + if (!hasLicense) { + throw new Error('Service requires a valid Enterprise license with the federation module'); + } + + const isEnabled = await Settings.get('Federation_Service_Enabled'); + if (!isEnabled) { + throw new Error('Service is disabled in settings (Federation_Service_Enabled = false)'); + } + const { FederationMatrix } = await import('@rocket.chat/federation-matrix'); const federationMatrix = await FederationMatrix.create(); api.registerService(federationMatrix); @@ -35,13 +57,14 @@ function handleHealthCheck(app: Hono) { app.mount('/_matrix', matrix.getHonoRouter().fetch); app.mount('/.well-known', wellKnown.getHonoRouter().fetch); - + handleHealthCheck(app); serve({ fetch: app.fetch, port: config.port, }); - - await api.start(); -})(); +})().catch((error) => { + console.error('Failed to start service:', error); + process.exit(1); +}); diff --git a/packages/core-services/src/LocalBroker.ts b/packages/core-services/src/LocalBroker.ts index be19791097658..2c1692d6bd4c4 100644 --- a/packages/core-services/src/LocalBroker.ts +++ b/packages/core-services/src/LocalBroker.ts @@ -71,6 +71,8 @@ export class LocalBroker implements IBroker { } instance.removeAllListeners(); await instance.stopped(); + + this.services.delete(namespace); } /** diff --git a/yarn.lock b/yarn.lock index 1bacca961c348..5597f3e60829e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9084,7 +9084,9 @@ __metadata: "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/federation-matrix": "workspace:^" "@rocket.chat/http-router": "workspace:*" + "@rocket.chat/license": "workspace:^" "@rocket.chat/models": "workspace:*" + "@rocket.chat/network-broker": "workspace:^" "@types/bun": "npm:latest" "@types/express": "npm:^4.17.17" hono: "npm:^3.11.0"