Initial commit: site monorepo with API, web, and infra.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
влад
2026-07-16 10:11:54 +03:00
co-authored by Cursor
commit 016910ffb7
447 changed files with 73972 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import Response
from app.core.media_signing import verify_signed_media
from app.core.storage import download_object
router = APIRouter()
@router.get("/files/{file_path:path}")
async def get_media_file(
file_path: str,
expires: int = Query(...),
sig: str = Query(...),
):
if not file_path.startswith("avatars/"):
raise HTTPException(status_code=404, detail="FILE_NOT_FOUND")
if not verify_signed_media(file_path, expires, sig):
raise HTTPException(status_code=403, detail="INVALID_SIGNATURE")
stored = download_object(file_path)
if not stored:
raise HTTPException(status_code=404, detail="FILE_NOT_FOUND")
body, content_type = stored
return Response(content=body, media_type=content_type)
+5
View File
@@ -0,0 +1,5 @@
from pydantic import BaseModel
class AvatarUploadOut(BaseModel):
message: str
+62
View File
@@ -0,0 +1,62 @@
from __future__ import annotations
from io import BytesIO
from uuid import uuid4
from PIL import Image
from app.core.config import settings
from app.core.storage import upload_object
ALLOWED_MIME = {
"image/jpeg": ".jpg",
"image/png": ".png",
"image/webp": ".webp",
}
FORMAT_TO_MIME = {
"JPEG": "image/jpeg",
"PNG": "image/png",
"WEBP": "image/webp",
}
class AvatarValidationError(ValueError):
pass
def validate_and_process_avatar(content: bytes) -> tuple[str, bytes]:
if len(content) > settings.avatar_max_bytes:
raise AvatarValidationError("FILE_TOO_LARGE")
if not content:
raise AvatarValidationError("INVALID_IMAGE")
try:
with Image.open(BytesIO(content)) as image:
image.verify()
with Image.open(BytesIO(content)) as image:
mime = FORMAT_TO_MIME.get(image.format or "")
if mime not in ALLOWED_MIME:
raise AvatarValidationError("INVALID_MIME")
buffer = BytesIO()
if mime == "image/jpeg":
rgb = image.convert("RGB")
rgb.save(buffer, format="JPEG", quality=85, optimize=True)
elif mime == "image/png":
image.save(buffer, format="PNG", optimize=True)
else:
image.save(buffer, format="WEBP", quality=85, method=6)
return mime, buffer.getvalue()
except AvatarValidationError:
raise
except Exception as exc:
raise AvatarValidationError("INVALID_IMAGE") from exc
def upload_user_avatar(user_id: str, content: bytes) -> str:
mime, processed = validate_and_process_avatar(content)
extension = ALLOWED_MIME[mime]
key = f"avatars/{user_id}/{uuid4()}{extension}"
upload_object(key, processed, mime)
return f"/api/v1/media/files/{key}"