Hamdy005 commited on
Commit
dc64ef1
·
1 Parent(s): bb87189

feat: implement profile caching and optimize database client initialization.

Browse files
auth/routes.py CHANGED
@@ -28,50 +28,68 @@ async def get_profile(
28
  current_user=Depends(get_current_user)
29
  ):
30
  user = get_user_by_id(user_id)
31
-
32
- # Identify if the email is a temporary placeholder
33
  email = user.get("email", "") if user else ""
34
- is_placeholder = not user or "@placeholder.ai" in email or "@studymate.ai" in email or "user" in email
35
-
 
 
 
 
 
36
  if is_placeholder:
37
- # Try to get DEFINITELY REAL data from Supabase Auth using the service role client
38
  from src.database import get_supabase
39
  supabase = get_supabase()
40
  if supabase:
41
  try:
42
- # Use admin API to get real user data
43
  res = supabase.auth.admin.get_user_by_id(user_id)
44
  if res.user and res.user.email and "@" in res.user.email:
45
  real_email = res.user.email
46
  real_name = res.user.user_metadata.get("name")
47
-
48
- # Use upsert to create or update the profile with real data
49
  from src.store import _table_supabase, _map_profile
50
  data = {"id": user_id, "email": real_email}
51
  if real_name:
52
  data["display_name"] = real_name
53
-
54
  try:
55
- res_upd = _table_supabase("profiles").upsert(data).execute()
56
  if res_upd.data:
57
  user = _map_profile(res_upd.data[0])
58
  except Exception:
59
- pass
 
 
 
 
 
 
 
 
 
 
 
60
  except Exception:
61
  pass
62
-
63
- if not user and isinstance(current_user, dict) and current_user.get("id"):
64
- # Last resort: return data from headers if DB/Supabase both failed
65
- # This keeps the UI working even if there's a temporary DB issue
66
- from src.store import _map_profile
67
- user = _map_profile({
68
- "id": current_user["id"],
69
- "display_name": current_user.get("name") or "User",
70
- "email": current_user.get("email") or "",
71
- "avatar_url": "",
72
- "daily_requests": 0,
73
- "last_request_date": ""
74
- })
 
 
 
 
75
 
76
  if not user:
77
  raise HTTPException(404, "Profile not found")
@@ -82,8 +100,8 @@ async def get_profile(
82
  async def update_profile(body: ProfileUpdateRequest, user_id: str = Depends(get_current_user_id)):
83
  try:
84
  updated_user = update_user_profile(
85
- user_id,
86
- name=body.name,
87
  avatar_url=body.avatar_url,
88
  theme=body.theme
89
  )
 
28
  current_user=Depends(get_current_user)
29
  ):
30
  user = get_user_by_id(user_id)
31
+
32
+ # Fast-path: profile row exists and has a real email — return immediately
33
  email = user.get("email", "") if user else ""
34
+ is_placeholder = (
35
+ not user
36
+ or not email
37
+ or "@placeholder.ai" in email
38
+ or "@studymate.ai" in email
39
+ )
40
+
41
  if is_placeholder:
42
+ # First-ever login: pull real data from Supabase Auth admin API and persist it
43
  from src.database import get_supabase
44
  supabase = get_supabase()
45
  if supabase:
46
  try:
 
47
  res = supabase.auth.admin.get_user_by_id(user_id)
48
  if res.user and res.user.email and "@" in res.user.email:
49
  real_email = res.user.email
50
  real_name = res.user.user_metadata.get("name")
51
+
 
52
  from src.store import _table_supabase, _map_profile
53
  data = {"id": user_id, "email": real_email}
54
  if real_name:
55
  data["display_name"] = real_name
56
+
57
  try:
58
+ res_upd = _table_supabase("profiles").insert(data).execute()
59
  if res_upd.data:
60
  user = _map_profile(res_upd.data[0])
61
  except Exception:
62
+ # Profile row probably already exists — do a targeted update instead
63
+ try:
64
+ res_upd = (
65
+ _table_supabase("profiles")
66
+ .update(data)
67
+ .eq("id", user_id)
68
+ .execute()
69
+ )
70
+ if res_upd.data:
71
+ user = _map_profile(res_upd.data[0])
72
+ except Exception:
73
+ pass
74
  except Exception:
75
  pass
76
+
77
+ if not user:
78
+ # Last resort: synthetic profile from the JWT claims so the UI doesn't break
79
+ user_obj = current_user
80
+ uid = getattr(user_obj, "id", None) or (user_obj.get("id") if isinstance(user_obj, dict) else None)
81
+ if uid:
82
+ from src.store import _map_profile
83
+ meta = getattr(user_obj, "user_metadata", {}) or {}
84
+ user = _map_profile({
85
+ "id": uid,
86
+ "display_name": meta.get("full_name") or meta.get("name") or "User",
87
+ "email": getattr(user_obj, "email", "") or "",
88
+ "avatar_url": "",
89
+ "daily_requests": 0,
90
+ "last_request_date": "",
91
+ "_is_fallback": True,
92
+ })
93
 
