From c80f78594eacfe74d1e59f66d033a22a9b115085 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Fri, 31 Jul 2026 23:29:19 +0200 Subject: [PATCH] fix: register mail layout partial on handlebars singleton for real mailer wiring The HandlebarsAdapter reads partials config from a top-level sibling of `template` (mailerOptions.options.partials), not from template.options.partials where it was nested. Because the mail templates use partial blocks ({{#> layout}}...{{/layout}}), the unregistered partial rendered silently as an unstyled fragment instead of throwing, so this went unnoticed. A config-only fix also breaks on Windows because the adapter's glob-based directory loader mishandles backslash path separators. Fix registers the shared `layout` partial directly on the handlebars module singleton in MailConfigService, bypassing the broken glob loader entirely. Also: - add mail-config.service.spec.ts, an integration test that drives the real MailerOptions + HandlebarsAdapter wiring (would have caught this bug, unlike the existing template-only spec which registers the partial itself) - remove stale nestjs-i18n references from .env.example, env-example, and the backend README (i18n was already removed from the code) - add missing trailing newlines to activation.hbs and reset-password.hbs Co-Authored-By: Claude Sonnet 5 --- .env.example | 2 - env-example | 2 - myteamwallet_backend/README.md | 1 - .../src/mail/mail-config.service.spec.ts | 56 +++++++++++++++++++ .../src/mail/mail-config.service.ts | 33 ++++++----- .../src/mail/mail-templates/activation.hbs | 2 +- .../mail/mail-templates/reset-password.hbs | 2 +- 7 files changed, 76 insertions(+), 22 deletions(-) create mode 100644 myteamwallet_backend/src/mail/mail-config.service.spec.ts diff --git a/.env.example b/.env.example index efc18b7..6c00e3a 100644 --- a/.env.example +++ b/.env.example @@ -2,8 +2,6 @@ NODE_ENV=production APP_PORT=3999 APP_NAME="NestJS API" API_PREFIX=api -APP_FALLBACK_LANGUAGE=en -APP_HEADER_LANGUAGE=x-custom-lang FRONTEND_DOMAIN=http://localhost:3999 BACKEND_DOMAIN=http://localhost:3999 diff --git a/env-example b/env-example index 0d4f88f..b5fd249 100644 --- a/env-example +++ b/env-example @@ -2,8 +2,6 @@ NODE_ENV=development APP_PORT=3000 APP_NAME="NestJS API" API_PREFIX=api -APP_FALLBACK_LANGUAGE=en -APP_HEADER_LANGUAGE=x-custom-lang FRONTEND_DOMAIN=http://localhost:3000 BACKEND_DOMAIN=http://localhost:3000 diff --git a/myteamwallet_backend/README.md b/myteamwallet_backend/README.md index 6979358..d12cc22 100644 --- a/myteamwallet_backend/README.md +++ b/myteamwallet_backend/README.md @@ -26,7 +26,6 @@ Seeden: npm run seed:run - [x] Sign in and sign up via email. - [x] Social sign in (Apple, Facebook, Google, Twitter). - [x] Admin and User roles. -- [x] I18N ([nestjs-i18n](https://www.npmjs.com/package/nestjs-i18n)). - [x] File uploads. Support local and Amazon S3 drivers. - [x] Swagger. - [x] E2E and units tests. diff --git a/myteamwallet_backend/src/mail/mail-config.service.spec.ts b/myteamwallet_backend/src/mail/mail-config.service.spec.ts new file mode 100644 index 0000000..e8fc09b --- /dev/null +++ b/myteamwallet_backend/src/mail/mail-config.service.spec.ts @@ -0,0 +1,56 @@ +import * as path from 'path'; +import { ConfigService } from '@nestjs/config'; +import { MailConfigService } from './mail-config.service'; + +describe('MailConfigService integration', () => { + it('produces mailer options whose adapter actually renders the shared layout partial', (done) => { + const workingDirectory = path.join(__dirname, '..', '..'); + const configValues: Record = { + 'app.workingDirectory': workingDirectory, + 'mail.host': 'localhost', + 'mail.port': 1025, + 'mail.ignoreTLS': true, + 'mail.secure': false, + 'mail.requireTLS': false, + 'mail.user': '', + 'mail.password': '', + 'mail.defaultName': 'TeamWallet', + 'mail.defaultEmail': 'test@example.com', + }; + const configService = { + get: (key: string) => configValues[key], + } as unknown as ConfigService; + + const options = new MailConfigService(configService).createMailerOptions(); + + const mail: { + data: { + template: string; + context: Record; + html?: string; + }; + } = { + data: { + template: 'activation', + context: { + title: 'Test', + year: 2026, + firstName: 'Max', + url: 'https://example.com/confirm-email/abc', + actionTitle: 'Jetzt bestätigen', + }, + }, + }; + + options.template.adapter.compile( + mail, + (err?: Error) => { + expect(err).toBeUndefined(); + expect(mail.data.html).toContain('TeamWallet'); + expect(mail.data.html).toContain('tw-wordmark'); + done(); + }, + options, + ); + }); +}); diff --git a/myteamwallet_backend/src/mail/mail-config.service.ts b/myteamwallet_backend/src/mail/mail-config.service.ts index cf71903..acec8db 100644 --- a/myteamwallet_backend/src/mail/mail-config.service.ts +++ b/myteamwallet_backend/src/mail/mail-config.service.ts @@ -1,4 +1,6 @@ import * as path from 'path'; +import * as fs from 'fs'; +import * as handlebars from 'handlebars'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { MailerOptions, MailerOptionsFactory } from '@nestjs-modules/mailer'; @@ -9,6 +11,21 @@ export class MailConfigService implements MailerOptionsFactory { constructor(private configService: ConfigService) {} createMailerOptions(): MailerOptions { + const templatesDir = path.join( + this.configService.get('app.workingDirectory'), + 'src', + 'mail', + 'mail-templates', + ); + + handlebars.registerPartial( + 'layout', + fs.readFileSync( + path.join(templatesDir, 'partials', 'layout.hbs'), + 'utf-8', + ), + ); + return { transport: { host: this.configService.get('mail.host'), @@ -27,24 +44,10 @@ export class MailConfigService implements MailerOptionsFactory { )}" <${this.configService.get('mail.defaultEmail')}>`, }, template: { - dir: path.join( - this.configService.get('app.workingDirectory'), - 'src', - 'mail', - 'mail-templates', - ), + dir: templatesDir, adapter: new HandlebarsAdapter(), options: { strict: true, - partials: { - dir: path.join( - this.configService.get('app.workingDirectory'), - 'src', - 'mail', - 'mail-templates', - 'partials', - ), - }, }, }, } as MailerOptions; diff --git a/myteamwallet_backend/src/mail/mail-templates/activation.hbs b/myteamwallet_backend/src/mail/mail-templates/activation.hbs index b4f3c47..62850ab 100644 --- a/myteamwallet_backend/src/mail/mail-templates/activation.hbs +++ b/myteamwallet_backend/src/mail/mail-templates/activation.hbs @@ -5,4 +5,4 @@ {{actionTitle}}

Falls der Button nicht funktioniert, kopiere diesen Link in deinen Browser:
{{url}}

-{{/layout}} \ No newline at end of file +{{/layout}} diff --git a/myteamwallet_backend/src/mail/mail-templates/reset-password.hbs b/myteamwallet_backend/src/mail/mail-templates/reset-password.hbs index d66c58c..ae32299 100644 --- a/myteamwallet_backend/src/mail/mail-templates/reset-password.hbs +++ b/myteamwallet_backend/src/mail/mail-templates/reset-password.hbs @@ -6,4 +6,4 @@

Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail einfach ignorieren — es wird nichts verändert.

Falls der Button nicht funktioniert, kopiere diesen Link in deinen Browser:
{{url}}

-{{/layout}} \ No newline at end of file +{{/layout}}