fix: Kontolöschung schlug mit 500 fehl (bytes-vs-str, psycopg-Eigenart)

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 <noreply@anthropic.com>
This commit is contained in:
Thomas Stallinger 2026-08-20 07:45:11 +02:00
parent ec11de1ee9
commit 9265d47916
2 changed files with 22 additions and 6 deletions

19
auth.py
View File

@ -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()

View File

@ -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)