94
  if not user:
95
  raise HTTPException(404, "Profile not found")
 
100
  async def update_profile(body: ProfileUpdateRequest, user_id: str = Depends(get_current_user_id)):
101
  try:
102
  updated_user = update_user_profile(
103
+ user_id,
104
+ name=body.name,
105
  avatar_url=body.avatar_url,
106
  theme=body.theme
107
  )
database.py CHANGED
@@ -2,14 +2,24 @@ from typing import Optional
2
  from supabase import Client, create_client
3
  from src.config import settings
4
 
 
 
 
 
5
 
6
  def get_supabase() -> Optional[Client]:
7
- if not settings.supabase_url or not settings.supabase_key:
8
- return None
9
- return create_client(settings.supabase_url, settings.supabase_key)
 
 
 
10
 
11
 
12
  def get_auth_supabase() -> Optional[Client]:
13
- if not settings.supabase_url or not settings.supabase_anon_key:
14
- return None
15
- return create_client(settings.supabase_url, settings.supabase_anon_key)
 
 
 
 
2
  from supabase import Client, create_client
3
  from src.config import settings
4
 
5
+ # Singletons — created once, reused on every request
6
+ _supabase_client: Optional[Client] = None
7
+ _auth_supabase_client: Optional[Client] = None
8
+
9
 
10
  def get_supabase() -> Optional[Client]:
11
+ global _supabase_client
12
+ if _supabase_client is None:
13
+ if not settings.supabase_url or not settings.supabase_key:
14
+ return None
15
+ _supabase_client = create_client(settings.supabase_url, settings.supabase_key)
16
+ return _supabase_client
17
 
18
 
19
  def get_auth_supabase() -> Optional[Client]:
20
+ global _auth_supabase_client
21
+ if _auth_supabase_client is None:
22
+ if not settings.supabase_url or not settings.supabase_anon_key:
23
+ return None
24
+ _auth_supabase_client = create_client(settings.supabase_url, settings.supabase_anon_key)
25
+ return _auth_supabase_client
dependencies.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from fastapi import Depends, HTTPException, Header, status, Request
2
  from fastapi.security import OAuth2PasswordBearer
3
  from typing import Any, Optional
@@ -9,6 +10,34 @@ DEV_USER = {"id": DEV_USER_ID, "email": "dev@studymate.ai", "name": "Dev User"}
9
 
10
  oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  def _extract_token(request: Request) -> Optional[str]:
14
  """
@@ -35,7 +64,8 @@ def _extract_token(request: Request) -> Optional[str]:
35
 
36
 
37
  async def get_current_user_id(request: Request) -> str:
38
- client = get_supabase()
 
39
 
40
  # Dev mode
41
  if client is None:
@@ -47,18 +77,15 @@ async def get_current_user_id(request: Request) -> str:
47
  raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Not authenticated")
48
 
49
  try:
50
- auth_client = get_auth_supabase()
51
- supabase = auth_client if auth_client is not None else client
52
- user = supabase.auth.get_user(token)
53
- user_obj = getattr(user, "user", None) or user
54
- return str(user_obj.id)
55
- except Exception as e:
56
  raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid or expired token")
57
 
58
 
59
  async def get_current_user(request: Request) -> Any:
60
- auth_client = get_auth_supabase()
61
- client = get_supabase()
62
 
63
  # Dev mode
64
  if client is None:
@@ -68,9 +95,7 @@ async def get_current_user(request: Request) -> Any:
68
 
69
  if token:
70
  try:
71
- verify_client = auth_client if auth_client is not None else client
72
- response = verify_client.auth.get_user(token)
73
- user = getattr(response, "user", None) or response
74
  if user:
75
  return user
76
  except Exception:
 
1
+ import time
2
  from fastapi import Depends, HTTPException, Header, status, Request
3
  from fastapi.security import OAuth2PasswordBearer
4
  from typing import Any, Optional
 
10
 
11
  oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
12
 
13
+ _TOKEN_CACHE: dict = {}
14
+ _TOKEN_CACHE_TTL = 300 # 5 minutes
15
+
16
+
17
+ def _verify_token_cached(client, token: str) -> Any:
18
+ now = time.time()
19
+
20
+ # Simple cleanup to prevent unbounded growth
21
+ if len(_TOKEN_CACHE) > 1000:
22
+ expired = [k for k, v in _TOKEN_CACHE.items() if now - v[1] > _TOKEN_CACHE_TTL]
23
+ for k in expired:
24
+ del _TOKEN_CACHE[k]
25
+
26
+ # Return cached user if valid
27
+ if token in _TOKEN_CACHE:
28
+ user, timestamp = _TOKEN_CACHE[token]
29
+ if now - timestamp < _TOKEN_CACHE_TTL:
30
+ return user
31
+
32
+ # Not cached or expired — fetch from Supabase
33
+ response = client.auth.get_user(token)
34
+ user = getattr(response, "user", None) or response
35
+ if user:
36
+ _TOKEN_CACHE[token] = (user, now)
37
+ return user
38
+
39
+ raise ValueError("Invalid token response")
40
+
41
 
42
  def _extract_token(request: Request) -> Optional[str]:
43
  """
 
