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:
1 parent
7a13dad693
commit
970986fef7
6 files changed
+158
-4
No files matched your search
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>`,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user