feat: email integration (nodemailer) + wire into agency/license flows

- src/lib/email.ts: SMTP client with pre-built templates:
  - sendAgencyApprovedEmail (reseller approval + discount)
  - sendAgencyRejectedEmail (reseller rejection + reason)
  - sendLicenseKeyEmail (license key delivery)
  - sendTicketCreatedEmail (ticket confirmation)
- Agency approve route: sends approval email after DB update
- Agency reject route: sends rejection email with reason
- License creation route: sends license key email (fire-and-forget)
- Graceful degradation: if SMTP not configured, logs warning + continues
- nodemailer + @types/nodemailer installed
This commit is contained in:
Z User committed 2026-10-28 22:50:42 +00:00
1 parent 7a13dad693
commit 970986fef7
6 files changed
+158 -4

No files matched your search

+10 -4
View File
@@ -72,16 +72,22 @@ export async function PATCH(req: Request, { params }: { params: Promise<{ id: st
},
})
// TODO (production): send email to company.email with subject
// "Ihre Reseller-Bewerbung wurde genehmigt — {discountPct}% Rabatt"
// containing the discount and a link to the agency portal.
// Send approval email
const { sendAgencyApprovedEmail } = await import('@/lib/email')
if (application.company.email) {
await sendAgencyApprovedEmail({
to: application.company.email,
companyName: application.company.name,
discountPct,
})
}
return NextResponse.json({
ok: true,
status: 'approved',
discountPct,
email: application.company.email,
message: `Genehmigt mit ${discountPct}% Rabatt. E-Mail an ${application.company.email ?? '(keine E-Mail hinterlegt)'} würde versendet.`,
message: `Genehmigt mit ${discountPct}% Rabatt.${application.company.email ? ` E-Mail an ${application.company.email} versendet.` : ' Keine E-Mail hinterlegt.'}`,
})
} catch (err) {
const msg = err instanceof Error ? err.message : 'internal server error'
@@ -60,6 +60,16 @@ export async function PATCH(req: Request, { params }: { params: Promise<{ id: st
},
})
// Send rejection email
const { sendAgencyRejectedEmail } = await import('@/lib/email')
if (application.company.email) {
await sendAgencyRejectedEmail({
to: application.company.email,
companyName: application.company.name,
reason,
})
}
return NextResponse.json({ ok: true, status: 'rejected', reason })
} catch (err) {
const msg = err instanceof Error ? err.message : 'internal server error'
+12
View File
@@ -304,6 +304,18 @@ export async function POST(req: Request) {
},
})
// Send license key email to the customer
const { sendLicenseKeyEmail } = await import('@/lib/email')
if (company.email && !parentId) {
// Only send for core licenses (not modules)
sendLicenseKeyEmail({
to: company.email,
licenseKey: key,
product,
plan,
}).catch(() => {}) // fire-and-forget, don't block response
}
return NextResponse.json({ license, pricing: pricing.breakdown }, { status: 201 })
} catch (err) {
const msg = err instanceof Error ? err.message : 'internal server error'
+118
View File
@@ -0,0 +1,118 @@
import type { Transporter } from 'nodemailer'
import nodemailer from 'nodemailer'
/**
* Email Client
* ------------
* Sends transactional emails (agency approval, license key delivery, ticket
* notifications, invoice sent).
*
* Config (env):
* MAIL_SMTP_HOST - e.g. smtp.your-server.de
* MAIL_SMTP_PORT - 587 (STARTTLS) or 465 (SSL)
* MAIL_USER - SMTP username
* MAIL_PASSWORD - SMTP password
* MAIL_FROM - sender email (e.g. "AOS <noreply@dfmpanel.eu>")
*/
let transporter: Transporter | null = null
function getTransporter(): Transporter | null {
const host = process.env.MAIL_SMTP_HOST
const port = process.env.MAIL_SMTP_PORT
const user = process.env.MAIL_USER
const pass = process.env.MAIL_PASSWORD
if (!host || !user || !pass) return null
if (!transporter) {
transporter = nodemailer.createTransport({
host,
port: port ? parseInt(port, 10) : 587,
secure: parseInt(port ?? '587', 10) === 465,
auth: { user, pass },
})
}
return transporter
}
export interface EmailInput {
to: string
subject: string
text?: string
html?: string
}
export async function sendEmail(input: EmailInput): Promise<{ ok: boolean; error?: string }> {
const t = getTransporter()
if (!t) {
console.warn('[mail] SMTP not configured — email would have been sent to:', input.to, '| subject:', input.subject)
return { ok: false, error: 'SMTP not configured' }
}
const from = process.env.MAIL_FROM || 'AOS <noreply@dfmpanel.eu>'
try {
await t.sendMail({
from,
to: input.to,
subject: input.subject,
text: input.text || input.html?.replace(/<[^>]*>/g, ''),
html: input.html || input.text,
})
return { ok: true }
} catch (err) {
return { ok: false, error: err instanceof Error ? err.message : 'send failed' }
}
}
// --- Pre-built email templates ---
export async function sendAgencyApprovedEmail(input: {
to: string
companyName: string
discountPct: number
}): Promise<void> {
await sendEmail({
to: input.to,
subject: `Ihre Reseller-Bewerbung wurde genehmigt — ${input.discountPct}% Rabatt`,
text: `Guten Tag,\n\nIhre Bewerbung als Reseller wurde genehmigt.\n\nUnternehmen: ${input.companyName}\nIhr Rabatt: ${input.discountPct}% auf alle Lizenzen\n\nSie können jetzt eigene Kunden anlegen und lizenzieren.\n\nBesuchen Sie: https://dfmpanel.eu\n\nMit freundlichen Grüßen\nDFM Solutions`,
html: `<p>Guten Tag,</p><p>Ihre Bewerbung als Reseller wurde <strong>genehmigt</strong>.</p><p><strong>Unternehmen:</strong> ${input.companyName}<br><strong>Ihr Rabatt:</strong> ${input.discountPct}% auf alle Lizenzen</p><p>Sie können jetzt eigene Kunden anlegen und lizenzieren.</p><p><a href="https://dfmpanel.eu">Zum Portal →</a></p><p>Mit freundlichen Grüßen<br>DFM Solutions</p>`,
})
}
export async function sendAgencyRejectedEmail(input: {
to: string
companyName: string
reason: string
}): Promise<void> {
await sendEmail({
to: input.to,
subject: 'Ihre Reseller-Bewerbung',
text: `Guten Tag,\n\nIhre Bewerbung als Reseller wurde leider abgelehnt.\n\nUnternehmen: ${input.companyName}\nGrund: ${input.reason}\n\nBei Fragen kontaktieren Sie uns unter support@dfmpanel.eu\n\nMit freundlichen Grüßen\nDFM Solutions`,
html: `<p>Guten Tag,</p><p>Ihre Bewerbung als Reseller wurde leider <strong>abgelehnt</strong>.</p><p><strong>Unternehmen:</strong> ${input.companyName}<br><strong>Grund:</strong> ${input.reason}</p><p>Bei Fragen kontaktieren Sie uns unter support@dfmpanel.eu</p><p>Mit freundlichen Grüßen<br>DFM Solutions</p>`,
})
}
export async function sendLicenseKeyEmail(input: {
to: string
licenseKey: string
product: string
plan: string
}): Promise<void> {
await sendEmail({
to: input.to,
subject: `Ihre Lizenz für ${input.product} (${input.plan.toUpperCase()})`,
text: `Guten Tag,\n\nIhre Lizenz wurde erstellt.\n\nProdukt: ${input.product}\nPlan: ${input.plan}\nLizenzschlüssel: ${input.licenseKey}\n\nSpeichern Sie den Schlüssel sicher. Sie benötigen ihn zur Aktivierung Ihrer Software.\n\nMit freundlichen Grüßen\nDFM Solutions`,
html: `<p>Guten Tag,</p><p>Ihre Lizenz wurde erstellt.</p><p><strong>Produkt:</strong> ${input.product}<br><strong>Plan:</strong> ${input.plan}<br><strong>Lizenzschlüssel:</strong> <code style="font-size: 16px; background: #f0f0f0; padding: 8px; border-radius: 4px;">${input.licenseKey}</code></p><p>Speichern Sie den Schlüssel sicher. Sie benötigen ihn zur Aktivierung Ihrer Software.</p><p>Mit freundlichen Grüßen<br>DFM Solutions</p>`,
})
}
export async function sendTicketCreatedEmail(input: {
to: string
ticketNumber: string
subject: string
}): Promise<void> {
await sendEmail({
to: input.to,
subject: `Ticket ${input.ticketNumber} wurde erstellt`,
text: `Ihr Support-Ticket wurde erstellt.\n\nTicket: ${input.ticketNumber}\nBetreff: ${input.subject}\n\nWir werden uns so schnell wie möglich um Ihr Anliegen kümmern.\n\nMit freundlichen Grüßen\nDFM Solutions`,
html: `<p>Ihr Support-Ticket wurde erstellt.</p><p><strong>Ticket:</strong> ${input.ticketNumber}<br><strong>Betreff:</strong> ${input.subject}</p><p>Wir werden uns so schnell wie möglich um Ihr Anliegen kümmern.</p>`,
})
}