mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
nodemailer to resend
This commit is contained in:
parent
378e16b5c7
commit
3fa4da6f9a
15 changed files with 141 additions and 126 deletions
71
package-lock.json
generated
71
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -2458,7 +2458,7 @@ export default function LandingPage() {
|
|||
<li>
|
||||
<span className="hover:text-white cursor-pointer transition-colors">Support</span>
|
||||
</li>
|
||||
<li>info@since3.de</li>
|
||||
<li>info@kumasy.de</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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: "<h1>Test erfolgreich!</h1><p>Der E-Mail-Versand funktioniert.</p>",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
74
src/lib/mail/resend.ts
Normal file
74
src/lib/mail/resend.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ const AuthSidebarComponent: React.FC<AuthSidebarComponentProps> = ({
|
|||
<CardFooter className="pointer-events-auto p-0 pt-4 mt-auto">
|
||||
<p className="text-yellow text-sm lg:text-base break-words">
|
||||
Bei Fragen:{" "}
|
||||
<a href={`mailto:info@since3.de`} className="hover:underline">
|
||||
<a href={`mailto:${footer}`} className="hover:underline">
|
||||
{footer}
|
||||
</a>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ const EmployeeOnboardingView = () => {
|
|||
<div className="hidden lg:block fixed left-0 top-0 h-screen w-[40%] p-2 z-10">
|
||||
<AuthSidebarComponent
|
||||
title="Werde Teil unseres Teams"
|
||||
footer="info@since3.de"
|
||||
footer="info@kumasy.de"
|
||||
isSteps
|
||||
step={step}
|
||||
totalSteps={2}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const ForgotPasswordView = () => {
|
|||
<AuthSidebarComponent
|
||||
title="Passwort zurücksetzen"
|
||||
description="Keine Sorge, wir helfen Ihnen dabei, wieder Zugang zu Ihrem Konto zu erhalten."
|
||||
footer="info@since3.de"
|
||||
footer="info@kumasy.de"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const LoginView = () => {
|
|||
<AuthSidebarComponent
|
||||
title={t("sidebarTitle")}
|
||||
description={t("sidebarDescription")}
|
||||
footer="info@since3.de"
|
||||
footer="info@kumasy.de"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ const RegisterAdmin = () => {
|
|||
<div className="hidden lg:block fixed left-0 top-0 h-screen w-[40%] p-2 z-10">
|
||||
<AuthSidebarComponent
|
||||
title="Werde zur besten Version deiner selbst"
|
||||
footer="info@since3.de"
|
||||
footer="info@kumasy.de"
|
||||
isSteps
|
||||
step={step}
|
||||
totalSteps={3}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ const RegisterUserView = () => {
|
|||
<div className="hidden lg:block fixed left-0 top-0 h-screen w-[40%] p-2 z-10">
|
||||
<AuthSidebarComponent
|
||||
title="Werde zur besten Version deiner selbst"
|
||||
footer="info@since3.de"
|
||||
footer="info@kumasy.de"
|
||||
isSteps
|
||||
step={step}
|
||||
totalSteps={2}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ const ResetPasswordView = () => {
|
|||
<AuthSidebarComponent
|
||||
title="Neues Passwort setzen"
|
||||
description="Wählen Sie ein sicheres Passwort für Ihr Konto."
|
||||
footer="info@since3.de"
|
||||
footer="info@kumasy.de"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue