added mail server (nodemailer)

This commit is contained in:
Rajpreet0 2026-01-09 15:21:18 +01:00
parent f849df9bfd
commit f99d38b5f5
7 changed files with 1739 additions and 11 deletions

1426
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -32,6 +32,7 @@
"lucide-react": "^0.552.0",
"next": "^16.0.10",
"next-themes": "^0.4.6",
"nodemailer": "^7.0.12",
"prisma": "^6.19.0",
"react": "19.2.0",
"react-dom": "19.2.0",
@ -47,6 +48,7 @@
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^20",
"@types/nodemailer": "^7.0.4",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^5.1.0",

View file

@ -0,0 +1,34 @@
import "dotenv/config";
import { sendMail, verifyMailConnection } from "@/src/lib/mail/nodemailer";
async function testEmail() {
// Verbindung testen
console.log("Teste SMTP-Verbindung...");
console.log("SMTP HOST= ", process.env.SMTP_HOST);
console.log("SMTP PORT= ", process.env.SMTP_PORT);
const isConnected = await verifyMailConnection();
if (!isConnected) {
console.error("SMTP-Verbindung fehlgeschlagen!");
return;
}
console.log("SMTP-Verbindung erfolgreich!");
// Test-Mail senden
console.log("Sende Test-Mail...");
const result = await sendMail({
to: "ha.bye@gmx.de",
subject: "Test-Email von S3 Kuma",
html: "<h1>Test erfolgreich!</h1><p>Der E-Mail-Versand funktioniert.</p>",
});
if (result.success) {
console.log("Test-Mail erfolgreich gesendet!");
} else {
console.log("Fehler beim Senden:", result.error);
}
}
testEmail();

View file

@ -0,0 +1,87 @@
/**
* 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;
}
}

View file

@ -0,0 +1,133 @@
/**
* E-Mail Template für Mitarbeiter-Onboarding
*
* Dieses Template wird an neue Mitarbeiter gesendet, um ihnen den Onboarding-Link zu schicken.
*/
interface OnboardingEmailParams {
email: string;
onboardingUrl: string;
companyName?: string;
}
/**
* Generiert HTML für die Onboarding-E-Mail
*/
export function generateOnboardingEmail({
email,
onboardingUrl,
companyName = "Ihr Unternehmen",
}: OnboardingEmailParams): { subject: string; html: string; text: string } {
const subject = `Willkommen bei ${companyName} - Vervollständigen Sie Ihr Profil`;
const html = `
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Willkommen</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f5f5f5; padding: 40px 0;">
<tr>
<td align="center">
<!-- Main Container -->
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px; text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 600;">
Willkommen bei ${companyName}!
</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px;">
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.6;">
Hallo,
</p>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.6;">
Sie wurden als Mitarbeiter bei <strong>${companyName}</strong> registriert. Um Ihr Profil zu vervollständigen und Zugang zum System zu erhalten, klicken Sie bitte auf den Button unten.
</p>
<p style="margin: 0 0 30px; color: #333333; font-size: 16px; line-height: 1.6;">
Ihre registrierte E-Mail-Adresse: <strong>${email}</strong>
</p>
<!-- CTA Button -->
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<a href="${onboardingUrl}"
style="display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; text-decoration: none; padding: 16px 40px; border-radius: 6px; font-size: 16px; font-weight: 600; box-shadow: 0 4px 6px rgba(102, 126, 234, 0.4);">
Profil vervollständigen
</a>
</td>
</tr>
</table>
<p style="margin: 30px 0 20px; color: #666666; font-size: 14px; line-height: 1.6;">
Oder kopieren Sie diesen Link in Ihren Browser:
</p>
<p style="margin: 0 0 30px; color: #667eea; font-size: 14px; word-break: break-all;">
${onboardingUrl}
</p>
<div style="border-top: 1px solid #e0e0e0; padding-top: 20px; margin-top: 30px;">
<p style="margin: 0 0 10px; color: #999999; font-size: 13px; line-height: 1.6;">
<strong>Wichtig:</strong> Dieser Link ist 7 Tage gültig.
</p>
<p style="margin: 0; color: #999999; font-size: 13px; line-height: 1.6;">
Falls Sie diese E-Mail nicht erwartet haben, ignorieren Sie sie bitte.
</p>
</div>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f8f9fa; padding: 30px; text-align: center; border-top: 1px solid #e0e0e0;">
<p style="margin: 0 0 10px; color: #666666; font-size: 14px;">
${companyName}
</p>
<p style="margin: 0; color: #999999; font-size: 12px;">
Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht auf diese E-Mail.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`;
const text = `
Willkommen bei ${companyName}!
Hallo,
Sie wurden als Mitarbeiter bei ${companyName} registriert. Um Ihr Profil zu vervollständigen und Zugang zum System zu erhalten, öffnen Sie bitte den folgenden Link:
${onboardingUrl}
Ihre registrierte E-Mail-Adresse: ${email}
Wichtig: Dieser Link ist 7 Tage gültig.
Falls Sie diese E-Mail nicht erwartet haben, ignorieren Sie sie bitte.
${companyName}
Diese E-Mail wurde automatisch generiert.
`;
return { subject, html, text };
}

