refactor: unify storage config keys and refresh local dev setup

This commit is contained in:
qzl
2026-03-26 13:25:25 +08:00
parent b765b9e3e1
commit 5900993ee7
61 changed files with 1164 additions and 129 deletions
+90 -1
View File
@@ -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)