fix: load origins from settings

This commit is contained in:
Mohamed Marrouchi 2025-01-23 16:31:49 +01:00
parent d9ef2152b7
commit 2694a4f802
5 changed files with 70 additions and 27 deletions

24
api/src/app.instance.ts Normal file
View File

@ -0,0 +1,24 @@
/*
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { INestApplication } from '@nestjs/common';
export class AppInstance {
private static app: INestApplication;
static setApp(app: INestApplication) {
this.app = app;
}
static getApp(): INestApplication {
if (!this.app) {
throw new Error('App instance has not been set yet.');
}
return this.app;
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Hexastack. All rights reserved. * Copyright © 2025 Hexastack. All rights reserved.
* *
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
@ -18,6 +18,7 @@ moduleAlias.addAliases({
'@': __dirname, '@': __dirname,
}); });
import { AppInstance } from './app.instance';
import { HexabotModule } from './app.module'; import { HexabotModule } from './app.module';
import { config } from './config'; import { config } from './config';
import { LoggerService } from './logger/logger.service'; import { LoggerService } from './logger/logger.service';
@ -36,6 +37,9 @@ async function bootstrap() {
bodyParser: false, bodyParser: false,
}); });
// Set the global app instance
AppInstance.setApp(app);
const rawBodyBuffer = (req, res, buf, encoding) => { const rawBodyBuffer = (req, res, buf, encoding) => {
if (buf?.length) { if (buf?.length) {
req.rawBody = buf.toString(encoding || 'utf8'); req.rawBody = buf.toString(encoding || 'utf8');
@ -45,10 +49,10 @@ async function bootstrap() {
app.use(bodyParser.json({ verify: rawBodyBuffer })); app.use(bodyParser.json({ verify: rawBodyBuffer }));
const settingService = app.get<SettingService>(SettingService); const settingService = app.get<SettingService>(SettingService);
const allowedDomains = await settingService.getAllowedDomains(); const allowedOrigins = await settingService.getAllowedOrigins();
app.enableCors({ app.enableCors({
origin: (origin, callback) => { origin: (origin, callback) => {
if (!origin || allowedDomains.has(origin)) { if (!origin || allowedOrigins.has(origin)) {
callback(null, true); callback(null, true);
} else { } else {
callback(new Error('Not allowed by CORS')); callback(new Error('Not allowed by CORS'));

View File

@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Hexastack. All rights reserved. * Copyright © 2025 Hexastack. All rights reserved.
* *
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
@ -15,7 +15,7 @@ import { config } from '@/config';
import { Config } from '@/config/types'; import { Config } from '@/config/types';
import { LoggerService } from '@/logger/logger.service'; import { LoggerService } from '@/logger/logger.service';
import { import {
ALLOWED_DOMAINS_CACHE_KEY, ALLOWED_ORIGINS_CACHE_KEY,
SETTING_CACHE_KEY, SETTING_CACHE_KEY,
} from '@/utils/constants/cache'; } from '@/utils/constants/cache';
import { Cacheable } from '@/utils/decorators/cacheable.decorator'; import { Cacheable } from '@/utils/decorators/cacheable.decorator';
@ -113,7 +113,7 @@ export class SettingService extends BaseService<Setting> {
*/ */
async clearCache() { async clearCache() {
this.cacheManager.del(SETTING_CACHE_KEY); this.cacheManager.del(SETTING_CACHE_KEY);
this.cacheManager.del(ALLOWED_DOMAINS_CACHE_KEY); this.cacheManager.del(ALLOWED_ORIGINS_CACHE_KEY);
} }
/** /**
@ -126,20 +126,24 @@ export class SettingService extends BaseService<Setting> {
} }
/** /**
* Retrieves allowed_domains from the cache if available, or loads them from the * Retrieves a set of unique allowed origins for CORS configuration.
* repository and caches the result.
* *
* @returns A promise that resolves to a Set of`allowed_domains` string. * This method combines all `allowed_domains` settings,
* splits their values (comma-separated), and removes duplicates to produce a
* whitelist of origins. The result is cached for better performance using the
* `Cacheable` decorator with the key `ALLOWED_ORIGINS_CACHE_KEY`.
*
* @returns A promise that resolves to a set of allowed origins
*/ */
@Cacheable(ALLOWED_DOMAINS_CACHE_KEY) @Cacheable(ALLOWED_ORIGINS_CACHE_KEY)
async getAllowedDomains() { async getAllowedOrigins() {
// combines all allowed_doamins and whitelist them for cors
const settings = await this.find({ label: 'allowed_domains' }); const settings = await this.find({ label: 'allowed_domains' });
const whiteListedOrigins = new Set( const uniqueOrigins = new Set(
settings.flatMap((setting) => setting.value.split(',')), settings.flatMap((setting) => setting.value.split(',')),
); );
return whiteListedOrigins;
return uniqueOrigins;
} }
/** /**

View File

@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Hexastack. All rights reserved. * Copyright © 2025 Hexastack. All rights reserved.
* *
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
@ -17,4 +17,4 @@ export const LANGUAGES_CACHE_KEY = 'languages';
export const DEFAULT_LANGUAGE_CACHE_KEY = 'default_language'; export const DEFAULT_LANGUAGE_CACHE_KEY = 'default_language';
export const ALLOWED_DOMAINS_CACHE_KEY = 'allowed-domains'; export const ALLOWED_ORIGINS_CACHE_KEY = 'allowed_origins';

View File

@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Hexastack. All rights reserved. * Copyright © 2025 Hexastack. All rights reserved.
* *
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
@ -10,7 +10,9 @@ import util from 'util';
import type { ServerOptions } from 'socket.io'; import type { ServerOptions } from 'socket.io';
import { AppInstance } from '@/app.instance';
import { config } from '@/config'; import { config } from '@/config';
import { SettingService } from '@/setting/services/setting.service';
export const buildWebSocketGatewayOptions = (): Partial<ServerOptions> => { export const buildWebSocketGatewayOptions = (): Partial<ServerOptions> => {
const opts: Partial<ServerOptions> = { const opts: Partial<ServerOptions> = {
@ -53,7 +55,14 @@ export const buildWebSocketGatewayOptions = (): Partial<ServerOptions> => {
...(config.sockets.onlyAllowOrigins && { ...(config.sockets.onlyAllowOrigins && {
cors: { cors: {
origin: (origin, cb) => { origin: (origin, cb) => {
if (origin && config.sockets.onlyAllowOrigins.includes(origin)) { // Retrieve the allowed origins from the settings
const app = AppInstance.getApp();
const settingService = app.get<SettingService>(SettingService);
settingService
.getAllowedOrigins()
.then((allowedOrigins) => {
if (origin && allowedOrigins.has(origin)) {
cb(null, true); cb(null, true);
} else { } else {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -63,6 +72,8 @@ export const buildWebSocketGatewayOptions = (): Partial<ServerOptions> => {
); );
cb(new Error('Origin not allowed'), false); cb(new Error('Origin not allowed'), false);
} }
})
.catch(cb);
}, },
}, },
}), }),