From 9265d47916e11610d61cd3ad884af4fa5c09bbc5 Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Thu, 20 Aug 2026 07:45:11 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Kontol=C3=B6schung=20schlug=20mit=20500?= =?UTF-8?q?=20fehl=20(bytes-vs-str,=20psycopg-Eigenart)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live bei der Verifikation gefunden: konto_loeschen() lud das Zielkonto über get_by_email(ziel['email']) nach - der email-Wert kam aus fetch_benutzer_fuer_organisation()'s sync-Verbindungspfad, wo psycopg TEXT-Spalten teils als bytes statt str liefert (dasselbe bereits bekannte, nicht deterministische Verhalten wie in _row_to_user(), siehe dortiger Kommentar) - die async get_by_email()-Abfrage bekam dadurch einen bytes-Parameter und scheiterte mit 'operator does not exist: text = bytea'. Zwei Fixes: konto_loeschen() nutzt jetzt user_manager.get(id) statt des E-Mail-Umwegs (nur async-Pfad, umgeht das Problem strukturell). fetch_benutzer_fuer_organisation() bekommt zusätzlich denselben defensiven bytes-Decode wie _row_to_user(), damit das nicht an anderer Stelle erneut zuschlägt. Co-Authored-By: Claude Sonnet 5 --- auth.py | 19 ++++++++++++++----- routers/organisation.py | 9 ++++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/auth.py b/auth.py index 7e2a06e..e313371 100644 --- a/auth.py +++ b/auth.py @@ -211,16 +211,25 @@ def fetch_benutzer_fuer_organisation(organization_id: str) -> list[dict]: """, (organization_id,), ) - return [ - { + ergebnis = [] + for benutzer_id, email, is_superuser, ist_organisationsadmin, created_at in cur.fetchall(): + # psycopg liefert TEXT-Spalten hier teils als bytes zurück, + # nicht deterministisch (dasselbe bekannte Verhalten wie in + # _row_to_user()) - live gefunden, als ein daraus + # weitergereichter bytes-Wert eine spätere SQL-Abfrage mit + # "operator does not exist: text = bytea" zum Absturz brachte. + if isinstance(email, bytes): + email = email.decode("utf-8") + + ergebnis.append({ "id": str(benutzer_id), "email": email, "is_superuser": is_superuser, "ist_organisationsadmin": ist_organisationsadmin, "created_at": created_at, - } - for benutzer_id, email, is_superuser, ist_organisationsadmin, created_at in cur.fetchall() - ] + }) + + return ergebnis router = APIRouter() diff --git a/routers/organisation.py b/routers/organisation.py index af3439b..3da2a6c 100644 --- a/routers/organisation.py +++ b/routers/organisation.py @@ -101,7 +101,14 @@ async def konto_loeschen( if ziel is None: raise HTTPException(status_code=404, detail="Konto wurde nicht gefunden.") - ziel_user = await user_manager.get_by_email(ziel["email"]) + # Über die ID statt per E-Mail nachladen: get_by_email() lief über den + # sync-Verbindungspfad von fetch_benutzer_fuer_organisation() zurück in + # eine async-Abfrage - psycopg liefert TEXT-Spalten dort teils als bytes + # zurück (dasselbe bekannte Verhalten wie bei _row_to_user()), was hier + # zu "operator does not exist: text = bytea" führte (live gefunden bei + # der Verifikation). user_manager.get(id) nutzt ausschließlich den + # async-Pfad (UserDatabase.get(), mit korrektem Decode in _row_to_user()). + ziel_user = await user_manager.get(user_manager.parse_id(konto_id)) await user_manager.delete(ziel_user) return RedirectResponse("/organisation", status_code=303)