feat: implement user avatars with DiceBear integration

- Add avatar_url field to MemberSettingsUpdate schema
- Create AvatarService for generating default avatars using DiceBear
- Update auth service to generate avatars on user registration
- Add avatar upload UI to settings page
- Update settings endpoint to handle avatar URL updates
- Display current avatar in settings with upload/generate options

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mistral Vibe
2026-03-30 19:15:24 +02:00
parent 3b8c4a0cb8
commit ccafcd38af
10 changed files with 836 additions and 3 deletions

View File

@@ -54,6 +54,8 @@ async def update_settings(
updates["nc_username"] = data.nc_username or None
if data.nc_password is not None:
updates["nc_password"] = data.nc_password or None
if data.avatar_url is not None:
updates["avatar_url"] = data.avatar_url or None
if updates:
member = await repo.update(current_member, **updates)

View File

@@ -33,3 +33,4 @@ class MemberSettingsUpdate(BaseModel):
nc_url: str | None = None
nc_username: str | None = None
nc_password: str | None = None # send null to clear, omit to leave unchanged
avatar_url: str | None = None # URL to user's avatar image

View File

@@ -12,6 +12,7 @@ from rehearsalhub.config import get_settings
from rehearsalhub.db.models import Member
from rehearsalhub.repositories.member import MemberRepository
from rehearsalhub.schemas.auth import RegisterRequest, TokenResponse
from rehearsalhub.services.avatar import AvatarService
def hash_password(plain: str) -> str:
@@ -47,11 +48,22 @@ class AuthService:
async def register(self, req: RegisterRequest) -> Member:
if await self._repo.email_exists(req.email):
raise ValueError(f"Email already registered: {req.email}")
# Create member without avatar first
member = await self._repo.create(
email=req.email.lower(),
display_name=req.display_name,
password_hash=hash_password(req.password),
)
# Generate default avatar for new member
avatar_service = AvatarService()
avatar_url = await avatar_service.generate_default_avatar(member)
# Update member with avatar URL
member.avatar_url = avatar_url
await self._session.flush()
return member
async def login(self, email: str, password: str) -> TokenResponse | None:

View File

@@ -0,0 +1,54 @@
"""Avatar generation service using DiceBear API."""
from typing import Optional
import httpx
from rehearsalhub.db.models import Member
class AvatarService:
"""Service for generating and managing user avatars."""
def __init__(self):
self.base_url = "https://api.dicebear.com/v6"
async def generate_avatar_url(self, seed: str, style: str = "identicon") -> str:
"""Generate a DiceBear avatar URL for the given seed.
Args:
seed: Unique identifier (user ID, email, etc.)
style: Avatar style (default: identicon)
Returns:
URL to the generated avatar
"""
# Clean the seed for URL usage
clean_seed = seed.replace("-", "").replace("_", "")
# Construct DiceBear URL
return f"{self.base_url}/{style}/svg?seed={clean_seed}&backgroundType=gradientLinear&size=128"
async def generate_default_avatar(self, member: Member) -> str:
"""Generate a default avatar for a member using their ID as seed.
Args:
member: Member object
Returns:
URL to the generated avatar
"""
return await self.generate_avatar_url(str(member.id))
async def get_avatar_url(self, member: Member) -> Optional[str]:
"""Get the avatar URL for a member, generating default if none exists.
Args:
member: Member object
Returns:
Avatar URL or None
"""
if member.avatar_url:
return member.avatar_url
# Generate default avatar if none exists
return await self.generate_default_avatar(member)