diff --git a/.env.example b/.env.example index 1f43bfd453..5819856e72 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ COMPOSE_PROJECT_NAME=ghostfolio REDIS_HOST=redis REDIS_PORT=6379 REDIS_PASSWORD= +REDIS_TLS=false # POSTGRES POSTGRES_DB=ghostfolio-db diff --git a/README.md b/README.md index 88f6108340..25221c978f 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ Find answers to commonly asked questions about self-hosting Ghostfolio in our [F | `REDIS_HOST` | `string` | | The host where _Redis_ is running | | `REDIS_PASSWORD` | `string` | | The password of _Redis_ | | `REDIS_PORT` | `number` | | The port where _Redis_ is running | +| `REDIS_TLS` | `boolean` (optional) | `false` | Enables TLS/SSL connection to _Redis_ | | `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds | | `ROOT_URL` | `string` (optional) | `http://0.0.0.0:3333` | The root URL of the Ghostfolio application, used for generating callback URLs and external links. | | `TRUST_PROXY` | `string` (optional) | | The [trust proxy](https://expressjs.com/en/guide/behind-proxies.html) setting of _Express.js_ to determine the client IP address for rate limiting, e.g. `1` if the Ghostfolio application runs behind a single reverse proxy | diff --git a/apps/api/src/helper/redis.helper.spec.ts b/apps/api/src/helper/redis.helper.spec.ts new file mode 100644 index 0000000000..d72e0e2b37 --- /dev/null +++ b/apps/api/src/helper/redis.helper.spec.ts @@ -0,0 +1,91 @@ +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import type { Environment } from '@ghostfolio/api/services/interfaces/environment.interface'; + +import { + getRedisConnectionOptions, + getRedisConnectionUrl +} from './redis.helper'; + +function createConfigurationService(overrides: Partial = {}) { + const configuration: Record = { + REDIS_DB: 0, + REDIS_HOST: 'localhost', + REDIS_PASSWORD: '***', + REDIS_PORT: 6379, + REDIS_TLS: false, + ...overrides + }; + + return { + get: jest.fn((key: string) => configuration[key]) + } as unknown as ConfigurationService; +} + +describe('redis.helper', () => { + describe('getRedisConnectionOptions', () => { + it('should return connection options without TLS when REDIS_TLS is false', () => { + const configurationService = createConfigurationService({ + REDIS_TLS: false + }); + + const options = getRedisConnectionOptions(configurationService); + + expect(options).toEqual({ + db: 0, + host: 'localhost', + password: '***', + port: 6379, + tls: undefined + }); + }); + + it('should return connection options with tls object when REDIS_TLS is true', () => { + const configurationService = createConfigurationService({ + REDIS_TLS: true + }); + + const options = getRedisConnectionOptions(configurationService); + + expect(options).toEqual({ + db: 0, + host: 'localhost', + password: '***', + port: 6379, + tls: {} + }); + }); + }); + + describe('getRedisConnectionUrl', () => { + it('should return standard redis:// URL when REDIS_TLS is false', () => { + const configurationService = createConfigurationService({ + REDIS_TLS: false + }); + + const url = getRedisConnectionUrl(configurationService); + + expect(url).toBe('redis://:***@localhost:6379/0'); + }); + + it('should return rediss:// URL when REDIS_TLS is true', () => { + const configurationService = createConfigurationService({ + REDIS_TLS: true + }); + + const url = getRedisConnectionUrl(configurationService); + + expect(url).toBe('rediss://:***@localhost:6379/0'); + }); + + it('should format URL correctly without password', () => { + const configurationService = createConfigurationService({ + REDIS_PASSWORD: '', + REDIS_TLS: false + }); + + const url = getRedisConnectionUrl(configurationService); + + expect(url).toBe('redis://@localhost:6379/0'); + }); + }); +}); diff --git a/apps/api/src/helper/redis.helper.ts b/apps/api/src/helper/redis.helper.ts index 81fe905eb5..b07677c41c 100644 --- a/apps/api/src/helper/redis.helper.ts +++ b/apps/api/src/helper/redis.helper.ts @@ -7,16 +7,17 @@ export function getRedisConnectionOptions( db: configurationService.get('REDIS_DB'), host: configurationService.get('REDIS_HOST'), password: configurationService.get('REDIS_PASSWORD'), - port: configurationService.get('REDIS_PORT') + port: configurationService.get('REDIS_PORT'), + tls: configurationService.get('REDIS_TLS') ? {} : undefined }; } export function getRedisConnectionUrl( configurationService: ConfigurationService ): string { - const { db, host, password, port } = + const { db, host, password, port, tls } = getRedisConnectionOptions(configurationService); const encodedPassword = encodeURIComponent(password); - return `redis://${encodedPassword ? `:${encodedPassword}` : ''}@${host}:${port}/${db}`; + return `${tls ? 'rediss' : 'redis'}://${encodedPassword ? `:${encodedPassword}` : ''}@${host}:${port}/${db}`; } diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index 67c7ae5393..bbd1a319b6 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -138,6 +138,7 @@ export class ConfigurationService { REDIS_HOST: str({ default: 'localhost' }), REDIS_PASSWORD: str({ default: '' }), REDIS_PORT: port({ default: 6379 }), + REDIS_TLS: bool({ default: false }), REQUEST_TIMEOUT: num({ default: ms('3 seconds') }), ROOT_URL: url({ default: environment.rootUrl diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index f0a7f39f3f..7f9d7813cc 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -57,6 +57,7 @@ export interface Environment extends CleanedEnvAccessors { REDIS_HOST: string; REDIS_PASSWORD: string; REDIS_PORT: number; + REDIS_TLS: boolean; REQUEST_TIMEOUT: number; ROOT_URL: string; STRIPE_SECRET_KEY: string;