fix: Portal-Passwort-Card im Vertragsdetail wieder sichtbar

Folge-Symptom zum PW-Save-Fix: das Speichern hat funktioniert,
aber die "Zugangsdaten"-Card im Read-Only-View hat das Passwort-
Feld nicht angezeigt. Ursache: das Frontend nutzte
`c.portalPasswordEncrypted` als Truthy-Check, aber
sanitizeContract strippt das Feld bewusst aus jeder Response
(Pentest Runde 15 - kein verschlüsselter Blob in /contracts/:id).

Fix: getContractById hängt jetzt ein virtuelles `hasPortalPassword`-
Bool-Flag an die Response. Frontend nutzt das statt
portalPasswordEncrypted. Der verschlüsselte Wert bleibt
server-seitig; der Klartext kommt weiterhin über
GET /contracts/:id/password mit Audit-Log.

Live-verifiziert: PUT setzt PW, GET liefert hasPortalPassword:true
+ portalPasswordEncrypted ist NICHT in der Response.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 02:30:54 +02:00
parent f41d1843e4
commit c9f4fcf8de
3 changed files with 14 additions and 4 deletions
+7
View File
@@ -177,6 +177,13 @@ export async function getContractById(id: number, decryptPassword = false) {
}
}
// Virtuelles Bool-Flag, damit das Frontend "PW gesetzt?" weiß, ohne dass
// der verschlüsselte Blob in die Response leakt (sanitizeContract strippt
// portalPasswordEncrypted bewusst). Pentest Runde 15 sensitive Feld
// raus aus /contracts/:id; UI nutzt jetzt `hasPortalPassword`.
(contract as Record<string, unknown>).hasPortalPassword =
!!contract.portalPasswordEncrypted;
return contract;
}
@@ -2399,7 +2399,7 @@ export default function ContractDetail() {
</div>
{/* Portal Credentials */}
{(c.portalUsername || c.stressfreiEmail || c.portalPasswordEncrypted) && (
{(c.portalUsername || c.stressfreiEmail || c.hasPortalPassword) && (
<Card className="mb-6" title="Zugangsdaten">
<dl className="grid grid-cols-2 gap-4">
{(c.portalUsername || c.stressfreiEmail) && (
@@ -2416,7 +2416,7 @@ export default function ContractDetail() {
</dd>
</div>
)}
{c.portalPasswordEncrypted && (
{c.hasPortalPassword && (
<div>
<dt className="text-sm text-gray-500">Passwort</dt>
<dd className="flex items-center gap-2">
@@ -2435,7 +2435,7 @@ export default function ContractDetail() {
</dl>
{/* Auto-Login Button */}
{c.provider?.portalUrl && (c.portalUsername || c.stressfreiEmail) && c.portalPasswordEncrypted && (
{c.provider?.portalUrl && (c.portalUsername || c.stressfreiEmail) && c.hasPortalPassword && (
<div className="mt-4 pt-4 border-t">
<Button
onClick={handleAutoLogin}
+4 -1
View File
@@ -429,7 +429,10 @@ export interface Contract {
contractDuration?: ContractDuration;
commission?: number;
portalUsername?: string;
portalPasswordEncrypted?: string;
/** Backend liefert nur ein Bool-Flag der verschlüsselte Wert selbst
* bleibt server-seitig (sanitizeContract strippt `portalPasswordEncrypted`).
* Entschlüsselter Wert kommt über `GET /contracts/:id/password`. */
hasPortalPassword?: boolean;
notes?: string;
// Kündigungsdokumente
cancellationLetterPath?: string;