From 3fa4da6f9aa612130da8cc14689f20e74390f4d9 Mon Sep 17 00:00:00 2001 From: Rajpreet0 Date: Tue, 28 Jul 2026 01:12:20 +0200 Subject: [PATCH] nodemailer to resend --- package-lock.json | 71 ++++++++++----- package.json | 3 +- src/app/api/stripe/webhook/route.ts | 2 +- src/app/page.tsx | 2 +- src/components/test/test-mail.ts | 4 +- src/lib/mail/nodemailer.ts | 87 ------------------- src/lib/mail/resend.ts | 74 ++++++++++++++++ .../ui/components/AuthSidebarComponent.tsx | 2 +- .../auth/ui/views/employee-onboaring-view.tsx | 2 +- .../auth/ui/views/forgot-password-view.tsx | 2 +- src/modules/auth/ui/views/login-view.tsx | 2 +- .../auth/ui/views/register-admin-view.tsx | 2 +- .../auth/ui/views/register-user-view.tsx | 2 +- .../auth/ui/views/reset-password-view.tsx | 2 +- .../employee/actions/employee-actions.ts | 10 +-- 15 files changed, 141 insertions(+), 126 deletions(-) delete mode 100644 src/lib/mail/nodemailer.ts create mode 100644 src/lib/mail/resend.ts diff --git a/package-lock.json b/package-lock.json index d1b5131..54fba31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,6 @@ "next": "16.2.6", "next-intl": "^4.13.2", "next-themes": "^0.4.6", - "nodemailer": "^8.0.5", "prisma": "^6.19.0", "radix-ui": "^1.4.3", "react": "19.2.0", @@ -43,6 +42,7 @@ "react-dom": "19.2.0", "react-icons": "^5.5.0", "react-phone-number-input": "^3.4.14", + "resend": "^6.18.0", "sonner": "^2.0.7", "stripe": "^22.1.1", "tailwind-merge": "^3.3.1", @@ -57,7 +57,6 @@ "@testing-library/user-event": "^14.6.1", "@types/dompurify": "^3.0.5", "@types/node": "^20", - "@types/nodemailer": "^7.0.4", "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^5.1.0", @@ -4003,6 +4002,12 @@ "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", "license": "MIT" }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -5219,16 +5224,6 @@ "undici-types": "~6.21.0" } }, - "node_modules/@types/nodemailer": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.12.tgz", - "integrity": "sha512-80vKwiIsVSyFA1rRovH59jNPLBOuc6dRZIHEu40gXTkBkZnQv8vog1xSGEb9j5q/tdMAs5ivvDR2pLTU0hGHXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/react": { "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", @@ -8014,6 +8009,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fast-string-truncated-width": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", @@ -10265,15 +10266,6 @@ "node": ">=18" } }, - "node_modules/nodemailer": { - "version": "8.0.11", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.11.tgz", - "integrity": "sha512-nrO/pDAUKl+wXX+lx16tDLbnm0fW6sK/x8mgohaCpg+CdCEl482bD4tCuAZk2DyliruiNTIZxRCoWkDqJEnAiA==", - "license": "MIT-0", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/nypm": { "version": "0.6.7", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.7.tgz", @@ -10653,6 +10645,12 @@ "node": ">= 0.4" } }, + "node_modules/postal-mime": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.5.tgz", + "integrity": "sha512-GNEXKvWFQnbgO5NlrGzVa0FmWzBZ24PersAWErttSg1Hjpf0ATxTwS5DOMGaOpTG6bUh5cTr7xi0jAD942wCJA==", + "license": "MIT-0" + }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", @@ -11296,6 +11294,27 @@ "node": ">=0.10.0" } }, + "node_modules/resend": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.18.0.tgz", + "integrity": "sha512-EjxZ9AVzywJgOlUoIJe9ytBWVrfbUtJbjeoLnRSvpU1sv97Hh9DSwhw+k8kiujrG4Rg4bzTBsjlmwWWuoOxSug==", + "license": "MIT", + "dependencies": { + "postal-mime": "2.7.5", + "standardwebhooks": "1.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -11833,6 +11852,16 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", diff --git a/package.json b/package.json index 1358bf6..2677f54 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "next": "16.2.6", "next-intl": "^4.13.2", "next-themes": "^0.4.6", - "nodemailer": "^8.0.5", "prisma": "^6.19.0", "radix-ui": "^1.4.3", "react": "19.2.0", @@ -51,6 +50,7 @@ "react-dom": "19.2.0", "react-icons": "^5.5.0", "react-phone-number-input": "^3.4.14", + "resend": "^6.18.0", "sonner": "^2.0.7", "stripe": "^22.1.1", "tailwind-merge": "^3.3.1", @@ -65,7 +65,6 @@ "@testing-library/user-event": "^14.6.1", "@types/dompurify": "^3.0.5", "@types/node": "^20", - "@types/nodemailer": "^7.0.4", "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^5.1.0", diff --git a/src/app/api/stripe/webhook/route.ts b/src/app/api/stripe/webhook/route.ts index 76e8ef2..4029fac 100644 --- a/src/app/api/stripe/webhook/route.ts +++ b/src/app/api/stripe/webhook/route.ts @@ -3,7 +3,7 @@ import Stripe from "stripe"; import { stripe, getWebhookSecret } from "@/src/lib/stripe"; import { prisma } from "@/src/lib/prisma"; import { revalidatePath, revalidateTag } from "next/cache"; -import { sendMail } from "@/src/lib/mail/nodemailer"; +import { sendMail } from "@/src/lib/mail/resend"; import { generateBookingConfirmationEmail } from "@/src/lib/mail/templates/booking-confirmation"; import { generateBookingRefundEmail } from "@/src/lib/mail/templates/booking-refund"; diff --git a/src/app/page.tsx b/src/app/page.tsx index 267acd2..0d938a1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2458,7 +2458,7 @@ export default function LandingPage() {
  • Support
  • -
  • info@since3.de
  • +
  • info@kumasy.de
  • diff --git a/src/components/test/test-mail.ts b/src/components/test/test-mail.ts index b9a73f9..5ffd6d9 100644 --- a/src/components/test/test-mail.ts +++ b/src/components/test/test-mail.ts @@ -1,5 +1,5 @@ import "dotenv/config"; -import { sendMail, verifyMailConnection } from "@/src/lib/mail/nodemailer"; +import { sendMail, verifyMailConnection } from "@/src/lib/mail/resend"; async function testEmail() { // Verbindung testen @@ -18,7 +18,7 @@ async function testEmail() { console.log("Sende Test-Mail..."); const result = await sendMail({ to: process.env.SMTP_TEST_MAIL || "", - subject: "Test-Email von S3 Kuma", + subject: "Test-Email von Kumasy", html: "

    Test erfolgreich!

    Der E-Mail-Versand funktioniert.

    ", }); diff --git a/src/lib/mail/nodemailer.ts b/src/lib/mail/nodemailer.ts deleted file mode 100644 index 5de0e03..0000000 --- a/src/lib/mail/nodemailer.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Nodemailer Configuration - * - * Diese Datei konfiguriert Nodemailer für den E-Mail-Versand. - * Unterstützt verschiedene SMTP-Anbieter wie Gmail, Outlook, SendGrid, etc. - */ - -import nodemailer from "nodemailer"; - -/** - * Erstellt einen Nodemailer Transporter - * - * Konfiguration über Umgebungsvariablen: - * - SMTP_HOST: SMTP Server Host (z.B. smtp.gmail.com) - * - SMTP_PORT: SMTP Server Port (z.B. 587) - * - SMTP_USER: SMTP Benutzername/E-Mail - * - SMTP_PASSWORD: SMTP Passwort/App-Passwort - * - SMTP_FROM: Absender E-Mail-Adresse - */ -export const transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST, - port: Number(process.env.SMTP_PORT || 587), - secure: Number(process.env.SMTP_PORT) === 465, // true für Port 465, false für andere Ports - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASSWORD, - }, -}); - -/** - * Sendet eine E-Mail - * - * @param to - Empfänger E-Mail-Adresse - * @param subject - E-Mail Betreff - * @param html - HTML-Inhalt der E-Mail - * @param text - Plain-Text Alternative (optional) - * @returns Promise mit dem Ergebnis des Versands - */ -export async function sendMail({ - to, - subject, - html, - text, -}: { - to: string; - subject: string; - html: string; - text?: string; -}) { - try { - const info = await transporter.sendMail({ - from: process.env.SMTP_FROM || process.env.SMTP_USER, - to, - subject, - html, - text: text || stripHtml(html), // Fallback: HTML zu Text konvertieren - }); - - console.log("E-Mail erfolgreich gesendet:", info.messageId); - return { success: true, messageId: info.messageId }; - } catch (error) { - console.error("Fehler beim E-Mail-Versand:", error); - return { success: false, error }; - } -} - -/** - * Einfache Funktion zum Entfernen von HTML-Tags (für Text-Alternative) - */ -function stripHtml(html: string): string { - return html.replace(/<[^>]*>/g, ""); -} - -/** - * Verifiziert die SMTP-Verbindung - * Nützlich zum Testen der Konfiguration - */ -export async function verifyMailConnection() { - try { - await transporter.verify(); - console.log("SMTP-Verbindung erfolgreich verifiziert"); - return true; - } catch (error) { - console.error("SMTP-Verbindung fehlgeschlagen:", error); - return false; - } -} diff --git a/src/lib/mail/resend.ts b/src/lib/mail/resend.ts new file mode 100644 index 0000000..a971535 --- /dev/null +++ b/src/lib/mail/resend.ts @@ -0,0 +1,74 @@ +/** + * Resend Konfiguration + * + * Diese Datei konfiguriert Resend für den E-Mail-Versand. + */ + +import { Resend } from "resend"; + +export const resend = new Resend(process.env.RESEND_API_KEY); + +/** + * Sendet eine E-Mail + * + * @param to - Empfänger E-Mail-Adresse + * @param subject - E-Mail Betreff + * @param html - HTML-Inhalt der E-Mail + * @param text - Plain-Text Alternative (optional) + * @returns Promise mit dem Ergebnis des Versands + */ +export async function sendMail({ + to, + subject, + html, + text, +}: { + to: string; + subject: string; + html: string; + text?: string; +}) { + try { + const { data, error } = await resend.emails.send({ + from: process.env.RESEND_FROM || "", + to, + subject, + html, + text: text || stripHtml(html), // Fallback: HTML zu Text konvertieren + }); + + if (error) { + console.error("Fehler beim E-Mail-Versand:", error); + return { success: false, error }; + } + + console.log("E-Mail erfolgreich gesendet:", data?.id); + return { success: true, messageId: data?.id }; + } catch (error) { + console.error("Fehler beim E-Mail-Versand:", error); + return { success: false, error }; + } +} + +/** + * Einfache Funktion zum Entfernen von HTML-Tags (für Text-Alternative) + */ +function stripHtml(html: string): string { + return html.replace(/<[^>]*>/g, ""); +} + +/** + * Prüft, ob die Resend-Konfiguration vollständig ist. + * Resend ist HTTP-basiert, es gibt keine dauerhafte Verbindung wie bei SMTP zu verifizieren. + */ +export async function verifyMailConnection() { + const isConfigured = Boolean(process.env.RESEND_API_KEY && process.env.RESEND_FROM); + + if (!isConfigured) { + console.error("Resend-Konfiguration unvollständig: RESEND_API_KEY oder RESEND_FROM fehlt"); + return false; + } + + console.log("Resend-Konfiguration vorhanden"); + return true; +} diff --git a/src/modules/auth/ui/components/AuthSidebarComponent.tsx b/src/modules/auth/ui/components/AuthSidebarComponent.tsx index 4219901..8a231c4 100644 --- a/src/modules/auth/ui/components/AuthSidebarComponent.tsx +++ b/src/modules/auth/ui/components/AuthSidebarComponent.tsx @@ -47,7 +47,7 @@ const AuthSidebarComponent: React.FC = ({

    Bei Fragen:{" "} - + {footer}

    diff --git a/src/modules/auth/ui/views/employee-onboaring-view.tsx b/src/modules/auth/ui/views/employee-onboaring-view.tsx index dd126b6..59d849b 100644 --- a/src/modules/auth/ui/views/employee-onboaring-view.tsx +++ b/src/modules/auth/ui/views/employee-onboaring-view.tsx @@ -212,7 +212,7 @@ const EmployeeOnboardingView = () => {
    {
    diff --git a/src/modules/auth/ui/views/login-view.tsx b/src/modules/auth/ui/views/login-view.tsx index 6d688c1..e959800 100644 --- a/src/modules/auth/ui/views/login-view.tsx +++ b/src/modules/auth/ui/views/login-view.tsx @@ -44,7 +44,7 @@ const LoginView = () => { diff --git a/src/modules/auth/ui/views/register-admin-view.tsx b/src/modules/auth/ui/views/register-admin-view.tsx index c4263d6..75c6864 100644 --- a/src/modules/auth/ui/views/register-admin-view.tsx +++ b/src/modules/auth/ui/views/register-admin-view.tsx @@ -140,7 +140,7 @@ const RegisterAdmin = () => {
    {
    {
    diff --git a/src/modules/employee/actions/employee-actions.ts b/src/modules/employee/actions/employee-actions.ts index e25dda8..399ccfe 100644 --- a/src/modules/employee/actions/employee-actions.ts +++ b/src/modules/employee/actions/employee-actions.ts @@ -19,7 +19,7 @@ function invalidateUserDataCache(email: string) { (revalidateTag as any)(`user-data-${email}`); } import { randomBytes } from "crypto"; -import { sendMail } from "@/src/lib/mail/nodemailer"; +import { sendMail } from "@/src/lib/mail/resend"; import { generateOnboardingEmail } from "@/src/lib/mail/templates/employee-onboarding"; import { generateOnboardingCompleteEmail } from "@/src/lib/mail/templates/onboarding-complete"; @@ -161,7 +161,7 @@ export async function createEmployee(data: EmployeeFormData, status: "draft" | " const { subject, html, text } = generateOnboardingEmail({ email: validatedData.email, onboardingUrl, - companyName: process.env.COMPANY_NAME || "S3 Kuma", + companyName: process.env.COMPANY_NAME || "Kumasy", }); // Send email asynchronously (fire and forget) @@ -522,7 +522,7 @@ export async function updateEmployee( const { subject, html, text } = generateOnboardingEmail({ email: validatedData.email, onboardingUrl, - companyName: process.env.COMPANY_NAME || "S3 Kuma", + companyName: process.env.COMPANY_NAME || "Kumasy", }); // Send email asynchronously (fire and forget) @@ -876,7 +876,7 @@ export async function completeEmployeeOnboarding( employeeFirstName: data.firstName, employeeLastName: data.lastName, employeeEmail: employee.email, - companyName: process.env.COMPANY_NAME || "S3 Kuma", + companyName: process.env.COMPANY_NAME || "Kumasy", }); sendMail({ to: manager.email, subject, html, text }).catch((error) => { @@ -946,7 +946,7 @@ export async function resendOnboardingEmail(employeeId: string) { const { subject, html, text } = generateOnboardingEmail({ email: employee.email, onboardingUrl, - companyName: process.env.COMPANY_NAME || "S3 Kuma", + companyName: process.env.COMPANY_NAME || "Kumasy", }); const mailResult = await sendMail({ to: employee.email, subject, html, text });