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 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-07-31 23:29:19 +02:00
parent 0a0990c6f4
commit c80f78594e
7 changed files with 76 additions and 22 deletions

View File

@@ -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;