feat(auth): Authentik OIDC sign-in + Gravatar avatars

Adds optional SSO via Authentik (or any OIDC provider) alongside the
existing password flow, and pulls profile images from the provider's
`picture` claim or Gravatar so the sharing UI stops looking anonymous.
Password login stays available as a recovery path; JIT provisioning and
admin-group mapping are env-configurable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 21:06:32 +02:00
parent 319be20389
commit e8e1adcf37
20 changed files with 852 additions and 60 deletions

View File

@@ -0,0 +1,28 @@
"""Gravatar URL helper.
Pure function — no HTTP calls. The browser does the actual image
fetch. We just build the deterministic URL from the user's email and
let Gravatar serve an identicon when no account exists for that hash,
so the avatar is never a broken image.
Current Gravatar guidance is SHA-256 of the trimmed, lower-cased email.
MD5 still works but is deprecated, so we prefer SHA-256.
"""
import hashlib
from typing import Optional
def gravatar_url(email: Optional[str], size: int = 240) -> Optional[str]:
"""Return a Gravatar image URL for `email`, or None when email is empty.
The `d=identicon` fallback guarantees a deterministic placeholder when
the address has no Gravatar account, so callers can treat the result
as a valid image URL.
"""
if not email:
return None
normalized = email.strip().lower()
if not normalized:
return None
digest = hashlib.sha256(normalized.encode("utf-8")).hexdigest()
return f"https://gravatar.com/avatar/{digest}?d=identicon&s={size}"