refactor: unify storage config keys and refresh local dev setup
This commit is contained in:
@@ -11,11 +11,13 @@ from core.agentscope.caches.user_context_cache import (
|
||||
create_user_context_cache,
|
||||
)
|
||||
from core.auth.models import CurrentUser
|
||||
from core.config.settings import config
|
||||
from core.db.base_service import BaseService
|
||||
from core.logging import get_logger
|
||||
from schemas.shared.user import UserContext, parse_profile_settings
|
||||
from services.base.supabase import supabase_service
|
||||
from v1.users.repository import UserRepository
|
||||
from v1.users.schemas import UserSearchRequest, UserUpdateRequest
|
||||
from v1.users.schemas import AvatarUploadResponse, UserSearchRequest, UserUpdateRequest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -27,6 +29,16 @@ logger = get_logger("v1.users.service")
|
||||
_PHONE_QUERY_PATTERN = re.compile(r"^[+()\-\s\d]{4,32}$")
|
||||
|
||||
|
||||
def _mime_to_suffix(mime_type: str) -> str:
|
||||
"""Convert MIME type to file suffix."""
|
||||
mapping = {
|
||||
"image/jpeg": "jpg",
|
||||
"image/png": "png",
|
||||
"image/webp": "webp",
|
||||
}
|
||||
return mapping.get(mime_type, "bin")
|
||||
|
||||
|
||||
class AuthLookupGateway(Protocol):
|
||||
async def search_user_ids_by_phone(
|
||||
self, query: str, limit: int = 20
|
||||
@@ -164,6 +176,83 @@ class UserService(BaseService):
|
||||
settings=parse_profile_settings(user.settings),
|
||||
)
|
||||
|
||||
async def upload_avatar(
|
||||
self,
|
||||
*,
|
||||
filename: str | None,
|
||||
content_type: str | None,
|
||||
payload: bytes,
|
||||
) -> AvatarUploadResponse:
|
||||
user_id = self.require_user_id()
|
||||
|
||||
if not isinstance(content_type, str):
|
||||
raise HTTPException(status_code=422, detail="Unsupported image type")
|
||||
|
||||
mime_type = content_type.lower()
|
||||
allowed_types = {"image/jpeg", "image/png", "image/webp"}
|
||||
if mime_type not in allowed_types:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail="Unsupported image type. Allowed: JPEG, PNG, WebP",
|
||||
)
|
||||
|
||||
max_size_bytes = config.storage.avatar.max_size_mb * 1024 * 1024
|
||||
if len(payload) > max_size_bytes:
|
||||
raise HTTPException(
|
||||
status_code=413,
|
||||
detail=f"Image too large. Maximum size: {config.storage.avatar.max_size_mb}MB",
|
||||
)
|
||||
|
||||
if not payload:
|
||||
raise HTTPException(status_code=422, detail="Empty image")
|
||||
|
||||
suffix = _mime_to_suffix(mime_type)
|
||||
path = f"{user_id}/avatar.{suffix}"
|
||||
bucket_name = config.storage.avatar.bucket
|
||||
|
||||
try:
|
||||
stored_path = await supabase_service.upload_bytes(
|
||||
bucket=bucket_name,
|
||||
path=path,
|
||||
content=payload,
|
||||
content_type=mime_type,
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.exception(
|
||||
"Avatar upload failed",
|
||||
extra={
|
||||
"bucket": bucket_name,
|
||||
"path": path,
|
||||
"mime_type": mime_type,
|
||||
"user_id": str(user_id),
|
||||
},
|
||||
)
|
||||
raise HTTPException(status_code=502, detail="Failed to upload avatar")
|
||||
|
||||
public_url = f"{config.supabase.public_url}/storage/v1/object/public/{bucket_name}/{stored_path}"
|
||||
|
||||
update_data: dict[str, str | None] = {"avatar_url": public_url}
|
||||
try:
|
||||
user = await self._repository.update_by_user_id(user_id, update_data)
|
||||
await self._session.commit()
|
||||
except SQLAlchemyError:
|
||||
await self._session.rollback()
|
||||
raise HTTPException(status_code=503, detail="User store unavailable")
|
||||
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
try:
|
||||
await self._user_context_cache.invalidate_user(user_id=user_id)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to invalidate user context cache after avatar upload",
|
||||
user_id=str(user_id),
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
return AvatarUploadResponse(url=public_url)
|
||||
|
||||
async def get_by_username(self, username: str) -> UserContext:
|
||||
try:
|
||||
user = await self._repository.get_by_username(username)
|
||||
|
||||
Reference in New Issue
Block a user