mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
added mail server (nodemailer)
This commit is contained in:
parent
f849df9bfd
commit
f99d38b5f5
7 changed files with 1739 additions and 11 deletions
1426
package-lock.json
generated
1426
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
34
src/components/test/test-mail.ts
Normal file
34
src/components/test/test-mail.ts
Normal 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();
|
||||
87
src/lib/mail/nodemailer.ts
Normal file
87
src/lib/mail/nodemailer.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
133
src/lib/mail/templates/employee-onboarding.ts
Normal file
133
src/lib/mail/templates/employee-onboarding.ts
Normal 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 };
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue