Hamdy005 commited on
Commit
86a47ea
Β·
1 Parent(s): 8c7a11b

feat: implement Supabase token authentication and migrate user management to profiles table

Browse files
Files changed (3) hide show
  1. database.py +7 -0
  2. dependencies.py +16 -2
  3. store.py +94 -65
database.py CHANGED
@@ -9,3 +9,10 @@ def get_supabase() -> Optional[Client]:
9
  if not settings.supabase_url or not settings.supabase_key:
10
  return None
11
  return create_client(settings.supabase_url, settings.supabase_key)
 
 
 
 
 
 
 
 
9
  if not settings.supabase_url or not settings.supabase_key:
10
  return None
11
  return create_client(settings.supabase_url, settings.supabase_key)
12
+
13
+
14
+ @lru_cache()
15
+ def get_auth_supabase() -> Optional[Client]:
16
+ if not settings.supabase_url or not settings.supabase_anon_key:
17
+ return None
18
+ return create_client(settings.supabase_url, settings.supabase_anon_key)
dependencies.py CHANGED
@@ -2,7 +2,7 @@ from fastapi import Depends, HTTPException, Header, status
2
  from fastapi.security import OAuth2PasswordBearer
3
  from typing import Any, Optional
4
 
5
- from src.database import get_supabase
6
 
7
  DEV_USER_ID = "00000000-0000-0000-0000-000000000001"
8
  DEV_USER = {"id": DEV_USER_ID, "email": "dev@studymate.ai", "name": "Dev User"}
@@ -16,6 +16,7 @@ async def get_current_user(
16
  x_user_name: Optional[str] = Header(None),
17
  x_user_email: Optional[str] = Header(None),
18
  ) -> Any:
 
19
  client = get_supabase()
20
 
21
  # Dev mode: no Supabase configured
@@ -28,7 +29,20 @@ async def get_current_user(
28
  }
29
  return DEV_USER
30
 
31
- # Real Supabase auth
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  if not token:
33
  raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Not authenticated")
34
  try:
 
2
  from fastapi.security import OAuth2PasswordBearer
3
  from typing import Any, Optional
4
 
5
+ from src.database import get_supabase, get_auth_supabase
6
 
7
  DEV_USER_ID = "00000000-0000-0000-0000-000000000001"
8
  DEV_USER = {"id": DEV_USER_ID, "email": "dev@studymate.ai", "name": "Dev User"}
 
16
  x_user_name: Optional[str] = Header(None),
17
  x_user_email: Optional[str] = Header(None),
18
  ) -> Any:
19
+ auth_client = get_auth_supabase()
20
  client = get_supabase()
21
 
22
  # Dev mode: no Supabase configured
 
29
  }
30
  return DEV_USER
31
 
32
+ # Real Supabase auth - use anon key client for token verification
33
+ if auth_client is not None:
34
+ if not token:
35
+ raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Not authenticated")
36
+ try:
37
+ response = auth_client.auth.get_user(token)
38
+ except Exception:
39
+ raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid authentication")
40
+ user = getattr(response, "user", None) or response
41
+ if not user:
42
+ raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Unauthorized")
43
+ return user
44
+
45
+ # Fallback to service_role client if anon key not available
46
  if not token:
47
  raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Not authenticated")
48
  try:
store.py CHANGED
@@ -25,65 +25,65 @@ def _db():
25
  return None
26
 
27
 
28
- def _table_supabase(table: str):
29
- client = _db()
30
- if client is not None:
31
- return client.table(table)
32
-
33
- class _FakeTable:
34
- def __init__(self, name):
35
- self.name = name
36
-
37
- def insert(self, data):
38
- if isinstance(data, list):
39
- for item in data:
40
- item["id"] = item.get("id", _get_next_id())
41
- _in_memory.setdefault(self.name, {})[item["id"]] = item
42
- class R:
43
- data = data
44
- return R()
45
- data["id"] = data.get("id", _get_next_id())
46
- _in_memory.setdefault(self.name, {})[data["id"]] = data
47
  class R:
48
- data = [data]
49
  return R()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
- def select(self, *args):
52
- return self
53
-
54
- def eq(self, field, value):
55
- self._eq_field = field
56
- self._eq_value = value
57
- return self
58
-
59
- def order(self, field):
60
- return self
61
-
62
- def maybe_single(self):
63
- records = list(_in_memory.get(self.name, {}).values())
64
- if hasattr(self, '_eq_field'):
65
- records = [r for r in records if r.get(self._eq_field) == self._eq_value]
66
- return self._make_response(records[0] if records else None)
67
-
68
- def execute(self):
69
- records = list(_in_memory.get(self.name, {}).values())
70
- if hasattr(self, '_eq_field'):
71
- records = [r for r in records if r.get(self._eq_field) == self._eq_value]
72
- if hasattr(self, '_order_field'):
73
- records.sort(key=lambda r: r.get(self._order_field, 0))
74
- return self._make_response(records)
75
-
76
- def update(self, data):
77
- self._update_data = data
78
- return self
79
-
80
- def _make_response(self, data):
81
- class R:
82
- pass
83
- r = R()
84
- r.data = data if isinstance(data, list) else ([data] if data else [])
85
- return r
86
 
 
 
 
 
87
  return _FakeTable(table)
88
 
89
 
@@ -112,8 +112,6 @@ def create_material(user_id: str, source_type: str, title: str,
112
  data["file_path"] = file_path
113
  if url:
114
  data["url"] = url
115
- if topic:
116
- data["topic"] = topic
117
  result = _table_supabase("materials").insert(data).execute()
118
  return result.data[0]
119
 
@@ -216,25 +214,56 @@ def get_quizzes(material_id: Optional[str] = None, user_id: Optional[str] = None
216
  return records
217
 
218
 
219
- # ── Users ────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
220
 
221
  def create_user(name: str, email: str, password: str) -> dict:
222
  existing = get_user_by_email(email)
223
  if existing:
224
  raise ValueError("Email already registered")
225
- data = {"name": name, "email": email, "password": password}
226
- result = _table_supabase("users").insert(data).execute()
227
- return result.data[0]
 
 
 
 
228
 
229
 
230
  def get_user_by_email(email: str) -> Optional[dict]:
231
- result = _table_supabase("users").select("*").eq("email", email).maybe_single().execute()
232
- return result.data[0] if result.data else None
 
 
 
 
 
 
 
 
 
233
 
234
 
235
  def get_user_by_id(user_id: str) -> Optional[dict]:
236
- result = _table_supabase("users").select("*").eq("id", user_id).maybe_single().execute()
237
- return result.data[0] if result.data else None
 
 
 
 
 
 
 
 
 
238
 
239
 
240
  # ── Conversation Memory (in-memory, ephemeral) ─────────
 
25
  return None
26
 
27
 
28
+ class _FakeTable:
29
+ def __init__(self, name):
30
+ self.name = name
31
+
32
+ def insert(self, data):
33
+ if isinstance(data, list):
34
+ for item in data:
35
+ item["id"] = item.get("id", _get_next_id())
36
+ _in_memory.setdefault(self.name, {})[item["id"]] = item
 
 
 
 
 
 
 
 
 
 
37
  class R:
38
+ data = data
39
  return R()
40
+ data["id"] = data.get("id", _get_next_id())
41
+ _in_memory.setdefault(self.name, {})[data["id"]] = data
42
+ class R:
43
+ data = [data]
44
+ return R()
45
+
46
+ def select(self, *args):
47
+ return self
48
+
49
+ def eq(self, field, value):
50
+ self._eq_field = field
51
+ self._eq_value = value
52
+ return self
53
+
54
+ def order(self, field):
55
+ return self
56
+
57
+ def maybe_single(self):
58
+ records = list(_in_memory.get(self.name, {}).values())
59
+ if hasattr(self, '_eq_field'):
60
+ records = [r for r in records if r.get(self._eq_field) == self._eq_value]
61
+ return self._make_response(records[0] if records else None)
62
+
63
+ def execute(self):
64
+ records = list(_in_memory.get(self.name, {}).values())
65
+ if hasattr(self, '_eq_field'):
66
+ records = [r for r in records if r.get(self._eq_field) == self._eq_value]
67
+ if hasattr(self, '_order_field'):
68
+ records.sort(key=lambda r: r.get(self._order_field, 0))
69
+ return self._make_response(records)
70
+
71
+ def update(self, data):
72
+ self._update_data = data
73
+ return self
74
+
75
+ def _make_response(self, data):
76
+ class R:
77
+ pass
78
+ r = R()
79
+ r.data = data if isinstance(data, list) else ([data] if data else [])
80
+ return r
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
+ def _table_supabase(table: str):
84
+ client = _db()
85
+ if client is not None:
86
+ return client.table(table)
87
  return _FakeTable(table)
88
 
89
 
 
112
  data["file_path"] = file_path
113
  if url:
114
  data["url"] = url
 
 
115
  result = _table_supabase("materials").insert(data).execute()
116
  return result.data[0]
117
 
 
214
  return records
215
 
216
 
217
+ # ── Users (maps to Supabase `profiles` table) ─────────
218
+
219
+ def _map_profile(profile: dict) -> dict:
220
+ return {
221
+ "id": profile["id"],
222
+ "name": profile.get("display_name", ""),
223
+ "email": profile.get("email", ""),
224
+ "avatar_url": profile.get("avatar_url", ""),
225
+ }
226
+
227
 
228
  def create_user(name: str, email: str, password: str) -> dict:
229
  existing = get_user_by_email(email)
230
  if existing:
231
  raise ValueError("Email already registered")
232
+ data = {"display_name": name, "email": email}
233
+ try:
234
+ result = _table_supabase("profiles").insert(data).execute()
235
+ return _map_profile(result.data[0])
236
+ except Exception:
237
+ result = _FakeTable("profiles").insert(data).execute()
238
+ return _map_profile(result.data[0])
239
 
240
 
241
  def get_user_by_email(email: str) -> Optional[dict]:
242
+ try:
243
+ result = _table_supabase("profiles").select("*").eq("email", email).maybe_single().execute()
244
+ if result.data:
245
+ return _map_profile(result.data[0])
246
+ except Exception:
247
+ pass
248
+ fake = _FakeTable("profiles")
249
+ result = fake.select("*").eq("email", email).maybe_single().execute()
250
+ if result.data:
251
+ return _map_profile(result.data[0])
252
+ return None
253
 
254
 
255
  def get_user_by_id(user_id: str) -> Optional[dict]:
256
+ try:
257
+ result = _table_supabase("profiles").select("*").eq("id", user_id).maybe_single().execute()
258
+ if result.data:
259
+ return _map_profile(result.data[0])
260
+ except Exception:
261
+ pass
262
+ fake = _FakeTable("profiles")
263
+ result = fake.select("*").eq("id", user_id).maybe_single().execute()
264
+ if result.data:
265
+ return _map_profile(result.data[0])
266
+ return None
267
 
268
 
269
  # ── Conversation Memory (in-memory, ephemeral) ─────────