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:
@@ -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,
|
||||
|
||||
@@ -97,6 +97,20 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
||||
|
||||
## ✅ Erledigt
|
||||
|
||||
- [x] **🐞 Stressfrei-Adressen: doppelte E-Mails beim Anlegen erlaubt**
|
||||
- Bug: User konnte dieselbe Adresse zweimal beim selben Kunden
|
||||
anlegen (siehe Screenshot mit 2× `max.mustermann@...`). `createEmail`
|
||||
hatte keinen Duplikatscheck, `updateEmail` ebenfalls nicht.
|
||||
- Service: Vor `prisma.create` jetzt `findFirst` auf
|
||||
`(customerId, email)` (case-insensitive). Bei Treffer → `ApiError(409)`.
|
||||
Unterschiedliche Meldung für aktive vs. inaktive Duplikate
|
||||
(Hinweis bei inaktiv: alten Eintrag reaktivieren statt neu anlegen).
|
||||
- `updateEmail`: gleicher Check beim Umbenennen, mit `NOT id`-Exclude.
|
||||
- Controller: `catch`-Blöcke honorieren jetzt den `ApiError.statusCode`
|
||||
(vorher pauschal 400) → 409 kommt sauber durch.
|
||||
- Frontend: `updateMutation` bekam ein `onError`, damit der 409 nicht
|
||||
nur ins Leere lief.
|
||||
|
||||
- [x] **🔒 Pentest 71.1–71.4: Härtung der Zusatz-Weiterleitungen**
|
||||
- **71.1 MEDIUM:** Reservierte/private TLDs (`local`, `internal`,
|
||||
`corp`, `lan`, `home`, `private`, `invalid`, `test`, `localhost`,
|
||||
|
||||
@@ -3960,6 +3960,9 @@ function StressfreiEmailModal({
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
onClose();
|
||||
},
|
||||
onError: (err) => {
|
||||
setProvisionError(err instanceof Error ? err.message : 'Fehler beim Speichern');
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
|
||||
Reference in New Issue
Block a user