64
 
65
 
66
  async def get_current_user_id(request: Request) -> str:
67
+ # Use a single cached client prefer the auth client, fall back to service client
68
+ client = get_auth_supabase() or get_supabase()
69
 
70
  # Dev mode
71
  if client is None:
 
77
  raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Not authenticated")
78
 
79
  try:
80
+ user = _verify_token_cached(client, token)
81
+ return str(user.id)
82
+ except Exception:
 
 
 
83
  raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid or expired token")
84
 
85
 
86
  async def get_current_user(request: Request) -> Any:
87
+ # Use a single cached client — prefer the auth client, fall back to service client
88
+ client = get_auth_supabase() or get_supabase()
89
 
90
  # Dev mode
91
  if client is None:
 
95
 
96
  if token:
97
  try:
98
+ user = _verify_token_cached(client, token)
 
 
99
  if user:
100
  return user
101
  except Exception:
quiz_generator/quiz.py CHANGED
@@ -56,6 +56,12 @@ Include exactly:
56
  ]
57
  }}
58
 
 
 
 
 
 
 
59
  **AVAILABLE CONTEXT:**
60
  {context}
61
 
 
56
  ]
57
  }}
58
 
59
+ **CRITICAL RULES:**
60
+ 1. Your final output MUST be exactly the JSON structure above.
61
+ 2. DO NOT include any conversational text, prefixes, or markdown blocks (like ```json).
62
+ 3. Even if you cannot find enough context or tools fail, YOU MUST STILL output a valid JSON containing questions based on your general knowledge.
63
+ 4. ANY deviation from the valid JSON format will break the system.
64
+
65
  **AVAILABLE CONTEXT:**
66
  {context}
67
 
store.py CHANGED
@@ -1,4 +1,6 @@
1
  import os
 
 
2
  from typing import Optional
3
  import logging
4
  from datetime import datetime, timezone, date
@@ -125,13 +127,12 @@ def _table_supabase(table: str):
125
  return _FakeTable(table)
126
 
127
  def _robust_execute(query):
128
- import time
129
- from httpx import RemoteProtocolError
130
  for attempt in range(3):
131
  try:
132
  return query.execute()
133
- except (RemoteProtocolError, Exception) as e:
134
- if attempt == 2: raise e
 
135
  time.sleep(0.5 * (attempt + 1))
136
  return query.execute()
137
 
 
1
  import os
2
+ import time
3
+ from httpx import RemoteProtocolError
4
  from typing import Optional
5
  import logging
6
  from datetime import datetime, timezone, date
 
127
  return _FakeTable(table)
128
 
129
  def _robust_execute(query):
 
 
130
  for attempt in range(3):
131
  try:
132
  return query.execute()
133
+ except RemoteProtocolError as e:
134
+ if attempt == 2:
135
+ raise e
136
  time.sleep(0.5 * (attempt + 1))
137
  return query.execute()
138
 
summary_generator/summary.py CHANGED
@@ -124,7 +124,8 @@ def web_summarizer(topic: str) -> str:
124
  logger.warning(f"Arxiv search for '{topic}' failed: {e}")
125
 
126
  if not all_content:
127
- raise ValueError(f"No content found for topic: {topic}")
 
128
 
129
  combined = "\n\n".join(all_content)
130
  logger.info(f"Web search combined text length for topic '{topic}': {len(combined)}")
 
124
  logger.warning(f"Arxiv search for '{topic}' failed: {e}")
125
 
126
  if not all_content:
127
+ logger.warning(f"No content found for topic: {topic}. Falling back to general knowledge.")
128
+ all_content.append(f"Topic: {topic}\n\nPlease provide a comprehensive educational summary of this topic based on your general knowledge.")
129
 
130
  combined = "\n\n".join(all_content)
131
  logger.info(f"Web search combined text length for topic '{topic}': {len(combined)}")