fix(i18n): write each refusal where it is sent, and read only that form

A refusal's detail now stands where it is sent (HTTPException's detail,
a JSONResponse's "detail") or is returned by a factory that only ever is,
with any spread before its code and reason, so no spread can replace
them. The scan follows no variable and no kwargs any more; what it cannot
read in a reason (a lambda's or comprehension's name, a nonlocal or
global, a local that reaches itself, a constant or map changed elsewhere,
an app import it cannot resolve) is reported. The detail guard reads the
same slots and fails on anything but a code dict or a factory's call.
This commit is contained in:
2026-10-31 23:54:55 +01:00
parent f8a8e905da
commit 31a75f4985
6 changed files with 546 additions and 515 deletions
+9 -9
View File
@@ -1001,15 +1001,15 @@ def _login_refused(
`email_verified` is false, and vice versa — the SPA renders whichever is actually outstanding
rather than assuming a sequence.
"""
detail: dict = {"code": "login", "reason": reason}
if user is not None:
detail |= {
"email": user.email,
"email_verified": user.email_verified,
"approved": user.is_active,
}
detail |= {k: v for k, v in extra.items() if v is not None}
return HTTPException(status_code=status, detail=detail)
account = (
{"email": user.email, "email_verified": user.email_verified, "approved": user.is_active}
if user is not None
else {}
)
given = {k: v for k, v in extra.items() if v is not None}
return HTTPException(
status_code=status, detail={**account, **given, "code": "login", "reason": reason}
)
@router.post("/password-login")