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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
54
api/src/rehearsalhub/services/avatar.py
Normal file
54
api/src/rehearsalhub/services/avatar.py
Normal 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)
|
||||
Reference in New Issue
Block a user