fix: 2FA setup paths — Pillow regression + clear TOTP key error (#838) (#852)
* fix: re-add Pillow — qrcode.make() needs it transitively for 2FA QR The unused-deps prune in #842 dropped Pillow because no Python file in backend/ has a direct `import PIL`. That was a false positive — `qrcode.make()` lazily imports `qrcode.image.pil` on its first call, which in turn does `from PIL import Image, ImageDraw`. Without Pillow installed, every 2FA enrolment crashes with: POST /api/auth/2fa/setup HTTP/1.0 500 Internal Server Error File "/opt/venv/lib/python3.11/site-packages/qrcode/image/pil.py", line 2 from PIL import Image, ImageDraw ModuleNotFoundError: No module named 'PIL' `qrcode 8.x`'s default image factory is the PIL one, used by `app/auth/services/totp.py:_generate_qr_data_uri` to produce the `data:image/png;base64,…` payload returned to the frontend. Adds Pillow to requirements.in with a comment explaining it's a transitive runtime dep that Python imports won't reveal at static audit time. pip-compile resolves to Pillow 12.2.0. Verified locally: - POST /api/auth/2fa/setup returns 200 with a valid PNG QR data URI (910 bytes), an otpauth_url, and 8 backup codes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: clear error when TOTP_ENCRYPTION_KEY is malformed (#838) When TOTP_ENCRYPTION_KEY is set to a value that isn't valid Fernet base64 (missing padding, surrounding quotes, accidental newline, truncated copy/paste), backend startup currently dies with an opaque chain: binascii.Error: Incorrect padding The above exception was the direct cause of the following exception: ValueError: Fernet key must be 32 url-safe base64-encoded bytes. …which doesn't tell the operator which env var is the problem or how to generate a valid replacement. Issue #838 reports exactly this class of confusion. Wraps the Fernet init at app/auth/services/totp.py:35 with try/except. On failure, raises a RuntimeError that names the env var, gives the expected format (32-byte url-safe base64, typically 44 chars ending with `=`), and includes the exact regen command: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" Plus an explicit hint about avoiding surrounding quotes or trailing whitespace in .env (a common cause). The underlying error is appended for diagnostic context. Defensive .strip() added when reading TOTP_ENCRYPTION_KEY — newlines or trailing spaces from .env editors no longer cause failures even if the rest of the value is correct. Verified locally: - garbage non-base64 string: surfaces the new clear RuntimeError - missing trailing `=` padding (the most likely real-world cause): surfaces the new clear RuntimeError - valid key: module loads cleanly (sanity check) Does not change behavior for correctly-configured deployments. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: taylor_socfortress <taylor.walton@socfortress.co> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>