From 2025026663a8068d6433b1cc5be523a3878f6dfd Mon Sep 17 00:00:00 2001 From: kim Date: Fri, 6 Jun 2025 11:42:24 +0000 Subject: [PATCH 1/5] feat: remove expiration time from env, use redis connection string --- .github/workflows/deploy-prod.yml | 6 ++-- .github/workflows/deploy-stage.yml | 6 ++-- README.md | 4 --- src/app.ts | 10 ++----- src/di/container.ts | 15 ++-------- src/jobs.ts | 13 ++------ src/services/websockets/multi-instance.ts | 13 +++++--- .../websockets/websocket.controller.ts | 1 + src/utils/config.ts | 30 ++++++++----------- 9 files changed, 33 insertions(+), 65 deletions(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 4a54d5345a..1fa8552d75 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -97,8 +97,7 @@ jobs: environment-variables: | DB_CONNECTION_POOL_SIZE=${{ vars.DB_CONNECTION_POOL_SIZE }} APPS_JWT_SECRET=${{ secrets.APPS_JWT_SECRET }} - APPS_PUBLISHER_ID=${{ secrets.APPS_PUBLISHER_ID }} - AUTH_TOKEN_EXPIRATION_IN_MINUTES=${{ vars.AUTH_TOKEN_EXPIRATION_IN_MINUTES }} + APPS_PUBLISHER_ID=${{ secrets.APPS_PUBLISHER_ID }} AUTH_TOKEN_JWT_SECRET=${{ secrets.AUTH_TOKEN_JWT_SECRET }} CLIENT_HOST=${{ vars.CLIENT_HOST }} COOKIE_DOMAIN=${{ vars.COOKIE_DOMAIN }} @@ -139,8 +138,7 @@ jobs: REDIS_HOST=${{ secrets.REDIS_HOST }} REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} REDIS_PORT=${{ secrets.REDIS_PORT }} - REDIS_USERNAME=${{ secrets.REDIS_USERNAME }} - REFRESH_TOKEN_EXPIRATION_IN_MINUTES=${{ vars.REFRESH_TOKEN_EXPIRATION_IN_MINUTES }} + REDIS_USERNAME=${{ secrets.REDIS_USERNAME }} REFRESH_TOKEN_JWT_SECRET=${{ secrets.REFRESH_TOKEN_JWT_SECRET }} S3_FILE_ITEM_ACCESS_KEY_ID=${{ secrets.S3_FILE_ITEM_ACCESS_KEY_ID }} S3_FILE_ITEM_BUCKET=${{ vars.S3_FILE_ITEM_BUCKET }} diff --git a/.github/workflows/deploy-stage.yml b/.github/workflows/deploy-stage.yml index 4c9d7fed38..6aaf9b50cb 100644 --- a/.github/workflows/deploy-stage.yml +++ b/.github/workflows/deploy-stage.yml @@ -97,8 +97,7 @@ jobs: environment-variables: | DB_CONNECTION_POOL_SIZE=${{ vars.DB_CONNECTION_POOL_SIZE }} APPS_JWT_SECRET=${{ secrets.APPS_JWT_SECRET }} - APPS_PUBLISHER_ID=${{ secrets.APPS_PUBLISHER_ID }} - AUTH_TOKEN_EXPIRATION_IN_MINUTES=${{ secrets.AUTH_TOKEN_EXPIRATION_IN_MINUTES }} + APPS_PUBLISHER_ID=${{ secrets.APPS_PUBLISHER_ID }} AUTH_TOKEN_JWT_SECRET=${{ secrets.AUTH_TOKEN_JWT_SECRET }} CLIENT_HOST=${{ vars.CLIENT_HOST }} COOKIE_DOMAIN=${{ vars.COOKIE_DOMAIN }} @@ -140,8 +139,7 @@ jobs: REDIS_HOST=${{ secrets.REDIS_HOST }} REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} REDIS_PORT=${{ secrets.REDIS_PORT }} - REDIS_USERNAME=${{ secrets.REDIS_USERNAME }} - REFRESH_TOKEN_EXPIRATION_IN_MINUTES=${{ vars.REFRESH_TOKEN_EXPIRATION_IN_MINUTES }} + REDIS_USERNAME=${{ secrets.REDIS_USERNAME }} REFRESH_TOKEN_JWT_SECRET=${{ secrets.REFRESH_TOKEN_JWT_SECRET }} S3_FILE_ITEM_ACCESS_KEY_ID=${{ secrets.S3_FILE_ITEM_ACCESS_KEY_ID }} S3_FILE_ITEM_BUCKET=${{ vars.S3_FILE_ITEM_BUCKET }} diff --git a/README.md b/README.md index cfc842c64b..53f741d2f5 100644 --- a/README.md +++ b/README.md @@ -132,16 +132,12 @@ SECURE_SESSION_SECRET_KEY= JWT_SECRET= # Auth JWT secret (can use the same command as for SECURE_SESSION_SECRET_KEY) AUTH_TOKEN_JWT_SECRET= -# AUTH_TOKEN_EXPIRATION_IN_MINUTES=10080 # Refresh JWT secret (can use the same command as for SECURE_SESSION_SECRET_KEY) REFRESH_TOKEN_JWT_SECRET= -# REFRESH_TOKEN_EXPIRATION_IN_MINUTES=86400 # Password reset JWT secret (can use the same command as for SECURE_SESSION_SECRET_KEY) PASSWORD_RESET_JWT_SECRET= -# PASSWORD_RESET_JWT_EXPIRATION_IN_MINUTES=1440 # Email change JWT secret (can use the same command as for SECURE_SESSION_SECRET_KEY) EMAIL_CHANGE_JWT_SECRET= -# EMAIL_CHANGE_JWT_EXPIRATION_IN_MINUTES=1440 ### Mail server configuration diff --git a/src/app.ts b/src/app.ts index 9d8f639bf0..c4c0eca89e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -17,7 +17,7 @@ import { maintenancePlugin } from './services/maintenance/maintenance.controller import MemberServiceApi from './services/member'; import tagPlugin from './services/tag/tag.controller'; import websocketsPlugin from './services/websockets/websocket.controller'; -import { REDIS_HOST, REDIS_PASSWORD, REDIS_PORT, REDIS_USERNAME } from './utils/config'; +import { REDIS_CONNECTION } from './utils/config'; export default async function (instance: FastifyInstance): Promise { await instance @@ -48,12 +48,8 @@ export default async function (instance: FastifyInstance): Promise { prefix: '/ws', redis: { channelName: 'graasp-realtime-updates', - config: { - host: REDIS_HOST, - port: REDIS_PORT, - username: REDIS_USERNAME, - password: REDIS_PASSWORD, - }, + connectionString: REDIS_CONNECTION, + config: {}, }, }) .register(fp(MemberServiceApi)) diff --git a/src/di/container.ts b/src/di/container.ts index fc0eba1eba..9661c0d700 100644 --- a/src/di/container.ts +++ b/src/di/container.ts @@ -29,10 +29,7 @@ import { MAILER_CONFIG_USERNAME, MEILISEARCH_MASTER_KEY, MEILISEARCH_URL, - REDIS_HOST, - REDIS_PASSWORD, - REDIS_PORT, - REDIS_USERNAME, + REDIS_CONNECTION, S3_FILE_ITEM_PLUGIN_OPTIONS, } from '../utils/config'; import { @@ -64,15 +61,7 @@ export const registerDependencies = (instance: FastifyInstance) => { // register geolocation key for the ItemGeolocationService. registerValue(GEOLOCATION_API_KEY_DI_KEY, GEOLOCATION_API_KEY); - registerValue( - Redis, - new Redis({ - host: REDIS_HOST, - port: REDIS_PORT, - username: REDIS_USERNAME, - password: REDIS_PASSWORD, - }), - ); + registerValue(Redis, new Redis(REDIS_CONNECTION)); // Register CachingService for the thumbnails urls. registerValue( diff --git a/src/jobs.ts b/src/jobs.ts index bef06755e7..34fee216d5 100644 --- a/src/jobs.ts +++ b/src/jobs.ts @@ -1,19 +1,10 @@ import { ConnectionOptions, Queue, Worker } from 'bullmq'; import { BaseLogger } from './logger'; -import { - JOB_SCHEDULING, - REDIS_HOST, - REDIS_PASSWORD, - REDIS_PORT, - REDIS_USERNAME, -} from './utils/config'; +import { JOB_SCHEDULING, REDIS_CONNECTION } from './utils/config'; const connection: ConnectionOptions = { - host: REDIS_HOST, - port: REDIS_PORT, - username: REDIS_USERNAME, - password: REDIS_PASSWORD, + url: REDIS_CONNECTION, }; export const CRON_3AM_MONDAY = '0 3 * * 1'; diff --git a/src/services/websockets/multi-instance.ts b/src/services/websockets/multi-instance.ts index c71a271a3d..e0d72e2459 100644 --- a/src/services/websockets/multi-instance.ts +++ b/src/services/websockets/multi-instance.ts @@ -67,8 +67,12 @@ const redisSerdes = { }; // Helper to create a redis client instance -function createRedisClientInstance(redisConfig: RedisOptions, log?: FastifyBaseLogger): Redis { - const redis = new Redis(redisConfig); +function createRedisClientInstance( + redisConnection: string, + redisConfig: RedisOptions, + log?: FastifyBaseLogger, +): Redis { + const redis = new Redis(redisConnection, redisConfig); redis.on('error', (err) => { log?.error( @@ -96,6 +100,7 @@ class MultiInstanceChannelsBroker { constructor( wsChannels: WebSocketChannels, redisParams: { + connectionString: string; config: RedisOptions; channelName: string; }, @@ -103,8 +108,8 @@ class MultiInstanceChannelsBroker { ) { this.wsChannels = wsChannels; this.notifChannel = redisParams.channelName; - this.sub = createRedisClientInstance(redisParams.config, log); - this.pub = createRedisClientInstance(redisParams.config, log); + this.sub = createRedisClientInstance(redisParams.connectionString, redisParams.config, log); + this.pub = createRedisClientInstance(redisParams.connectionString, redisParams.config, log); this.sub.subscribe(this.notifChannel, (err, _result) => { if (err) { diff --git a/src/services/websockets/websocket.controller.ts b/src/services/websockets/websocket.controller.ts index 2852abb9ff..daa31331a4 100644 --- a/src/services/websockets/websocket.controller.ts +++ b/src/services/websockets/websocket.controller.ts @@ -26,6 +26,7 @@ export interface WebsocketsPluginOptions { redis: { config: RedisOptions; channelName: string; + connectionString: string; }; } diff --git a/src/utils/config.ts b/src/utils/config.ts index 5673c5a559..c5c2000e98 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -114,10 +114,8 @@ if (!process.env.SECURE_SESSION_SECRET_KEY) { throw new Error('SECURE_SESSION_SECRET_KEY is not defined'); } export const SECURE_SESSION_SECRET_KEY: string = process.env.SECURE_SESSION_SECRET_KEY!; -export const SECURE_SESSION_EXPIRATION_IN_SECONDS: number = - +process.env.SECURE_SESSION_EXPIRATION_IN_SECONDS! || 604800; // 7days -export const MAX_SECURE_SESSION_EXPIRATION_IN_SECONDS: number = - +process.env.MAX_SECURE_SESSION_EXPIRATION_IN_SECONDS! || 15552000; // 6 * 30days +export const SECURE_SESSION_EXPIRATION_IN_SECONDS = 604800; // 7days +export const MAX_SECURE_SESSION_EXPIRATION_IN_SECONDS = 15552000; // 6 * 30days /** * JWT */ @@ -140,13 +138,9 @@ if (!process.env.REFRESH_TOKEN_JWT_SECRET) { } export const REFRESH_TOKEN_JWT_SECRET = process.env.REFRESH_TOKEN_JWT_SECRET; /** Auth token expiration, in minutes */ -export const AUTH_TOKEN_EXPIRATION_IN_MINUTES = process.env.AUTH_TOKEN_EXPIRATION_IN_MINUTES - ? +process.env.AUTH_TOKEN_EXPIRATION_IN_MINUTES - : 10080; +export const AUTH_TOKEN_EXPIRATION_IN_MINUTES = 10080; /** Refresh token expiration, in minutes */ -export const REFRESH_TOKEN_EXPIRATION_IN_MINUTES = process.env.REFRESH_TOKEN_EXPIRATION_IN_MINUTES - ? +process.env.REFRESH_TOKEN_EXPIRATION_IN_MINUTES - : 86400; +export const REFRESH_TOKEN_EXPIRATION_IN_MINUTES = 86400; /** Password reset token Secret */ export const PASSWORD_RESET_JWT_SECRET: string = process.env.PASSWORD_RESET_JWT_SECRET!; @@ -154,8 +148,7 @@ if (!PASSWORD_RESET_JWT_SECRET) { throw new Error('PASSWORD_RESET_JWT_SECRET should be defined'); } /** Password reset token expiration, in minutes */ -export const PASSWORD_RESET_JWT_EXPIRATION_IN_MINUTES: number = - Number(process.env.PASSWORD_RESET_JWT_EXPIRATION_IN_MINUTES) || 1440; +export const PASSWORD_RESET_JWT_EXPIRATION_IN_MINUTES = 1440; /** Email change token Secret */ export const EMAIL_CHANGE_JWT_SECRET: string = asDefined( @@ -165,8 +158,7 @@ export const EMAIL_CHANGE_JWT_SECRET: string = asDefined( ); /** Email change token expiration, in minutes */ -export const EMAIL_CHANGE_JWT_EXPIRATION_IN_MINUTES: number = - Number(process.env.EMAIL_CHANGE_JWT_EXPIRATION_IN_MINUTES) || 1440; +export const EMAIL_CHANGE_JWT_EXPIRATION_IN_MINUTES = 1440; // Graasp mailer config if ( @@ -312,10 +304,12 @@ if (!process.env.APPS_JWT_SECRET) { export const APPS_JWT_SECRET = process.env.APPS_JWT_SECRET; // Graasp websockets -export const REDIS_HOST = process.env.REDIS_HOST; -export const REDIS_PORT: number = +process.env.REDIS_PORT! || 6379; -export const REDIS_PASSWORD = process.env.REDIS_PASSWORD; -export const REDIS_USERNAME = process.env.REDIS_USERNAME; +// export const REDIS_HOST = process.env.REDIS_HOST; +// export const REDIS_PORT: number = +process.env.REDIS_PORT! || 6379; +// export const REDIS_PASSWORD = process.env.REDIS_PASSWORD; +// export const REDIS_USERNAME = process.env.REDIS_USERNAME; +// redis[s]://[[username][:password]@][host][:port][/db-number]: +export const REDIS_CONNECTION = 'redis://redis:6379'; // validation export const IMAGE_CLASSIFIER_API = process.env.IMAGE_CLASSIFIER_API ?? ''; From ada85a0d2ce11c12dede09195ca21175034062ff Mon Sep 17 00:00:00 2001 From: kim Date: Fri, 6 Jun 2025 11:50:20 +0000 Subject: [PATCH 2/5] refactor: remove REDIS_PORT, etc and update workflows --- .devcontainer/docker-compose.yml | 4 ++-- .github/workflows/deploy-prod.yml | 5 +---- .github/workflows/deploy-stage.yml | 5 +---- .github/workflows/test.yml | 3 +-- README.md | 6 ++---- docker/compose.yml | 2 +- .../plugins/password/password.controller.test.ts | 12 ++---------- src/services/websockets/README.md | 5 +---- src/services/websockets/test/test-utils.ts | 7 +++---- src/utils/config.ts | 11 +++++------ 10 files changed, 19 insertions(+), 41 deletions(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 7373c056d6..4a17c12ed1 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -32,8 +32,8 @@ services: # Api key is set by ./etherpad/devApiKey.txt ETHERPAD_API_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef # the Redis config is set by the "redis" service below - REDIS_HOST: redis - REDIS_PORT: 6379 + REDIS_CONNECTION: redis://redis:6379 + # the Mailer config is set by the "mailer" service below MAILER_CONFIG_SMTP_HOST: mailer MAILER_CONFIG_SMTP_PORT: 1025 diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 1fa8552d75..c2ac394f95 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -135,10 +135,7 @@ jobs: PORT=${{ vars.PORT }} PUBLIC_URL=${{ secrets.PUBLIC_URL }} RECAPTCHA_SECRET_ACCESS_KEY=${{ secrets.RECAPTCHA_SECRET_ACCESS_KEY }} - REDIS_HOST=${{ secrets.REDIS_HOST }} - REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} - REDIS_PORT=${{ secrets.REDIS_PORT }} - REDIS_USERNAME=${{ secrets.REDIS_USERNAME }} + REDIS_CONNECTION=${{ secrets.REDIS_CONNECTION }} REFRESH_TOKEN_JWT_SECRET=${{ secrets.REFRESH_TOKEN_JWT_SECRET }} S3_FILE_ITEM_ACCESS_KEY_ID=${{ secrets.S3_FILE_ITEM_ACCESS_KEY_ID }} S3_FILE_ITEM_BUCKET=${{ vars.S3_FILE_ITEM_BUCKET }} diff --git a/.github/workflows/deploy-stage.yml b/.github/workflows/deploy-stage.yml index 6aaf9b50cb..87c2b94e6e 100644 --- a/.github/workflows/deploy-stage.yml +++ b/.github/workflows/deploy-stage.yml @@ -136,10 +136,7 @@ jobs: PORT=${{ vars.PORT }} PUBLIC_URL=${{ secrets.PUBLIC_URL }} RECAPTCHA_SECRET_ACCESS_KEY=${{ secrets.RECAPTCHA_SECRET_ACCESS_KEY }} - REDIS_HOST=${{ secrets.REDIS_HOST }} - REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} - REDIS_PORT=${{ secrets.REDIS_PORT }} - REDIS_USERNAME=${{ secrets.REDIS_USERNAME }} + REDIS_CONNECTION=${{ secrets.REDIS_CONNECTION }} REFRESH_TOKEN_JWT_SECRET=${{ secrets.REFRESH_TOKEN_JWT_SECRET }} S3_FILE_ITEM_ACCESS_KEY_ID=${{ secrets.S3_FILE_ITEM_ACCESS_KEY_ID }} S3_FILE_ITEM_BUCKET=${{ vars.S3_FILE_ITEM_BUCKET }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b721abee5..1e4f8c0e91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,8 +43,7 @@ env: MAILER_CONFIG_SMTP_HOST: localhost MAILER_CONFIG_USERNAME: username RECAPTCHA_SECRET_ACCESS_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef - REDIS_HOST: localhost - REDIS_PORT: 6379 + REDIS_CONNECTION=: redis://localhost:6379 REFRESH_TOKEN_JWT_SECRET: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef S3_FILE_ITEM_ACCESS_KEY_ID: graasp-user S3_FILE_ITEM_BUCKET: graasp diff --git a/README.md b/README.md index 53f741d2f5..58da3505f2 100644 --- a/README.md +++ b/README.md @@ -197,10 +197,8 @@ GRAASPER_CREATOR_ID= # Graasp websockets # Redis config set by ./.devcontainer/docker-compose.yml -# REDIS_HOST=redis -# REDIS_PORT=6379 -# REDIS_USERNAME= -# REDIS_PASSWORD= +# redis[s]://[[username][:password]@][host][:port][/db-number]: +# REDIS_CONNECTION= # Graasp Actions SAVE_ACTIONS=true diff --git a/docker/compose.yml b/docker/compose.yml index 906eba7c0b..e02a21eb8b 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -80,7 +80,7 @@ services: ETHERPAD_API_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef # Redis - REDIS_HOST: redis + REDIS_CONNECTION: redis://redis:6379 EMBEDDED_LINK_ITEM_IFRAMELY_HREF_ORIGIN: http://iframely:8061 diff --git a/src/services/auth/plugins/password/password.controller.test.ts b/src/services/auth/plugins/password/password.controller.test.ts index bf38206804..8f34b15fa6 100644 --- a/src/services/auth/plugins/password/password.controller.test.ts +++ b/src/services/auth/plugins/password/password.controller.test.ts @@ -28,10 +28,7 @@ import { MailerService } from '../../../../plugins/mailer/mailer.service'; import { assertIsDefined } from '../../../../utils/assertions'; import { PASSWORD_RESET_JWT_EXPIRATION_IN_MINUTES, - REDIS_HOST, - REDIS_PASSWORD, - REDIS_PORT, - REDIS_USERNAME, + REDIS_CONNECTION, } from '../../../../utils/config'; import { assertIsMember, assertIsMemberForTest } from '../../../authentication'; @@ -458,12 +455,7 @@ describe('Password', () => { // Overwrite the setex method to test the expiration jest.spyOn(Redis.prototype, 'setex').mockImplementationOnce((key, seconds, value) => { expect(seconds).toBe(PASSWORD_RESET_JWT_EXPIRATION_IN_MINUTES * 60); - const redis = new Redis({ - host: REDIS_HOST, - port: REDIS_PORT, - username: REDIS_USERNAME, - password: REDIS_PASSWORD, - }); + const redis = new Redis(REDIS_CONNECTION); return redis.setex(key, 1, value); }); diff --git a/src/services/websockets/README.md b/src/services/websockets/README.md index 5df6ea8828..1b2d98706a 100644 --- a/src/services/websockets/README.md +++ b/src/services/websockets/README.md @@ -44,11 +44,8 @@ The plugin accepts the following options (which all have sane defaults): await instance.register(graaspWebSockets, { prefix: '/ws', redis: { + connectionString: config: { - host: REDIS_HOST, - port: +REDIS_PORT, - username: REDIS_USERNAME, - password: REDIS_PASSWORD, ... // any other RedisOptions property from 'ioredis' } channelName: 'graasp-notif', diff --git a/src/services/websockets/test/test-utils.ts b/src/services/websockets/test/test-utils.ts index 6f61b51022..a9c1d53f26 100644 --- a/src/services/websockets/test/test-utils.ts +++ b/src/services/websockets/test/test-utils.ts @@ -10,7 +10,7 @@ import fp from 'fastify-plugin'; import { Websocket as GraaspWebsocket } from '@graasp/sdk'; -import { REDIS_HOST } from '../../../utils/config'; +import { REDIS_CONNECTION } from '../../../utils/config'; import { AjvMessageSerializer } from '../message-serializer'; import graaspWebSockets, { WebsocketsPluginOptions } from '../websocket.controller'; import { WebSocketChannels } from '../ws-channels'; @@ -40,9 +40,8 @@ export function createDefaultLocalConfig( port: options.port, prefix: options.prefix || '/ws', redis: options.redis || { - config: { - host: REDIS_HOST, - }, + connectionString: REDIS_CONNECTION, + config: {}, channelName: 'notifications', }, }; diff --git a/src/utils/config.ts b/src/utils/config.ts index c5c2000e98..9aa8befd2d 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -304,12 +304,11 @@ if (!process.env.APPS_JWT_SECRET) { export const APPS_JWT_SECRET = process.env.APPS_JWT_SECRET; // Graasp websockets -// export const REDIS_HOST = process.env.REDIS_HOST; -// export const REDIS_PORT: number = +process.env.REDIS_PORT! || 6379; -// export const REDIS_PASSWORD = process.env.REDIS_PASSWORD; -// export const REDIS_USERNAME = process.env.REDIS_USERNAME; -// redis[s]://[[username][:password]@][host][:port][/db-number]: -export const REDIS_CONNECTION = 'redis://redis:6379'; +if (!process.env.REDIS_CONNECTION) { + console.error('REDIS_CONNECTION environment variable missing.'); + process.exit(1); +} +export const REDIS_CONNECTION = process.env.REDIS_CONNECTION; // validation export const IMAGE_CLASSIFIER_API = process.env.IMAGE_CLASSIFIER_API ?? ''; From 611790df2c9ca12bb5d48b0908f2be598ccd19e6 Mon Sep 17 00:00:00 2001 From: kim Date: Fri, 6 Jun 2025 12:04:54 +0000 Subject: [PATCH 3/5] refactor: replace mailer config for connection string --- .devcontainer/docker-compose.yml | 8 ++------ .github/workflows/deploy-prod.yml | 4 +--- .github/workflows/deploy-stage.yml | 4 +--- .github/workflows/test.yml | 4 +--- README.md | 6 ++---- docker/compose.yml | 6 +----- src/di/container.ts | 20 ++------------------ src/plugins/mailer/mailer.service.ts | 18 +++--------------- src/utils/config.ts | 20 +++----------------- 9 files changed, 16 insertions(+), 74 deletions(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 4a17c12ed1..f33bef09b0 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -34,12 +34,8 @@ services: # the Redis config is set by the "redis" service below REDIS_CONNECTION: redis://redis:6379 - # the Mailer config is set by the "mailer" service below - MAILER_CONFIG_SMTP_HOST: mailer - MAILER_CONFIG_SMTP_PORT: 1025 - MAILER_CONFIG_SMTP_USE_SSL: 'false' # only for dev purposes - MAILER_CONFIG_USERNAME: docker - MAILER_CONFIG_PASSWORD: docker + # the Mailer config is set by the "mailer" service below + MAILER_CONNECTION: smtp://docker:docker@mailer:1025 # the Localstack config is set by the "localstack" service below S3_FILE_ITEM_HOST: http://localstack:4566 # the Iframely config is set by the "iframely" service below diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index c2ac394f95..cdbdf8397c 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -125,9 +125,7 @@ jobs: LIBRARY_CLIENT_HOST=${{ vars.LIBRARY_CLIENT_HOST }} LOG_LEVEL=${{ vars.LOG_LEVEL }} MAILER_CONFIG_FROM_EMAIL=${{ secrets.MAILER_CONFIG_FROM_EMAIL }} - MAILER_CONFIG_PASSWORD=${{ secrets.MAILER_CONFIG_PASSWORD }} - MAILER_CONFIG_SMTP_HOST=${{ secrets.MAILER_CONFIG_SMTP_HOST }} - MAILER_CONFIG_USERNAME=${{ secrets.MAILER_CONFIG_USERNAME }} + MAILER_CONNECTION=${{ secrets.MAILER_CONNECTION }} MEILISEARCH_MASTER_KEY=${{ secrets.MEILISEARCH_MASTER_KEY }} MEILISEARCH_REBUILD_SECRET=${{ secrets.MEILISEARCH_REBUILD_SECRET }} MEILISEARCH_URL=${{ secrets.MEILISEARCH_URL }} diff --git a/.github/workflows/deploy-stage.yml b/.github/workflows/deploy-stage.yml index 87c2b94e6e..e909cf794c 100644 --- a/.github/workflows/deploy-stage.yml +++ b/.github/workflows/deploy-stage.yml @@ -126,9 +126,7 @@ jobs: LIBRARY_CLIENT_HOST=${{ vars.LIBRARY_CLIENT_HOST }} LOG_LEVEL=${{ vars.LOG_LEVEL }} MAILER_CONFIG_FROM_EMAIL=${{ secrets.MAILER_CONFIG_FROM_EMAIL }} - MAILER_CONFIG_PASSWORD=${{ secrets.MAILER_CONFIG_PASSWORD }} - MAILER_CONFIG_SMTP_HOST=${{ secrets.MAILER_CONFIG_SMTP_HOST }} - MAILER_CONFIG_USERNAME=${{ secrets.MAILER_CONFIG_USERNAME }} + MAILER_CONNECTION=${{ secrets.MAILER_CONNECTION }} MEILISEARCH_MASTER_KEY=${{ secrets.MEILISEARCH_MASTER_KEY }} MEILISEARCH_REBUILD_SECRET=${{ secrets.MEILISEARCH_REBUILD_SECRET }} MEILISEARCH_URL=${{ secrets.MEILISEARCH_URL }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1e4f8c0e91..0517fd791c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,9 +39,7 @@ env: JWT_SECRET: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef PASSWORD_RESET_JWT_SECRET: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef EMAIL_CHANGE_JWT_SECRET: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef - MAILER_CONFIG_PASSWORD: password - MAILER_CONFIG_SMTP_HOST: localhost - MAILER_CONFIG_USERNAME: username + MAILER_CONNECTION: smtp://username:password@localhost:1025 RECAPTCHA_SECRET_ACCESS_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef REDIS_CONNECTION=: redis://localhost:6379 REFRESH_TOKEN_JWT_SECRET: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef diff --git a/README.md b/README.md index 58da3505f2..514b3ad2b4 100644 --- a/README.md +++ b/README.md @@ -144,9 +144,7 @@ EMAIL_CHANGE_JWT_SECRET= # Mailer config (set by ./.devcontainer/docker-compose.yml) # Set to random values if you don't want to use the mock mailbox at http://localhost:1080 -# MAILER_CONFIG_SMTP_HOST=mailer -# MAILER_CONFIG_USERNAME=graasp -# MAILER_CONFIG_PASSWORD=graasp +# MAILER_CONNECTION= ### File storages configuration @@ -244,7 +242,7 @@ You can also run `yarn seed` to feed the database with predefined mock data. ## Utilities -The development [docker-compose.yml](.devcontainer/docker-compose.yml) provides an instance of [mailcatcher](https://mailcatcher.me/), which emulates a SMTP server for sending e-mails. When using the email authentication flow, the mailbox web UI is accessible at [http://localhost:1080](http://localhost:1080). If you do not want to use mailcatcher, set the `MAILER_CONFIG_SMTP_HOST` variable in your `.env.development` to some random value (e.g. empty string). This will log the authentication links in the server console instead. +The development [docker-compose.yml](.devcontainer/docker-compose.yml) provides an instance of [mailcatcher](https://mailcatcher.me/), which emulates a SMTP server for sending e-mails. When using the email authentication flow, the mailbox web UI is accessible at [http://localhost:1080](http://localhost:1080). If you do not want to use mailcatcher, set the `MAILER_CONNECTION` variable in your `.env.development` to some random value (e.g. empty string). This will log the authentication links in the server console instead. The development [docker-compose.yml](.devcontainer/docker-compose.yml) provides a [static file server](https://static-web-server.net/) for serving files when using the `local` storage option (alternative to the `s3` option). This option has the added benefit of being persistent when used locally in opposition to localstack (see the [known issues section](#known-issues) for more informations). The server is available at `http://localhost:1081`. diff --git a/docker/compose.yml b/docker/compose.yml index e02a21eb8b..ff3bff7c17 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -53,11 +53,7 @@ services: APPS_JWT_SECRET: # replace with your own value # the Mailer config is set by the "mailer" service below - MAILER_CONFIG_SMTP_HOST: mailer - MAILER_CONFIG_SMTP_PORT: 1025 - MAILER_CONFIG_SMTP_USE_SSL: 'false' # enable if your mail server supports SSL (if using the local mailcatcher, disable SSL) - MAILER_CONFIG_USERNAME: docker - MAILER_CONFIG_PASSWORD: docker + MAILER_CONNECTION: smtp://docker:docker@mailer:1025 # H5P configuration H5P_FILE_STORAGE_TYPE: local diff --git a/src/di/container.ts b/src/di/container.ts index 9661c0d700..d8e5feed79 100644 --- a/src/di/container.ts +++ b/src/di/container.ts @@ -22,11 +22,7 @@ import { GEOLOCATION_API_KEY, IMAGE_CLASSIFIER_API, MAILER_CONFIG_FROM_EMAIL, - MAILER_CONFIG_PASSWORD, - MAILER_CONFIG_SMTP_HOST, - MAILER_CONFIG_SMTP_PORT, - MAILER_CONFIG_SMTP_USE_SSL, - MAILER_CONFIG_USERNAME, + MAILER_CONNECTION, MEILISEARCH_MASTER_KEY, MEILISEARCH_URL, REDIS_CONNECTION, @@ -120,20 +116,8 @@ export const registerDependencies = (instance: FastifyInstance) => { registerValue( MailerService, new MailerService({ - host: MAILER_CONFIG_SMTP_HOST, - port: MAILER_CONFIG_SMTP_PORT, - useSsl: MAILER_CONFIG_SMTP_USE_SSL, - username: MAILER_CONFIG_USERNAME, - password: MAILER_CONFIG_PASSWORD, + connection: MAILER_CONNECTION, fromEmail: MAILER_CONFIG_FROM_EMAIL, }), ); - // registerValue('MAIL_KEY', { - // host: MAILER_CONFIG_SMTP_HOST, - // port: MAILER_CONFIG_SMTP_PORT, - // useSsl: MAILER_CONFIG_SMTP_USE_SSL, - // username: MAILER_CONFIG_USERNAME, - // password: MAILER_CONFIG_PASSWORD, - // fromEmail: MAILER_CONFIG_FROM_EMAIL, - // }); }; diff --git a/src/plugins/mailer/mailer.service.ts b/src/plugins/mailer/mailer.service.ts index 5f2dee49d5..af2158a1e2 100644 --- a/src/plugins/mailer/mailer.service.ts +++ b/src/plugins/mailer/mailer.service.ts @@ -11,11 +11,7 @@ export interface Mail { } export interface MailerOptions { - host: string; - port?: number; - useSsl?: boolean; - username: string; - password: string; + connection: string; fromEmail: string; } @@ -24,17 +20,9 @@ export class MailerService { private readonly fromEmail: string; private readonly transporter: Transporter; - constructor({ host, port, useSsl, username, password, fromEmail }: MailerOptions) { + constructor({ connection, fromEmail }: MailerOptions) { this.fromEmail = fromEmail; - this.transporter = createTransport({ - host, - port: port ?? 465, - secure: useSsl ?? true, - auth: { - user: username, - pass: password, - }, - }); + this.transporter = createTransport(connection); } /** diff --git a/src/utils/config.ts b/src/utils/config.ts index 9aa8befd2d..2940872c50 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -161,24 +161,10 @@ export const EMAIL_CHANGE_JWT_SECRET: string = asDefined( export const EMAIL_CHANGE_JWT_EXPIRATION_IN_MINUTES = 1440; // Graasp mailer config -if ( - !process.env.MAILER_CONFIG_SMTP_HOST || - !process.env.MAILER_CONFIG_USERNAME || - !process.env.MAILER_CONFIG_PASSWORD -) { - throw new Error( - `Email config is not fully defined: ${JSON.stringify({ - host: process.env.MAILER_CONFIG_SMTP_HOST, - username: process.env.MAILER_CONFIG_USERNAME, - password: process.env.MAILER_CONFIG_PASSWORD, - })}`, - ); +if (!process.env.MAILER_CONNECTION) { + throw new Error(`MAILER_CONNECTION is not defined`); } -export const MAILER_CONFIG_SMTP_HOST = process.env.MAILER_CONFIG_SMTP_HOST; -export const MAILER_CONFIG_SMTP_PORT = parseInt(process.env.MAILER_CONFIG_SMTP_PORT ?? '465'); -export const MAILER_CONFIG_SMTP_USE_SSL = process.env.MAILER_CONFIG_SMTP_USE_SSL !== 'false'; -export const MAILER_CONFIG_USERNAME = process.env.MAILER_CONFIG_USERNAME; -export const MAILER_CONFIG_PASSWORD = process.env.MAILER_CONFIG_PASSWORD; +export const MAILER_CONNECTION = process.env.MAILER_CONNECTION; export const MAILER_CONFIG_FROM_EMAIL = process.env.MAILER_CONFIG_FROM_EMAIL ?? 'no-reply@graasp.org'; From 6898338e88d65bf0eeae14a2205f067e8c6ea4d7 Mon Sep 17 00:00:00 2001 From: kim Date: Fri, 6 Jun 2025 12:15:41 +0000 Subject: [PATCH 4/5] refactor: remove unused redis config for websockets --- src/app.ts | 3 +- src/services/websockets/multi-instance.ts | 17 ++++------- .../websockets/test/multi-instance.test.ts | 4 +-- src/services/websockets/test/test-utils.ts | 3 +- .../websockets/websocket.controller.ts | 30 ++----------------- src/utils/config.ts | 3 +- 6 files changed, 14 insertions(+), 46 deletions(-) diff --git a/src/app.ts b/src/app.ts index c4c0eca89e..4d90f7d851 100644 --- a/src/app.ts +++ b/src/app.ts @@ -48,8 +48,7 @@ export default async function (instance: FastifyInstance): Promise { prefix: '/ws', redis: { channelName: 'graasp-realtime-updates', - connectionString: REDIS_CONNECTION, - config: {}, + connection: REDIS_CONNECTION, }, }) .register(fp(MemberServiceApi)) diff --git a/src/services/websockets/multi-instance.ts b/src/services/websockets/multi-instance.ts index e0d72e2459..99bec71897 100644 --- a/src/services/websockets/multi-instance.ts +++ b/src/services/websockets/multi-instance.ts @@ -9,7 +9,7 @@ */ import { JTDSchemaType } from 'ajv/dist/core'; import { Ajv } from 'ajv/dist/jtd'; -import { Redis, RedisOptions } from 'ioredis'; +import { Redis } from 'ioredis'; import { FastifyBaseLogger } from 'fastify'; @@ -67,12 +67,8 @@ const redisSerdes = { }; // Helper to create a redis client instance -function createRedisClientInstance( - redisConnection: string, - redisConfig: RedisOptions, - log?: FastifyBaseLogger, -): Redis { - const redis = new Redis(redisConnection, redisConfig); +function createRedisClientInstance(redisConnection: string, log?: FastifyBaseLogger): Redis { + const redis = new Redis(redisConnection); redis.on('error', (err) => { log?.error( @@ -100,16 +96,15 @@ class MultiInstanceChannelsBroker { constructor( wsChannels: WebSocketChannels, redisParams: { - connectionString: string; - config: RedisOptions; + connection: string; channelName: string; }, log?: FastifyBaseLogger, ) { this.wsChannels = wsChannels; this.notifChannel = redisParams.channelName; - this.sub = createRedisClientInstance(redisParams.connectionString, redisParams.config, log); - this.pub = createRedisClientInstance(redisParams.connectionString, redisParams.config, log); + this.sub = createRedisClientInstance(redisParams.connection, log); + this.pub = createRedisClientInstance(redisParams.connection, log); this.sub.subscribe(this.notifChannel, (err, _result) => { if (err) { diff --git a/src/services/websockets/test/multi-instance.test.ts b/src/services/websockets/test/multi-instance.test.ts index 8a26739f38..a0a5842bb9 100644 --- a/src/services/websockets/test/multi-instance.test.ts +++ b/src/services/websockets/test/multi-instance.test.ts @@ -113,9 +113,7 @@ test.skip('incorrect Redis message format', async () => { const server = await createWsFastifyInstance(config, async (instance) => { logInfoSpy = jest.spyOn(instance.log, 'info'); }); - const pub = new Redis({ - host: config.redis.config.host, - }); + const pub = new Redis(config.redis.connection); pub.publish(config.redis.channelName, JSON.stringify('Mock invalid redis message')); await waitForExpect(() => { expect(logInfoSpy).toHaveBeenCalledWith( diff --git a/src/services/websockets/test/test-utils.ts b/src/services/websockets/test/test-utils.ts index a9c1d53f26..c26587f9ae 100644 --- a/src/services/websockets/test/test-utils.ts +++ b/src/services/websockets/test/test-utils.ts @@ -40,8 +40,7 @@ export function createDefaultLocalConfig( port: options.port, prefix: options.prefix || '/ws', redis: options.redis || { - connectionString: REDIS_CONNECTION, - config: {}, + connection: REDIS_CONNECTION, channelName: 'notifications', }, }; diff --git a/src/services/websockets/websocket.controller.ts b/src/services/websockets/websocket.controller.ts index daa31331a4..857c4dea4d 100644 --- a/src/services/websockets/websocket.controller.ts +++ b/src/services/websockets/websocket.controller.ts @@ -6,10 +6,8 @@ * Integrates the {@link WebSocketChannels} abstraction * in a fastify server plugin with @fastify/websocket */ -import { RedisOptions } from 'ioredis'; - import fws from '@fastify/websocket'; -import { FastifyBaseLogger, FastifyPluginAsync } from 'fastify'; +import { FastifyPluginAsync } from 'fastify'; import { NODE_ENV } from '../../utils/config'; import { optionalIsAuthenticated } from '../auth/plugins/passport'; @@ -24,31 +22,9 @@ import { WebsocketService } from './ws-service'; export interface WebsocketsPluginOptions { prefix: string; redis: { - config: RedisOptions; channelName: string; - connectionString: string; - }; -} - -/** - * Helper function to log boot message after plugin initialization - */ -function logBootMessage(log: FastifyBaseLogger, options: WebsocketsPluginOptions) { - const { redis, ...rest } = options; - const { config, channelName } = redis; - - const loggedOptions = { - ...rest, - redis: { - // don't log password - ...config, - password: undefined, - channelName, - }, + connection: string; }; - delete loggedOptions.redis.password; - - log.info('graasp-plugin-websockets: plugin booted with options', loggedOptions); } const plugin: FastifyPluginAsync = async (fastify, options) => { @@ -118,7 +94,7 @@ const plugin: FastifyPluginAsync = async (fastify, opti done(); }); - logBootMessage(log, options); + log.info('graasp-plugin-websockets: plugin booted'); }; export default plugin; diff --git a/src/utils/config.ts b/src/utils/config.ts index 2940872c50..01705679f5 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -162,7 +162,8 @@ export const EMAIL_CHANGE_JWT_EXPIRATION_IN_MINUTES = 1440; // Graasp mailer config if (!process.env.MAILER_CONNECTION) { - throw new Error(`MAILER_CONNECTION is not defined`); + console.error('MAILER_CONNECTION environment variable missing.'); + process.exit(1); } export const MAILER_CONNECTION = process.env.MAILER_CONNECTION; export const MAILER_CONFIG_FROM_EMAIL = From 4518339c37a940c2b098b6892e7049e2d7a8e66f Mon Sep 17 00:00:00 2001 From: kim Date: Fri, 6 Jun 2025 12:22:13 +0000 Subject: [PATCH 5/5] refactor: fix test workflow --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0517fd791c..517935ca77 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ env: EMAIL_CHANGE_JWT_SECRET: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef MAILER_CONNECTION: smtp://username:password@localhost:1025 RECAPTCHA_SECRET_ACCESS_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef - REDIS_CONNECTION=: redis://localhost:6379 + REDIS_CONNECTION: 'redis://localhost:6379' REFRESH_TOKEN_JWT_SECRET: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef S3_FILE_ITEM_ACCESS_KEY_ID: graasp-user S3_FILE_ITEM_BUCKET: graasp