diff --git a/modules/admin/actions/client.ts b/modules/admin/actions/client.ts index 754a907..b8a8884 100644 --- a/modules/admin/actions/client.ts +++ b/modules/admin/actions/client.ts @@ -55,13 +55,19 @@ export async function createClient( return { error: error.code === "email_exists" ? "Diese Email ist bereits vergeben." : "Client konnte nicht angelegt werden." } } - const mail = welcomeMail({ name, email, appUrl: await getAppUrl() }); - const sent = await sendMail({ - to: email, - ...mail, - type: "welcome", - idempotencyKey: `welcome-${data.user.id}`, - }); + // Der Account existiert bereits: ein Mailfehler darf nicht verhindern, dass das Passwort angezeigt wird. + let welcomeMailSent = false; + try { + const sent = await sendMail({ + to: email, + ...welcomeMail({ name, email, appUrl: await getAppUrl() }), + type: "welcome", + idempotencyKey: `welcome-${data.user.id}`, + }); + welcomeMailSent = sent.ok; + } catch (mailError) { + console.error("[mail] Willkommensmail fehlgeschlagen:", mailError); + } - return { success: true, email, password, welcomeMailSent: sent.ok } + return { success: true, email, password, welcomeMailSent } } \ No newline at end of file diff --git a/modules/shared/actions/credentials.ts b/modules/shared/actions/credentials.ts index c34be66..b81f67f 100644 --- a/modules/shared/actions/credentials.ts +++ b/modules/shared/actions/credentials.ts @@ -63,17 +63,20 @@ export async function createCredential( // Nur wenn wir den Zugang hinterlegen – legt der Kunde ihn selbst an, braucht er keine Mail. if (session.isAdmin) { - const appUrl = await getAppUrl(); const serviceLabel = getServiceById(validatedFields.data.service).label; after(async () => { - const recipient = await getMailRecipient(profileId); - if (!recipient) return; - await sendMail({ - to: recipient.email, - ...credentialAddedMail({ name: recipient.name, serviceLabel, appUrl }), - type: "credential", - idempotencyKey: `credential-${credentialId}`, - }); + try { + const recipient = await getMailRecipient(profileId); + if (!recipient) return; + await sendMail({ + to: recipient.email, + ...credentialAddedMail({ name: recipient.name, serviceLabel, appUrl: await getAppUrl() }), + type: "credential", + idempotencyKey: `credential-${credentialId}`, + }); + } catch (error) { + console.error("[mail] Hinweis-Mail zum neuen Zugang fehlgeschlagen:", error); + } }); } diff --git a/modules/shared/actions/invoices.ts b/modules/shared/actions/invoices.ts index f5b2442..a38e387 100644 --- a/modules/shared/actions/invoices.ts +++ b/modules/shared/actions/invoices.ts @@ -41,34 +41,43 @@ async function deliverInvoiceMail(profileId: string, clientId: string, invoiceId if (!invoice) return { error: "Rechnung nicht gefunden." }; if (invoice.status === "cancelled") return { error: "Stornierte Rechnungen werden nicht verschickt." }; - const [pdfData] = await getInvoicesForPdf([invoiceId]); - const recipient = await getMailRecipient(profileId); - if (!pdfData || !recipient) return { error: "Rechnung oder Empfänger konnte nicht geladen werden." }; + // Die Rechnung ist beim Aufruf aus createInvoice schon gespeichert: Fehler hier dürfen nicht nach oben durchschlagen. + try { + const [pdfData] = await getInvoicesForPdf([invoiceId]); + const recipient = await getMailRecipient(profileId); + if (!pdfData || !recipient) return { error: "Rechnung oder Empfänger konnte nicht geladen werden." }; - const pdf = await renderInvoicePdf(pdfData); - const mail = invoiceMail({ - name: recipient.name, - invoiceNumber: pdfData.invoiceNumber, - invoiceDate: pdfData.invoiceDate, - dueDate: pdfData.dueDate, - grossTotal: invoiceTotals(pdfData.items).gross, - appUrl: await getAppUrl(), - }); + const [pdf, appUrl] = await Promise.all([renderInvoicePdf(pdfData), getAppUrl()]); + const mail = invoiceMail({ + name: recipient.name, + invoiceNumber: pdfData.invoiceNumber, + invoiceDate: pdfData.invoiceDate, + dueDate: pdfData.dueDate, + grossTotal: invoiceTotals(pdfData.items).gross, + appUrl, + }); - const sent = await sendMail({ - to: recipient.email, - ...mail, - type: "invoice", - attachments: [{ filename: invoiceFileName(pdfData.invoiceNumber), content: new Uint8Array(pdf), contentType: "application/pdf" }], - }); - if (!sent.ok) return { error: sent.error }; + const sent = await sendMail({ + to: recipient.email, + ...mail, + type: "invoice", + attachments: [{ filename: invoiceFileName(pdfData.invoiceNumber), content: new Uint8Array(pdf), contentType: "application/pdf" }], + }); + if (!sent.ok) return { error: sent.error }; - await supabase - .from("invoices") - .update({ email_sent_at: new Date().toISOString(), email_sent_to: recipient.email }) - .eq("id", invoiceId); + const { error: updateError } = await supabase + .from("invoices") + .update({ email_sent_at: new Date().toISOString(), email_sent_to: recipient.email }) + .eq("id", invoiceId); + if (updateError) { + console.error(`[mail] Rechnung ${invoiceId} wurde versendet, Versandstatus aber nicht gespeichert:`, updateError.message); + } - return null; + return null; + } catch (error) { + console.error(`[mail] Rechnung ${invoiceId} konnte nicht versendet werden:`, error); + return { error: "E-Mail konnte nicht gesendet werden." }; + } } export async function createInvoice(draft: InvoiceDraft, options: { sendEmail?: boolean } = {}): Promise { diff --git a/modules/shared/mail/app-url.ts b/modules/shared/mail/app-url.ts index 0a21b3e..9aa2fd2 100644 --- a/modules/shared/mail/app-url.ts +++ b/modules/shared/mail/app-url.ts @@ -1,15 +1,19 @@ import "server-only" import { headers } from "next/headers" -// Basis-URL für Links in Mails. APP_URL hat Vorrang, auf Vercel greift die Produktionsdomain, sonst der aktuelle Request. +// Basis-URL für Links in Mails. Nie aus Request-Headern in Produktion: Host/X-Forwarded-Host sind vom Client fälschbar. +// Reihenfolge: APP_URL, Vercel-Produktionsdomain, Vercel-Deployment-URL (Preview), lokal der Request-Host. export async function getAppUrl() { if (process.env.APP_URL) return process.env.APP_URL.replace(/\/$/, "") if (process.env.VERCEL_ENV === "production" && process.env.VERCEL_PROJECT_PRODUCTION_URL) { return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` } + if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}` - const headerList = await headers() - const host = headerList.get("x-forwarded-host") ?? headerList.get("host") ?? "localhost:3000" - const protocol = headerList.get("x-forwarded-proto") ?? (host.startsWith("localhost") ? "http" : "https") - return `${protocol}://${host}` + if (process.env.NODE_ENV === "development") { + const host = (await headers()).get("host") ?? "localhost:3000" + return `http://${host}` + } + + throw new Error("APP_URL ist nicht gesetzt – ohne feste Basis-URL werden keine Mail-Links erzeugt.") }