fix(stressfrei): sync-forwarding sichtbar + Passwort-Push + Toast-Meldungen
Drei Verbesserungen am gestrigen Sync-Feature:
1. Bug-Fix: isProvisioned wurde nie auf true gesetzt
`createEmail` mit `provisionAtProvider: true` hat das Flag
`isProvisioned` nie gesetzt → blieb auf @default(false). Damit
blieb der Refresh-Button in der UI unsichtbar (Bedingung
`emailItem.isProvisioned`). Jetzt:
- createEmail setzt isProvisioned + provisionedAt korrekt
- Self-Healing: syncForwardingForEmail setzt das Flag nachträglich
auf true sobald der Provider-Aufruf erfolgreich war (Backfill
für historisch falsch markierte Einträge)
- UI-Sichtbarkeit: Bedingung entfernt – der Button erscheint jetzt
immer; ein Klick auf eine nicht-provisionierte Adresse liefert
eine sprechende Fehlermeldung statt stiller Verstecken
2. Passwort-Push bei hasMailbox: true
Bisher wurden nur die Forwards aktualisiert. Jetzt entschlüsselt
syncForwardingForEmail bei Mailbox-Adressen zusätzlich das im CRM
gespeicherte Passwort und setzt es am Provider neu – Self-Healing
für IMAP/SMTP-Logins falls jemand im Plesk-UI manuell ein anderes
Passwort gesetzt hat. Response enthält `passwordReset: true` als
Marker.
3. react-hot-toast statt alert()
Erfolgs-Toast listet die neu gesetzten Forward-Targets + Hinweis
ob Passwort-Reset durchgeführt wurde. Fehler-Toast zeigt die
Backend-Fehlermeldung (z.B. „E-Mail-Adresse beim Provider nicht
gefunden – wurde sie dort gelöscht?").
Audit-Log-Label enthält jetzt sowohl Forwards als auch Passwort-Reset-
Marker, damit der Vorgang im AuditLog nachvollziehbar bleibt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -114,12 +114,15 @@ export async function syncForwarding(req: AuthRequest, res: Response): Promise<v
|
||||
return;
|
||||
}
|
||||
|
||||
const labelParts = [`Weiterleitungen: ${(result.forwardTargets || []).join(', ')}`];
|
||||
if (result.passwordReset) labelParts.push('Mailbox-Passwort am Provider neu gesetzt');
|
||||
|
||||
await logChange({
|
||||
req,
|
||||
action: 'UPDATE',
|
||||
resourceType: 'StressfreiEmail',
|
||||
resourceId: emailId.toString(),
|
||||
label: `Weiterleitungen synchronisiert: ${(result.forwardTargets || []).join(', ')}`,
|
||||
label: `Stressfrei-Sync: ${labelParts.join(' | ')}`,
|
||||
});
|
||||
|
||||
res.json({
|
||||
@@ -127,6 +130,7 @@ export async function syncForwarding(req: AuthRequest, res: Response): Promise<v
|
||||
data: {
|
||||
forwardTargets: result.forwardTargets,
|
||||
customerEmail: result.customerEmail,
|
||||
passwordReset: result.passwordReset,
|
||||
},
|
||||
message: 'Weiterleitungen aktualisiert',
|
||||
} as ApiResponse);
|
||||
|
||||
@@ -115,6 +115,8 @@ export async function createEmail(data: CreateEmailData) {
|
||||
...emailData,
|
||||
isActive: true,
|
||||
hasMailbox: true,
|
||||
isProvisioned: true,
|
||||
provisionedAt: new Date(),
|
||||
emailPasswordEncrypted: passwordEncrypted,
|
||||
},
|
||||
});
|
||||
@@ -133,6 +135,11 @@ export async function createEmail(data: CreateEmailData) {
|
||||
...emailData,
|
||||
isActive: true,
|
||||
hasMailbox: createMailbox || false,
|
||||
// Provisioned-Flag nur setzen wenn Provider-Aufruf gerade lief (oder
|
||||
// die Mail bei Plesk schon existierte und der „existiert bereits"-Pfad
|
||||
// gegriffen hat).
|
||||
isProvisioned: !!provisionAtProvider,
|
||||
provisionedAt: provisionAtProvider ? new Date() : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -257,27 +264,38 @@ export async function getDecryptedPassword(id: number): Promise<string | null> {
|
||||
// Stamm-E-Mail des Kunden). Ersetzt alle bestehenden Forwards durch
|
||||
// [aktuelle Kunden-E-Mail, defaultForwardEmail aus Provider-Config].
|
||||
//
|
||||
// Wenn die Adresse `hasMailbox` ist: setzt zusätzlich das im CRM verschlüsselt
|
||||
// hinterlegte Passwort am Provider neu (Use-Case: Plesk-Restore, manueller
|
||||
// Eingriff im Plesk-UI etc. – CRM und Provider können sich entkoppeln, sodass
|
||||
// IMAP/SMTP-Logins im CRM nicht mehr passen). Self-Healing.
|
||||
//
|
||||
// Idempotent: das Plesk-CLI `set:` überschreibt die Adressliste komplett, kein
|
||||
// Duplikat-Risiko bei Mehrfachaufruf.
|
||||
// Duplikat-Risiko bei Mehrfachaufruf. Wenn die Operation erfolgreich war wird
|
||||
// das `isProvisioned`-Flag automatisch auf `true` gezogen (historische
|
||||
// Einträge, bei denen das Flag nie gesetzt wurde, werden so geheilt).
|
||||
export async function syncForwardingForEmail(
|
||||
id: number,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
forwardTargets?: string[];
|
||||
customerEmail?: string;
|
||||
passwordReset?: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
|
||||
where: { id },
|
||||
select: { email: true, customerId: true, isProvisioned: true },
|
||||
select: {
|
||||
email: true,
|
||||
customerId: true,
|
||||
isProvisioned: true,
|
||||
hasMailbox: true,
|
||||
emailPasswordEncrypted: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!stressfreiEmail) {
|
||||
return { success: false, error: 'StressfreiEmail nicht gefunden' };
|
||||
}
|
||||
if (!stressfreiEmail.isProvisioned) {
|
||||
return { success: false, error: 'E-Mail ist nicht beim Provider angelegt – Sync nicht möglich' };
|
||||
}
|
||||
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: stressfreiEmail.customerId },
|
||||
@@ -294,15 +312,65 @@ export async function syncForwardingForEmail(
|
||||
}
|
||||
|
||||
const localPart = stressfreiEmail.email.split('@')[0];
|
||||
const result = await setEmailForwardTargets(localPart, forwardTargets);
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Provider-Update fehlgeschlagen' };
|
||||
|
||||
// 1) Forwards neu setzen.
|
||||
const forwardResult = await setEmailForwardTargets(localPart, forwardTargets);
|
||||
if (!forwardResult.success) {
|
||||
// Wenn Plesk meldet „nicht gefunden", liefern wir eine sprechende Meldung
|
||||
// statt der rohen Provider-Nachricht.
|
||||
const err = forwardResult.error || 'Provider-Update fehlgeschlagen';
|
||||
const friendly = /not\s*found|nicht\s*gefunden/i.test(err)
|
||||
? 'E-Mail-Adresse beim Provider nicht gefunden – wurde sie dort gelöscht?'
|
||||
: err;
|
||||
return { success: false, error: friendly };
|
||||
}
|
||||
|
||||
// 2) Wenn Mailbox: Passwort aus CRM-Speicher entschlüsseln und am Provider
|
||||
// neu setzen (Self-Healing nach Provider-seitigen Änderungen).
|
||||
let passwordReset = false;
|
||||
if (stressfreiEmail.hasMailbox && stressfreiEmail.emailPasswordEncrypted) {
|
||||
try {
|
||||
const password = decrypt(stressfreiEmail.emailPasswordEncrypted);
|
||||
const pwResult = await updateMailboxPassword(localPart, password);
|
||||
if (!pwResult.success) {
|
||||
// Forwards waren schon erfolgreich – wir geben Forward-Erfolg + Passwort-
|
||||
// Fehler kombiniert zurück, statt die ganze Operation rot zu machen.
|
||||
return {
|
||||
success: false,
|
||||
forwardTargets,
|
||||
customerEmail: customer.email,
|
||||
error:
|
||||
'Weiterleitungen aktualisiert, aber Passwort-Sync fehlgeschlagen: ' +
|
||||
(pwResult.error || 'unbekannt'),
|
||||
};
|
||||
}
|
||||
passwordReset = true;
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
forwardTargets,
|
||||
customerEmail: customer.email,
|
||||
error:
|
||||
'Weiterleitungen aktualisiert, aber Passwort konnte nicht entschlüsselt werden – ' +
|
||||
'evtl. wurde der ENCRYPTION_KEY rotiert',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Self-Healing: nach erfolgreichem Provider-Aufruf wissen wir definitiv,
|
||||
// dass die Adresse beim Provider existiert → Flag korrigieren.
|
||||
if (!stressfreiEmail.isProvisioned) {
|
||||
await prisma.stressfreiEmail.update({
|
||||
where: { id },
|
||||
data: { isProvisioned: true, provisionedAt: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
forwardTargets,
|
||||
customerEmail: customer.email,
|
||||
passwordReset,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user