Stressfrei-Adressen: Duplikate beim Anlegen ablehnen

Bug: dieselbe E-Mail-Adresse konnte beim selben Kunden mehrfach
angelegt werden – im Screenshot zwei identische Einträge nach
einem Doppel-Submit.

- createEmail: findFirst auf (customerId, email) case-insensitive,
  bei Treffer ApiError(409). Eigene Meldung für inaktive
  Duplikate (Hinweis: alten Eintrag reaktivieren statt neu anlegen).
- updateEmail: gleicher Check beim Umbenennen, NOT id-Exclude.
- Controller: catch-Blöcke honorieren ApiError.statusCode (vorher
  pauschal 400) → 409 kommt sauber an die UI durch.
- Frontend: updateMutation bekam onError, damit der Fehler nicht
  schlucken bleibt.
This commit is contained in:
2026-06-18 14:01:35 +02:00
parent 246999be01
commit 8992bb7a5d
4 changed files with 70 additions and 2 deletions
@@ -84,7 +84,8 @@ export async function createEmail(req: Request, res: Response): Promise<void> {
});
res.status(201).json({ success: true, data: email } as ApiResponse);
} catch (error) {
res.status(400).json({
const status = error instanceof ApiError ? error.statusCode : 400;
res.status(status).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Stressfrei-Wechseln Adresse',
} as ApiResponse);
@@ -104,7 +105,8 @@ export async function updateEmail(req: AuthRequest, res: Response): Promise<void
});
res.json({ success: true, data: email } as ApiResponse);
} catch (error) {
res.status(400).json({
const status = error instanceof ApiError ? error.statusCode : 400;
res.status(status).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Stressfrei-Wechseln Adresse',
} as ApiResponse);
@@ -152,6 +152,27 @@ export interface CreateEmailData {
export async function createEmail(data: CreateEmailData) {
const { provisionAtProvider, createMailbox, ...emailData } = data;
// Duplikatscheck: pro Kunde darf eine Stressfrei-Adresse nur einmal
// existieren. Case-insensitive, weil RFC-localpart-Großschreibung in
// der Praxis nie semantischen Unterschied macht und der Provider eh
// einheitlich lowercased.
const normalized = data.email.trim().toLowerCase();
const conflict = await prisma.stressfreiEmail.findFirst({
where: {
customerId: data.customerId,
email: { equals: normalized },
},
select: { id: true, isActive: true },
});
if (conflict) {
const hint = conflict.isActive
? 'Diese E-Mail-Adresse ist bei diesem Kunden bereits angelegt.'
: 'Diese E-Mail-Adresse existiert bei diesem Kunden bereits (deaktiviert). Bitte reaktiviere den vorhandenen Eintrag, statt einen neuen anzulegen.';
throw new ApiError(409, hint);
}
// Wert in DB ist eh schon lowercase wir setzen es einheitlich.
emailData.email = normalized;
// Falls beim Provider anlegen gewünscht
if (provisionAtProvider) {
// Kunde laden für Weiterleitung
@@ -222,6 +243,34 @@ export async function updateEmail(
isActive?: boolean;
}
) {
// Beim Umbenennen der E-Mail-Adresse erneut Kollisionscheck. Sonst
// kann man eine zweite Adresse mit identischer Mail beim selben Kunden
// anlegen (Umweg um den Create-Check).
if (typeof data.email === 'string' && data.email.trim() !== '') {
const normalized = data.email.trim().toLowerCase();
const current = await prisma.stressfreiEmail.findUnique({
where: { id },
select: { customerId: true, email: true },
});
if (!current) {
throw new ApiError(404, 'StressfreiEmail nicht gefunden');
}
if (normalized !== current.email.toLowerCase()) {
const conflict = await prisma.stressfreiEmail.findFirst({
where: {
customerId: current.customerId,
email: { equals: normalized },
NOT: { id },
},
select: { id: true },
});
if (conflict) {
throw new ApiError(409, 'Diese E-Mail-Adresse ist bei diesem Kunden bereits angelegt.');
}
}
data.email = normalized;
}
return prisma.stressfreiEmail.update({
where: { id },
data,