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);
|
res.status(201).json({ success: true, data: email } as ApiResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({
|
const status = error instanceof ApiError ? error.statusCode : 400;
|
||||||
|
res.status(status).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Stressfrei-Wechseln Adresse',
|
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Stressfrei-Wechseln Adresse',
|
||||||
} as ApiResponse);
|
} as ApiResponse);
|
||||||
@@ -104,7 +105,8 @@ export async function updateEmail(req: AuthRequest, res: Response): Promise<void
|
|||||||
});
|
});
|
||||||
res.json({ success: true, data: email } as ApiResponse);
|
res.json({ success: true, data: email } as ApiResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({
|
const status = error instanceof ApiError ? error.statusCode : 400;
|
||||||
|
res.status(status).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Stressfrei-Wechseln Adresse',
|
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Stressfrei-Wechseln Adresse',
|
||||||
} as ApiResponse);
|
} as ApiResponse);
|
||||||
|
|||||||
@@ -152,6 +152,27 @@ export interface CreateEmailData {
|
|||||||
export async function createEmail(data: CreateEmailData) {
|
export async function createEmail(data: CreateEmailData) {
|
||||||
const { provisionAtProvider, createMailbox, ...emailData } = data;
|
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
|
// Falls beim Provider anlegen gewünscht
|
||||||
if (provisionAtProvider) {
|
if (provisionAtProvider) {
|
||||||
// Kunde laden für Weiterleitung
|
// Kunde laden für Weiterleitung
|
||||||
@@ -222,6 +243,34 @@ export async function updateEmail(
|
|||||||
isActive?: boolean;
|
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({
|
return prisma.stressfreiEmail.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -97,6 +97,20 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ 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**
|
- [x] **🔒 Pentest 71.1–71.4: Härtung der Zusatz-Weiterleitungen**
|
||||||
- **71.1 MEDIUM:** Reservierte/private TLDs (`local`, `internal`,
|
- **71.1 MEDIUM:** Reservierte/private TLDs (`local`, `internal`,
|
||||||
`corp`, `lan`, `home`, `private`, `invalid`, `test`, `localhost`,
|
`corp`, `lan`, `home`, `private`, `invalid`, `test`, `localhost`,
|
||||||
|
|||||||
@@ -3960,6 +3960,9 @@ function StressfreiEmailModal({
|
|||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
setProvisionError(err instanceof Error ? err.message : 'Fehler beim Speichern');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user