feat(backend): mail module + send personal Gemeinde-Teamer invites
New global mail/ module mirroring the files/storage/ provider pattern: - MailProvider abstraction; default LogMailProvider only logs (no delivery), MAIL_PROVIDER=smtp switches to a nodemailer SMTP transport (SMTP_*, MAIL_FROM). - MailService.sendTeamerInvite() composes the invite email with a link built from APP_BASE_URL. TeamerService.createInvite() now mails personal invites (those with an email) best-effort and returns `emailSent`; group links are unchanged. Delivery failures are logged and swallowed, never blocking invite creation. New env: APP_BASE_URL, MAIL_PROVIDER, MAIL_FROM, SMTP_HOST/PORT/SECURE/ USER/PASS. Tests: teamer spec covers mail-on-personal-invite, no-mail-on-group-link, and transport-drop; npm test green at 56. Docs updated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { Role, SyncOperation } from '@prisma/client';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { PrismaClient } from '../prisma/prisma.module';
|
||||
import { SyncService } from '../sync/sync.service';
|
||||
import { MailService } from '../mail/mail.service';
|
||||
import { AuthenticatedUser } from '../auth/authenticated-request';
|
||||
import { CreateTeamerInviteDto } from './dto/create-teamer-invite.dto';
|
||||
|
||||
@@ -30,6 +31,7 @@ export class TeamerService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaClient,
|
||||
private readonly sync: SyncService,
|
||||
private readonly mail: MailService,
|
||||
) {}
|
||||
|
||||
async createTeamer(
|
||||
@@ -118,7 +120,24 @@ export class TeamerService {
|
||||
},
|
||||
});
|
||||
await this.sync.capture('TeamerInvite', SyncOperation.CREATE, invite.id, invite);
|
||||
return invite;
|
||||
|
||||
// Personal invites go out by email (best-effort); group links are shared
|
||||
// by the Verantwortliche/r directly.
|
||||
let emailSent = false;
|
||||
if (email) {
|
||||
const kc = await this.prisma.kc.findUnique({
|
||||
where: { id: gemeinde.kcId },
|
||||
select: { name: true },
|
||||
});
|
||||
emailSent = await this.mail.sendTeamerInvite({
|
||||
to: email,
|
||||
kcName: kc?.name ?? '',
|
||||
gemeindeName: gemeinde.name,
|
||||
token: invite.token,
|
||||
expiresAt: invite.expiresAt,
|
||||
});
|
||||
}
|
||||
return { ...invite, emailSent };
|
||||
}
|
||||
|
||||
async listInvites(caller: AuthenticatedUser, gemeindeId: string) {
|
||||
|
||||
Reference in New Issue
Block a user