View file

@ -13,6 +13,8 @@ import { getUserData, isManager } from "@/src/lib/auth/getUser";
import { employeeSchema, type EmployeeFormData } from "../schemas/employee-schema";
import { revalidatePath } from "next/cache";
import { randomBytes } from "crypto";
import { sendMail } from "@/src/lib/mail/nodemailer";
import { generateOnboardingEmail } from "@/src/lib/mail/templates/employee-onboarding";
/**
* Generiert einen sicheren Onboarding-Token
@ -107,7 +109,31 @@ export async function createEmployee(data: EmployeeFormData, status: "draft" | "
},
});
// Schritt 8: Next.js Cache für /employee Seite invalidieren, damit neue Daten angezeigt werden
// Schritt 8: E-Mail versenden wenn veröffentlicht
if (status === "published" && onboardingToken) {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
const onboardingUrl = `${baseUrl}/employee/onboarding/${onboardingToken}`;
const { subject, html, text } = generateOnboardingEmail({
email: validatedData.email,
onboardingUrl,
companyName: process.env.COMPANY_NAME || "S3 Kuma",
});
// E-Mail asynchron versenden (nicht auf Antwort warten)
sendMail({
to: validatedData.email,
subject,
html,
text,
}).catch((error) => {
console.error("Fehler beim E-Mail-Versand:", error);
// E-Mail-Fehler wird nicht an den Benutzer weitergegeben
// Der Mitarbeiter wurde trotzdem erstellt
});
}
// Schritt 9: Next.js Cache für /employee Seite invalidieren, damit neue Daten angezeigt werden
revalidatePath("/employee");
return {
@ -116,7 +142,7 @@ export async function createEmployee(data: EmployeeFormData, status: "draft" | "
onboardingToken: onboardingToken || undefined,
message:
status === "published"
? "Mitarbeiter erfolgreich veröffentlicht. Onboarding-Link wurde generiert."
? "Mitarbeiter erfolgreich veröffentlicht. Onboarding-E-Mail wurde versendet."
: "Entwurf erfolgreich gespeichert",
};
} catch (error) {
@ -304,11 +330,13 @@ export async function updateEmployee(
// Schritt 8: Onboarding-Token generieren, falls noch nicht vorhanden und veröffentlicht
let onboardingToken = existingEmployee.onboardingToken;
let onboardingTokenExpiry = existingEmployee.onboardingTokenExpiry;
let shouldSendEmail = false;
if (status === "published" && !existingEmployee.onboardingToken) {
onboardingToken = generateOnboardingToken();
onboardingTokenExpiry = new Date();
onboardingTokenExpiry.setDate(onboardingTokenExpiry.getDate() + 7);
shouldSendEmail = true; // E-Mail nur senden wenn neuer Token generiert wurde
}
// Schritt 9: Mitarbeiter in der Datenbank aktualisieren
@ -325,7 +353,30 @@ export async function updateEmployee(
},
});
// Schritt 10: Next.js Cache invalidieren
// Schritt 10: E-Mail versenden wenn neuer Token generiert wurde
if (shouldSendEmail && onboardingToken) {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
const onboardingUrl = `${baseUrl}/employee/onboarding/${onboardingToken}`;
const { subject, html, text } = generateOnboardingEmail({
email: validatedData.email,
onboardingUrl,
companyName: process.env.COMPANY_NAME || "S3 Kuma",
});
// E-Mail asynchron versenden (nicht auf Antwort warten)
sendMail({
to: validatedData.email,
subject,
html,
text,
}).catch((error) => {
console.error("Fehler beim E-Mail-Versand:", error);
// E-Mail-Fehler wird nicht an den Benutzer weitergegeben
});
}
// Schritt 11: Next.js Cache invalidieren
revalidatePath("/employee");
revalidatePath(`/employee/edit/${employeeId}`);
@ -333,7 +384,9 @@ export async function updateEmployee(
success: true,
message:
status === "published"
? "Mitarbeiter erfolgreich aktualisiert und veröffentlicht"
? shouldSendEmail
? "Mitarbeiter erfolgreich aktualisiert. Onboarding-E-Mail wurde versendet."
: "Mitarbeiter erfolgreich aktualisiert und veröffentlicht"
: "Entwurf erfolgreich aktualisiert",
};
} catch (error) {

View file

@ -74,13 +74,6 @@ const EmployeeCreateView = ({ customRoles }: EmployeeCreateViewProps) => {
if (result.success) {
toast.success(result.message);
if (result.onboardingToken) {
const onboardingUrl = `${window.location.origin}/employee/onboarding/${result.onboardingToken}`;
console.log("Onboarding URL:", onboardingUrl);
// TODO: Hier später die Mail-Versendung implementieren
}
router.push("/employee");
} else {
toast.error(result.error || "Ein Fehler ist aufgetreten");