Hamdy005 commited on
Commit
6594ddb
·
1 Parent(s): 6c898a6

feat: implement profile avatar upload functionality to Supabase storage

Browse files
Files changed (2) hide show
  1. auth/routes.py +65 -2
  2. main.py +13 -1
auth/routes.py CHANGED
@@ -1,17 +1,80 @@
1
  import uuid
2
- from fastapi import APIRouter, HTTPException
3
  from fastapi import Depends
4
  from typing import Optional
5
 
6
- from src.database import get_auth_supabase
7
  from src.store import create_user, get_user_by_email, delete_user_data, update_user_profile, get_user_by_id
8
  from src.dependencies import get_current_user_id, get_current_user
9
  from .schemas import ProfileUpdateRequest
10
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  PLACEHOLDER_DOMAINS = ["@placeholder.ai", "@studymate.ai"]
12
 
13
  router = APIRouter(prefix="/api/auth", tags=["Auth"])
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  @router.delete("/me")
16
  async def delete_account(user_id: str = Depends(get_current_user_id)):
17
  delete_user_data(user_id)
 
1
  import uuid
2
+ from fastapi import APIRouter, HTTPException, UploadFile, File
3
  from fastapi import Depends
4
  from typing import Optional
5
 
6
+ from src.database import get_auth_supabase, get_supabase
7
  from src.store import create_user, get_user_by_email, delete_user_data, update_user_profile, get_user_by_id
8
  from src.dependencies import get_current_user_id, get_current_user
9
  from .schemas import ProfileUpdateRequest
10
 
11
+ ALLOWED_MIME_TYPES = {
12
+ "image/jpeg",
13
+ "image/jpg",
14
+ "image/png",
15
+ "image/webp",
16
+ "image/gif",
17
+ "image/avif",
18
+ "image/svg+xml",
19
+ }
20
+ MAX_FILE_SIZE_BYTES = 6 * 1024 * 1024 # 6 MB
21
+ AVATAR_BUCKET = "avatars"
22
+
23
  PLACEHOLDER_DOMAINS = ["@placeholder.ai", "@studymate.ai"]
24
 
25
  router = APIRouter(prefix="/api/auth", tags=["Auth"])
26
 
27
+
28
+ @router.post("/upload-avatar")
29
+ async def upload_avatar(
30
+ file: UploadFile = File(...),
31
+ user_id: str = Depends(get_current_user_id),
32
+ ):
33
+ """Upload a profile avatar image to Supabase Storage and return its public URL."""
34
+
35
+ # 1. Validate MIME type
36
+ if file.content_type not in ALLOWED_MIME_TYPES:
37
+ raise HTTPException(
38
+ 400,
39
+ f"Unsupported file type '{file.content_type}'. "
40
+ f"Allowed types: {', '.join(sorted(ALLOWED_MIME_TYPES))}",
41
+ )
42
+
43
+ # 2. Read file bytes and validate size
44
+ content = await file.read()
45
+ if len(content) > MAX_FILE_SIZE_BYTES:
46
+ raise HTTPException(
47
+ 400,
48
+ f"File is too large ({len(content) // 1024} KB). Maximum allowed size is 6 MB.",
49
+ )
50
+
51
+ # 3. Build a unique storage path: avatars/<user_id>/<uuid>.<ext>
52
+ ext = (file.filename or "image").rsplit(".", 1)[-1].lower()
53
+ if ext not in {"jpg", "jpeg", "png", "webp", "gif", "avif", "svg"}:
54
+ ext = "jpg" # safe fallback
55
+ storage_path = f"{user_id}/{uuid.uuid4()}.{ext}"
56
+
57
+ # 4. Upload to Supabase Storage using the service-role client
58
+ client = get_supabase()
59
+ if client is None:
60
+ raise HTTPException(503, "Storage service unavailable")
61
+
62
+ try:
63
+ client.storage.from_(AVATAR_BUCKET).upload(
64
+ path=storage_path,
65
+ file=content,
66
+ file_options={"content-type": file.content_type, "upsert": "true"},
67
+ )
68
+ except Exception as e:
69
+ raise HTTPException(500, f"Failed to upload avatar: {e}")
70
+
71
+ # 5. Get the public URL from the bucket
72
+ public_url_resp = client.storage.from_(AVATAR_BUCKET).get_public_url(storage_path)
73
+ public_url = public_url_resp if isinstance(public_url_resp, str) else str(public_url_resp)
74
+
75
+ return {"status": "success", "avatar_url": public_url}
76
+
77
+
78
  @router.delete("/me")
79
  async def delete_account(user_id: str = Depends(get_current_user_id)):
80
  delete_user_data(user_id)
main.py CHANGED
@@ -1,6 +1,13 @@
1
  import logging
2
  import logging.handlers
3
  import os
 
 
 
 
 
 
 
4
  from contextlib import asynccontextmanager
5
  from fastapi import FastAPI, Depends
6
  from fastapi.middleware.cors import CORSMiddleware
@@ -60,7 +67,7 @@ async def lifespan(app: FastAPI):
60
  logger.warning(f"English ASR model failed to load: {e}")
61
 
62
  try:
63
- from src.asr.models import get_audio_model_ar
64
  get_audio_model_ar()
65
  except Exception as e:
66
  logger.warning(f"Arabic ASR model failed to load: {e}")
@@ -118,3 +125,8 @@ async def health_check():
118
  @app.get("/api/usage")
119
  async def get_user_usage(user_id: str = Depends(get_current_user_id)):
120
  return get_usage(user_id)
 
 
 
 
 
 
1
  import logging
2
  import logging.handlers
3
  import os
4
+ import sys
5
+
6
+ current_dir = os.path.dirname(os.path.abspath(__file__))
7
+ parent_dir = os.path.dirname(current_dir)
8
+ if parent_dir not in sys.path:
9
+ sys.path.insert(0, parent_dir)
10
+
11
  from contextlib import asynccontextmanager
12
  from fastapi import FastAPI, Depends
13
  from fastapi.middleware.cors import CORSMiddleware
 
67
  logger.warning(f"English ASR model failed to load: {e}")
68
 
69
  try:
70
+ from src.asr.models import get_audio_model_ard
71
  get_audio_model_ar()
72
  except Exception as e:
73
  logger.warning(f"Arabic ASR model failed to load: {e}")
 
125
  @app.get("/api/usage")
126
  async def get_user_usage(user_id: str = Depends(get_current_user_id)):
127
  return get_usage(user_id)
128
+
129
+
130
+ if __name__ == "__main__":
131
+ import uvicorn
132
+ uvicorn.run("src.main:app", host="0.0.0.0", port=8000, reload=True)