Hydra-Bolt commited on
Commit
7b10350
·
1 Parent(s): 11bf216
Files changed (3) hide show
  1. app/auth/routes.py +25 -136
  2. app/config/settings.py +0 -5
  3. test_auth_timeout.py +0 -76
app/auth/routes.py CHANGED
@@ -5,10 +5,6 @@ from datetime import datetime, timedelta
5
  from typing import Optional
6
  import bcrypt
7
  import logging
8
- import asyncio
9
- import httpx
10
- from gotrue.errors import AuthRetryableError
11
- from gotrue.types import AuthResponse as GoTrueAuthResponse
12
 
13
  from app.config.settings import settings
14
  from app.db.models import (
@@ -30,14 +26,12 @@ logger = logging.getLogger(__name__)
30
 
31
  # Initialize Supabase client
32
  def get_supabase_client() -> Client:
33
- """Get Supabase client instance with proper timeout configuration."""
34
  if not settings.SUPABASE_URL or not settings.SUPABASE_SERVICE_KEY:
35
  raise HTTPException(
36
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
37
  detail="Supabase configuration is missing"
38
  )
39
-
40
- # Create client - we'll handle timeouts in the retry logic
41
  return create_client(settings.SUPABASE_URL, settings.SUPABASE_SERVICE_KEY)
42
 
43
 
@@ -52,38 +46,6 @@ def verify_password(password: str, hashed_password: str) -> bool:
52
  return bcrypt.checkpw(password.encode('utf-8'), hashed_password.encode('utf-8'))
53
 
54
 
55
- async def retry_auth_operation(operation, max_retries: int = 3, delay: float = 1.0) -> GoTrueAuthResponse:
56
- """Retry authentication operations with exponential backoff."""
57
- last_exception = None
58
-
59
- for attempt in range(max_retries):
60
- try:
61
- result = operation()
62
- return result
63
- except (AuthRetryableError, httpx.TimeoutException, httpx.ReadTimeout) as e:
64
- last_exception = e
65
- if attempt == max_retries - 1:
66
- logger.error(f"Auth operation failed after {max_retries} attempts: {e}")
67
- raise HTTPException(
68
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
69
- detail=f"Authentication service is currently unavailable. Please try again in a few minutes. (Attempt {max_retries}/{max_retries})"
70
- )
71
-
72
- wait_time = delay * (2 ** attempt)
73
- logger.warning(f"Auth operation timeout, retrying in {wait_time}s (attempt {attempt + 1}/{max_retries}): {e}")
74
- await asyncio.sleep(wait_time)
75
- except Exception as e:
76
- # For non-retryable errors, fail immediately
77
- logger.error(f"Non-retryable auth error: {e}")
78
- raise
79
-
80
- # This should never be reached, but just in case
81
- raise HTTPException(
82
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
83
- detail=f"Authentication service failed after {max_retries} attempts"
84
- )
85
-
86
-
87
  async def create_user_session(
88
  user_id: str,
89
  access_token: str,
@@ -139,55 +101,32 @@ async def register(request: Request, user_data: RegisterRequest):
139
  detail="User with this email already exists"
140
  )
141
 
142
- # Create user using Supabase Auth with retry logic
143
  logger.info(f"Creating auth user in Supabase for email={user_data.email}")
144
-
145
- def create_auth_user():
146
- return supabase.auth.sign_up({
147
- "email": user_data.email,
148
- "password": user_data.password,
149
- "options": {
150
- "data": {
151
- "username": user_data.username,
152
- "full_name": user_data.full_name
153
- }
154
  }
155
- })
156
-
157
- try:
158
- auth_response = await retry_auth_operation(
159
- create_auth_user,
160
- max_retries=settings.AUTH_RETRY_ATTEMPTS,
161
- delay=settings.AUTH_RETRY_DELAY
162
- )
163
- except HTTPException as e:
164
- logger.error(f"Supabase auth sign_up failed for email={user_data.email} after retries: {e.detail}")
165
- raise HTTPException(
166
- status_code=e.status_code,
167
- detail=f"User registration is temporarily unavailable. {e.detail}"
168
- )
169
 
170
  logger.debug(f"Supabase auth sign_up response for email={user_data.email}: user_present={bool(getattr(auth_response, 'user', None))}")
171
- if not hasattr(auth_response, 'user') or not auth_response.user:
172
  logger.error(f"Supabase auth failed to create user for email={user_data.email}")
173
  raise HTTPException(
174
  status_code=status.HTTP_400_BAD_REQUEST,
175
  detail="Failed to create user"
176
  )
177
 
178
- user_id = getattr(auth_response.user, 'id', None)
179
- if not user_id:
180
- logger.error(f"Auth response missing user ID for email={user_data.email}")
181
- raise HTTPException(
182
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
183
- detail="Authentication response is invalid"
184
- )
185
-
186
- logger.info(f"Supabase auth created user id={user_id} for email={user_data.email}")
187
 
188
  # Create user record in our custom table with error handling for duplicates
189
  user_record = {
190
- "id": user_id,
191
  "email": user_data.email,
192
  "username": user_data.username,
193
  "full_name": user_data.full_name,
@@ -212,15 +151,15 @@ async def register(request: Request, user_data: RegisterRequest):
212
  logger.warning(f"DB insert error for user id={user_record['id']}: {db_error}")
213
  # Check if this is a duplicate key error (user already exists)
214
  if "duplicate key" in str(db_error) or "23505" in str(db_error):
215
- logger.info(f"Duplicate key detected when inserting user id={user_id}, attempting to fetch existing record")
216
  # User already exists in our table, fetch existing user
217
- existing_user_response = supabase.table("users").select("*").eq("id", user_id).execute()
218
- logger.debug(f"Existing user fetch result for id={user_id}: {existing_user_response.data}")
219
  if existing_user_response.data:
220
  user = User(**existing_user_response.data[0])
221
  logger.info(f"Fetched existing user record for id={user.id}")
222
  else:
223
- logger.error(f"Duplicate key error but failed to fetch existing user id={user_id}")
224
  raise HTTPException(
225
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
226
  detail="User creation failed"
@@ -271,41 +210,20 @@ async def login(request: Request, credentials: LoginRequest):
271
  supabase = get_supabase_client()
272
 
273
  try:
274
- # Authenticate with Supabase using retry logic
275
- def auth_signin():
276
- return supabase.auth.sign_in_with_password({
277
- "email": credentials.email,
278
- "password": credentials.password
279
- })
280
-
281
- try:
282
- auth_response = await retry_auth_operation(
283
- auth_signin,
284
- max_retries=settings.AUTH_RETRY_ATTEMPTS,
285
- delay=settings.AUTH_RETRY_DELAY
286
- )
287
- except HTTPException as e:
288
- logger.error(f"Supabase auth sign_in failed for email={credentials.email} after retries: {e.detail}")
289
- raise HTTPException(
290
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
291
- detail="Authentication service is temporarily unavailable. Please try again in a few minutes."
292
- )
293
 
294
- if not hasattr(auth_response, 'user') or not auth_response.user:
295
  raise HTTPException(
296
  status_code=status.HTTP_401_UNAUTHORIZED,
297
  detail="Invalid email or password"
298
  )
299
 
300
  # Get user from our custom table
301
- user_id = getattr(auth_response.user, 'id', None)
302
- if not user_id:
303
- raise HTTPException(
304
- status_code=status.HTTP_401_UNAUTHORIZED,
305
- detail="Authentication response is invalid"
306
- )
307
-
308
- user_response = supabase.table("users").select("*").eq("id", user_id).execute()
309
  if not user_response.data:
310
  raise HTTPException(
311
  status_code=status.HTTP_404_NOT_FOUND,
@@ -457,32 +375,3 @@ async def get_user_sessions(current_user: User = Depends(auth_middleware.get_cur
457
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
458
  detail="Failed to fetch sessions"
459
  )
460
-
461
-
462
- @router.get("/health")
463
- async def auth_health_check():
464
- """Check authentication service health."""
465
- try:
466
- supabase = get_supabase_client()
467
- # Simple health check by attempting to access the users table
468
- start_time = datetime.utcnow()
469
- supabase.table("users").select("id").limit(1).execute()
470
- end_time = datetime.utcnow()
471
- response_time = (end_time - start_time).total_seconds()
472
-
473
- return {
474
- "status": "healthy",
475
- "service": "authentication",
476
- "response_time_seconds": response_time,
477
- "timestamp": datetime.utcnow().isoformat(),
478
- "supabase_connection": "ok"
479
- }
480
- except Exception as e:
481
- logger.error(f"Auth health check failed: {e}")
482
- return {
483
- "status": "unhealthy",
484
- "service": "authentication",
485
- "error": str(e),
486
- "timestamp": datetime.utcnow().isoformat(),
487
- "supabase_connection": "failed"
488
- }
 
5
  from typing import Optional
6
  import bcrypt
7
  import logging
 
 
 
 
8
 
9
  from app.config.settings import settings
10
  from app.db.models import (
 
26
 
27
  # Initialize Supabase client
28
  def get_supabase_client() -> Client:
29
+ """Get Supabase client instance."""
30
  if not settings.SUPABASE_URL or not settings.SUPABASE_SERVICE_KEY:
31
  raise HTTPException(
32
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
33
  detail="Supabase configuration is missing"
34
  )
 
 
35
  return create_client(settings.SUPABASE_URL, settings.SUPABASE_SERVICE_KEY)
36
 
37
 
 
46
  return bcrypt.checkpw(password.encode('utf-8'), hashed_password.encode('utf-8'))
47
 
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  async def create_user_session(
50
  user_id: str,
51
  access_token: str,
 
101
  detail="User with this email already exists"
102
  )
103
 
104
+ # Create user using Supabase Auth
105
  logger.info(f"Creating auth user in Supabase for email={user_data.email}")
106
+ auth_response = supabase.auth.sign_up({
107
+ "email": user_data.email,
108
+ "password": user_data.password,
109
+ "options": {
110
+ "data": {
111
+ "username": user_data.username,
112
+ "full_name": user_data.full_name
 
 
 
113
  }
114
+ }
115
+ })
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
  logger.debug(f"Supabase auth sign_up response for email={user_data.email}: user_present={bool(getattr(auth_response, 'user', None))}")
118
+ if not auth_response.user:
119
  logger.error(f"Supabase auth failed to create user for email={user_data.email}")
120
  raise HTTPException(
121
  status_code=status.HTTP_400_BAD_REQUEST,
122
  detail="Failed to create user"
123
  )
124
 
125
+ logger.info(f"Supabase auth created user id={auth_response.user.id} for email={user_data.email}")
 
 
 
 
 
 
 
 
126
 
127
  # Create user record in our custom table with error handling for duplicates
128
  user_record = {
129
+ "id": auth_response.user.id,
130
  "email": user_data.email,
131
  "username": user_data.username,
132
  "full_name": user_data.full_name,
 
151
  logger.warning(f"DB insert error for user id={user_record['id']}: {db_error}")
152
  # Check if this is a duplicate key error (user already exists)
153
  if "duplicate key" in str(db_error) or "23505" in str(db_error):
154
+ logger.info(f"Duplicate key detected when inserting user id={user_record['id']}, attempting to fetch existing record")
155
  # User already exists in our table, fetch existing user
156
+ existing_user_response = supabase.table("users").select("*").eq("id", auth_response.user.id).execute()
157
+ logger.debug(f"Existing user fetch result for id={auth_response.user.id}: {existing_user_response.data}")
158
  if existing_user_response.data:
159
  user = User(**existing_user_response.data[0])
160
  logger.info(f"Fetched existing user record for id={user.id}")
161
  else:
162
+ logger.error(f"Duplicate key error but failed to fetch existing user id={auth_response.user.id}")
163
  raise HTTPException(
164
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
165
  detail="User creation failed"
 
210
  supabase = get_supabase_client()
211
 
212
  try:
213
+ # Authenticate with Supabase
214
+ auth_response = supabase.auth.sign_in_with_password({
215
+ "email": credentials.email,
216
+ "password": credentials.password
217
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
+ if not auth_response.user:
220
  raise HTTPException(
221
  status_code=status.HTTP_401_UNAUTHORIZED,
222
  detail="Invalid email or password"
223
  )
224
 
225
  # Get user from our custom table
226
+ user_response = supabase.table("users").select("*").eq("id", auth_response.user.id).execute()
 
 
 
 
 
 
 
227
  if not user_response.data:
228
  raise HTTPException(
229
  status_code=status.HTTP_404_NOT_FOUND,
 
375
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
376
  detail="Failed to fetch sessions"
377
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/config/settings.py CHANGED
@@ -30,11 +30,6 @@ class Settings:
30
  SUPABASE_URL: Optional[str] = os.getenv("SUPABASE_URL")
31
  SUPABASE_SERVICE_KEY: Optional[str] = os.getenv("SUPABASE_SERVICE_KEY")
32
  SUPABASE_ANON_KEY: Optional[str] = os.getenv("SUPABASE_ANON_KEY")
33
-
34
- # Timeout Configuration
35
- AUTH_TIMEOUT_SECONDS: int = int(os.getenv("AUTH_TIMEOUT_SECONDS", "30"))
36
- AUTH_RETRY_ATTEMPTS: int = int(os.getenv("AUTH_RETRY_ATTEMPTS", "3"))
37
- AUTH_RETRY_DELAY: float = float(os.getenv("AUTH_RETRY_DELAY", "2.0"))
38
 
39
  # Database
40
  DATABASE_URL: Optional[str] = os.getenv("DATABASE_URL")
 
30
  SUPABASE_URL: Optional[str] = os.getenv("SUPABASE_URL")
31
  SUPABASE_SERVICE_KEY: Optional[str] = os.getenv("SUPABASE_SERVICE_KEY")
32
  SUPABASE_ANON_KEY: Optional[str] = os.getenv("SUPABASE_ANON_KEY")
 
 
 
 
 
33
 
34
  # Database
35
  DATABASE_URL: Optional[str] = os.getenv("DATABASE_URL")
test_auth_timeout.py DELETED
@@ -1,76 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Test script to verify authentication timeout handling.
4
- """
5
-
6
- import asyncio
7
- import httpx
8
- import sys
9
- import os
10
-
11
- # Add the app directory to the path so we can import our modules
12
- sys.path.insert(0, '/home/muneeb/Projects/SanadCheck/sanad-llm')
13
-
14
- from app.config.settings import settings
15
-
16
- async def test_auth_endpoints():
17
- """Test authentication endpoints with proper timeout handling."""
18
-
19
- base_url = f"http://{settings.HOST}:{settings.PORT}"
20
- timeout = httpx.Timeout(30.0, connect=10.0)
21
-
22
- async with httpx.AsyncClient(timeout=timeout) as client:
23
- print(f"Testing authentication endpoints at {base_url}")
24
-
25
- # Test health endpoint
26
- print("\n1. Testing auth health endpoint...")
27
- try:
28
- response = await client.get(f"{base_url}/auth/health")
29
- print(f"Health check status: {response.status_code}")
30
- if response.status_code == 200:
31
- health_data = response.json()
32
- print(f"Health status: {health_data.get('status')}")
33
- print(f"Response time: {health_data.get('response_time_seconds')}s")
34
- else:
35
- print(f"Health check failed: {response.text}")
36
- except Exception as e:
37
- print(f"Health check error: {e}")
38
-
39
- # Test registration with timeout-prone data
40
- print("\n2. Testing registration endpoint...")
41
- test_email = "test_timeout_user@example.com"
42
- test_data = {
43
- "email": test_email,
44
- "password": "SecurePassword123!",
45
- "username": "timeout_test_user",
46
- "full_name": "Timeout Test User"
47
- }
48
-
49
- try:
50
- response = await client.post(f"{base_url}/auth/register", json=test_data)
51
- print(f"Registration status: {response.status_code}")
52
- if response.status_code == 200:
53
- print("Registration successful!")
54
- reg_data = response.json()
55
- print(f"User ID: {reg_data.get('user', {}).get('id')}")
56
- elif response.status_code == 400:
57
- print("User might already exist or validation error")
58
- print(f"Error: {response.json().get('detail')}")
59
- elif response.status_code == 503:
60
- print("Service unavailable - this is expected if Supabase is timing out")
61
- print(f"Error: {response.json().get('detail')}")
62
- else:
63
- print(f"Unexpected response: {response.text}")
64
- except httpx.TimeoutException as e:
65
- print(f"Client timeout (this shouldn't happen with our retry logic): {e}")
66
- except Exception as e:
67
- print(f"Registration error: {e}")
68
-
69
- if __name__ == "__main__":
70
- print("Authentication Timeout Test")
71
- print("=" * 50)
72
- print(f"AUTH_TIMEOUT_SECONDS: {settings.AUTH_TIMEOUT_SECONDS}")
73
- print(f"AUTH_RETRY_ATTEMPTS: {settings.AUTH_RETRY_ATTEMPTS}")
74
- print(f"AUTH_RETRY_DELAY: {settings.AUTH_RETRY_DELAY}")
75
-
76
- asyncio.run(test_auth_endpoints())