see comments

This commit is contained in:
Rajpreet0 2026-10-01 13:59:20 +02:00
parent 14e1f03b66
commit c700a1b0e2
4 changed files with 68 additions and 46 deletions

View file

@ -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 }
}

View file

@ -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);
}
});
}

View file

@ -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<CreateInvoiceState> {

View file

@ -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.")
}