Mark-Lasfar commited on
Commit
5da2025
·
1 Parent(s): 7f8c5ab

Update backend and server frontend for OAuth JSON response, client-side navigation, and add .gitignore

Browse files
Files changed (5) hide show
  1. api/auth.py +44 -7
  2. api/endpoints.py +10 -4
  3. requirements.txt +2 -1
  4. static/js/chat.js +34 -15
  5. templates/login.html +37 -37
api/auth.py CHANGED
@@ -16,6 +16,8 @@ import os
16
  import logging
17
  import secrets
18
  import httpx
 
 
19
 
20
  from api.database import User, OAuthAccount, CustomSQLAlchemyUserDatabase, get_user_db
21
  from api.models import UserRead, UserCreate, UserUpdate
@@ -58,7 +60,7 @@ github_oauth_client = GitHubOAuth2(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET)
58
  github_oauth_client._access_token_url = "https://github.com/login/oauth/access_token"
59
  github_oauth_client._access_token_params = {"headers": {"Accept": "application/json"}}
60
 
61
- # مدير المستخدمين (بدون تغيير)
62
  class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
63
  reset_password_token_secret = SECRET
64
  verification_token_secret = SECRET
@@ -153,6 +155,15 @@ fastapi_users = FastAPIUsers[User, int](
153
 
154
  current_active_user = fastapi_users.current_user(active=True, optional=True)
155
 
 
 
 
 
 
 
 
 
 
156
  # --- إضافة custom authorize endpoints (يرجع JSON مع authorization_url) ---
157
 
158
  async def custom_google_authorize(
@@ -187,13 +198,20 @@ async def custom_github_authorize(
187
 
188
  async def custom_oauth_callback(
189
  code: str,
 
190
  user_manager: UserManager = Depends(get_user_manager),
191
  oauth_client=Depends(lambda: google_oauth_client),
192
  redirect_url: str = GOOGLE_REDIRECT_URL,
193
  response: Response = None,
194
  ):
195
- logger.debug(f"Processing Google callback with code: {code}")
196
  try:
 
 
 
 
 
 
197
  # Get access token
198
  token_data = await oauth_client.get_access_token(code, redirect_url)
199
  access_token = token_data["access_token"]
@@ -219,9 +237,10 @@ async def custom_oauth_callback(
219
  is_verified_by_default=True,
220
  )
221
 
222
- jwt_strategy = get_jwt_strategy()
223
- token = await jwt_strategy.write(user)
224
 
 
225
  cookie_transport.set_cookie(response, token)
226
 
227
  return JSONResponse(content={
@@ -230,17 +249,29 @@ async def custom_oauth_callback(
230
  }, status_code=200)
231
  except Exception as e:
232
  logger.error(f"Error in Google OAuth callback: {str(e)}")
 
 
 
 
 
233
  return JSONResponse(content={"detail": str(e)}, status_code=400)
234
 
235
  async def custom_github_oauth_callback(
236
  code: str,
 
237
  user_manager: UserManager = Depends(get_user_manager),
238
  oauth_client=Depends(lambda: github_oauth_client),
239
  redirect_url: str = GITHUB_REDIRECT_URL,
240
  response: Response = None,
241
  ):
242
- logger.debug(f"Processing GitHub callback with code: {code}")
243
  try:
 
 
 
 
 
 
244
  # Get access token
245
  token_data = await oauth_client.get_access_token(code, redirect_url)
246
  access_token = token_data["access_token"]
@@ -284,9 +315,10 @@ async def custom_github_oauth_callback(
284
  is_verified_by_default=True,
285
  )
286
 
287
- jwt_strategy = get_jwt_strategy()
288
- token = await jwt_strategy.write(user)
289
 
 
290
  cookie_transport.set_cookie(response, token)
291
 
292
  return JSONResponse(content={
@@ -295,6 +327,11 @@ async def custom_github_oauth_callback(
295
  }, status_code=200)
296
  except Exception as e:
297
  logger.error(f"Error in GitHub OAuth callback: {str(e)}")
 
 
 
 
 
298
  return JSONResponse(content={"detail": str(e)}, status_code=400)
299
 
300
  # تضمين الراوترات داخل التطبيق
 
16
  import logging
17
  import secrets
18
  import httpx
19
+ from datetime import datetime
20
+ import jwt # إضافة مكتبة pyjwt لتوليد الـ token يدويًا
21
 
22
  from api.database import User, OAuthAccount, CustomSQLAlchemyUserDatabase, get_user_db
23
  from api.models import UserRead, UserCreate, UserUpdate
 
60
  github_oauth_client._access_token_url = "https://github.com/login/oauth/access_token"
61
  github_oauth_client._access_token_params = {"headers": {"Accept": "application/json"}}
62
 
63
+ # مدير المستخدمين
64
  class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
65
  reset_password_token_secret = SECRET
66
  verification_token_secret = SECRET
 
155
 
156
  current_active_user = fastapi_users.current_user(active=True, optional=True)
157
 
158
+ # دالة مساعدة لتوليد JWT token يدويًا
159
+ async def generate_jwt_token(user: User, secret: str, lifetime_seconds: int) -> str:
160
+ payload = {
161
+ "sub": str(user.id),
162
+ "aud": "fastapi-users:auth",
163
+ "exp": int(datetime.utcnow().timestamp()) + lifetime_seconds,
164
+ }
165
+ return jwt.encode(payload, secret, algorithm="HS256")
166
+
167
  # --- إضافة custom authorize endpoints (يرجع JSON مع authorization_url) ---
168
 
169
  async def custom_google_authorize(
 
198
 
199
  async def custom_oauth_callback(
200
  code: str,
201
+ state: Optional[str] = None,
202
  user_manager: UserManager = Depends(get_user_manager),
203
  oauth_client=Depends(lambda: google_oauth_client),
204
  redirect_url: str = GOOGLE_REDIRECT_URL,
205
  response: Response = None,
206
  ):
207
+ logger.debug(f"Processing Google callback with code: {code}, state: {state}")
208
  try:
209
+ # تحقق من الـ state لو موجود (اختياري لـ CSRF protection)
210
+ if state:
211
+ logger.debug(f"Received state: {state}")
212
+ # إضافة تحقق من الـ state لو بتستخدمه لمنع CSRF
213
+ # مثال: if state != stored_state: raise ValueError("Invalid state parameter")
214
+
215
  # Get access token
216
  token_data = await oauth_client.get_access_token(code, redirect_url)
217
  access_token = token_data["access_token"]
 
237
  is_verified_by_default=True,
238
  )
239
 
240
+ # توليد الـ JWT token يدويًا
241
+ token = await generate_jwt_token(user, SECRET, 3600)
242
 
243
+ # ضبط الـ cookie
244
  cookie_transport.set_cookie(response, token)
245
 
246
  return JSONResponse(content={
 
249
  }, status_code=200)
250
  except Exception as e:
251
  logger.error(f"Error in Google OAuth callback: {str(e)}")
252
+ if "400" in str(e):
253
+ return JSONResponse(
254
+ content={"detail": "Invalid authorization code. Please try logging in again."},
255
+ status_code=400
256
+ )
257
  return JSONResponse(content={"detail": str(e)}, status_code=400)
258
 
259
  async def custom_github_oauth_callback(
260
  code: str,
261
+ state: Optional[str] = None,
262
  user_manager: UserManager = Depends(get_user_manager),
263
  oauth_client=Depends(lambda: github_oauth_client),
264
  redirect_url: str = GITHUB_REDIRECT_URL,
265
  response: Response = None,
266
  ):
267
+ logger.debug(f"Processing GitHub callback with code: {code}, state: {state}")
268
  try:
269
+ # تحقق من الـ state لو موجود (اختياري لـ CSRF protection)
270
+ if state:
271
+ logger.debug(f"Received state: {state}")
272
+ # إضافة تحقق من الـ state لو بتستخدمه لمنع CSRF
273
+ # مثال: if state != stored_state: raise ValueError("Invalid state parameter")
274
+
275
  # Get access token
276
  token_data = await oauth_client.get_access_token(code, redirect_url)
277
  access_token = token_data["access_token"]
 
315
  is_verified_by_default=True,
316
  )
317
 
318
+ # توليد الـ JWT token يدويًا
319
+ token = await generate_jwt_token(user, SECRET, 3600)
320
 
321
+ # ضبط الـ cookie
322
  cookie_transport.set_cookie(response, token)
323
 
324
  return JSONResponse(content={
 
327
  }, status_code=200)
328
  except Exception as e:
329
  logger.error(f"Error in GitHub OAuth callback: {str(e)}")
330
+ if "400" in str(e):
331
+ return JSONResponse(
332
+ content={"detail": "Invalid authorization code. Please try logging in again."},
333
+ status_code=400
334
+ )
335
  return JSONResponse(content={"detail": str(e)}, status_code=400)
336
 
337
  # تضمين الراوترات داخل التطبيق
api/endpoints.py CHANGED
@@ -4,7 +4,7 @@
4
 
5
  import os
6
  import uuid
7
- from fastapi import APIRouter, Depends, HTTPException, Request, status, UploadFile, File
8
  from fastapi.responses import StreamingResponse
9
  from api.database import User, Conversation, Message
10
  from api.models import QueryRequest, ConversationOut, ConversationCreate, UserUpdate
@@ -24,7 +24,6 @@ import logging
24
  from typing import List, Optional
25
  # from utils.constants import MODEL_ALIASES, MODEL_NAME, SECONDARY_MODEL_NAME, TERTIARY_MODEL_NAME, CLIP_BASE_MODEL, CLIP_LARGE_MODEL, ASR_MODEL, TTS_MODEL, IMAGE_GEN_MODEL, SECONDARY_IMAGE_GEN_MODEL
26
  from utils.constants import MODEL_ALIASES, MODEL_NAME, SECONDARY_MODEL_NAME, TERTIARY_MODEL_NAME, CLIP_BASE_MODEL, CLIP_LARGE_MODEL, ASR_MODEL, TTS_MODEL, IMAGE_GEN_MODEL, SECONDARY_IMAGE_GEN_MODEL, IMAGE_INFERENCE_API
27
- from fastapi import Body
28
  import psutil
29
  import time
30
  router = APIRouter()
@@ -899,12 +898,19 @@ async def get_user_settings(user: User = Depends(current_active_user)):
899
  }
900
 
901
 
 
902
  @router.get("/api/verify-token")
903
  async def verify_token(user: User = Depends(current_active_user)):
904
  if not user:
905
  raise HTTPException(status_code=401, detail="Invalid or expired token")
906
- return {"status": "valid"}
907
-
 
 
 
 
 
 
908
 
909
  @router.put("/users/me")
910
  async def update_user_settings(
 
4
 
5
  import os
6
  import uuid
7
+ from fastapi import APIRouter, Depends, HTTPException, Request, status, UploadFile, File , Body
8
  from fastapi.responses import StreamingResponse
9
  from api.database import User, Conversation, Message
10
  from api.models import QueryRequest, ConversationOut, ConversationCreate, UserUpdate
 
24
  from typing import List, Optional
25
  # from utils.constants import MODEL_ALIASES, MODEL_NAME, SECONDARY_MODEL_NAME, TERTIARY_MODEL_NAME, CLIP_BASE_MODEL, CLIP_LARGE_MODEL, ASR_MODEL, TTS_MODEL, IMAGE_GEN_MODEL, SECONDARY_IMAGE_GEN_MODEL
26
  from utils.constants import MODEL_ALIASES, MODEL_NAME, SECONDARY_MODEL_NAME, TERTIARY_MODEL_NAME, CLIP_BASE_MODEL, CLIP_LARGE_MODEL, ASR_MODEL, TTS_MODEL, IMAGE_GEN_MODEL, SECONDARY_IMAGE_GEN_MODEL, IMAGE_INFERENCE_API
 
27
  import psutil
28
  import time
29
  router = APIRouter()
 
898
  }
899
 
900
 
901
+
902
  @router.get("/api/verify-token")
903
  async def verify_token(user: User = Depends(current_active_user)):
904
  if not user:
905
  raise HTTPException(status_code=401, detail="Invalid or expired token")
906
+ return {
907
+ "status": "valid",
908
+ "user": {
909
+ "id": user.id,
910
+ "email": user.email,
911
+ "is_active": user.is_active
912
+ }
913
+ }
914
 
915
  @router.put("/users/me")
916
  async def update_user_settings(
requirements.txt CHANGED
@@ -51,4 +51,5 @@ diffusers>=0.30.0
51
  psutil>=5.9.0
52
  xformers>=0.0.27
53
  anyio==4.6.0
54
- duckduckgo-search
 
 
51
  psutil>=5.9.0
52
  xformers>=0.0.27
53
  anyio==4.6.0
54
+ pyjwt>=2.4.0
55
+ duckduckgo-search
static/js/chat.js CHANGED
@@ -61,17 +61,32 @@ document.addEventListener('DOMContentLoaded', async () => {
61
  // Force chat view to be visible immediately
62
  enterChatView(true);
63
 
64
- if (await checkAuth() && currentConversationId) {
65
- console.log('Loading conversation with ID:', currentConversationId);
 
66
  await loadConversation(currentConversationId);
67
- } else if (!(await checkAuth()) && conversationHistory.length > 0) {
68
- console.log('Restoring conversation history from sessionStorage:', conversationHistory);
 
 
 
 
 
69
  conversationHistory.forEach(msg => {
70
  console.log('Adding message from history:', msg);
71
  addMsg(msg.role, msg.content);
72
  });
 
 
 
 
 
73
  } else {
74
  console.log('No conversation history or ID, starting fresh');
 
 
 
 
75
  }
76
 
77
  autoResizeTextarea();
@@ -88,28 +103,32 @@ document.addEventListener('DOMContentLoaded', async () => {
88
  async function checkAuth() {
89
  const token = localStorage.getItem('token');
90
  if (!token) {
91
- console.log('No auth token found');
92
- return false;
93
  }
94
  try {
95
  const response = await fetch('/api/verify-token', {
96
- headers: { 'Authorization': `Bearer ${token}` }
 
 
 
 
97
  });
98
- if (response.ok) {
99
- console.log('Auth token verified');
100
- return true;
 
101
  } else {
102
- console.log('Token verification failed:', response.status);
103
  localStorage.removeItem('token');
104
- return false;
105
  }
106
  } catch (error) {
107
  console.error('Error verifying token:', error);
108
  localStorage.removeItem('token');
109
- return false;
110
  }
111
  }
112
-
113
  // Handle session for non-logged-in users
114
  async function handleSession() {
115
  const sessionId = sessionStorage.getItem('session_id');
@@ -1260,4 +1279,4 @@ function autoResizeTextarea() {
1260
  // Function to check if text contains Arabic characters
1261
  function isArabicText(text) {
1262
  return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
1263
- }
 
61
  // Force chat view to be visible immediately
62
  enterChatView(true);
63
 
64
+ const authResult = await checkAuth();
65
+ if (authResult.authenticated && currentConversationId) {
66
+ console.log('Authenticated user, loading conversation with ID:', currentConversationId);
67
  await loadConversation(currentConversationId);
68
+ // عرض بيانات المستخدم في الواجهة
69
+ const userInfoElement = document.getElementById('user-info');
70
+ if (userInfoElement && authResult.user) {
71
+ userInfoElement.textContent = `Welcome, ${authResult.user.email}`;
72
+ }
73
+ } else if (!authResult.authenticated && conversationHistory.length > 0) {
74
+ console.log('Unauthenticated user, restoring conversation history from sessionStorage:', conversationHistory);
75
  conversationHistory.forEach(msg => {
76
  console.log('Adding message from history:', msg);
77
  addMsg(msg.role, msg.content);
78
  });
79
+ // عرض Anonymous لو مفيش مستخدم موثّق
80
+ const userInfoElement = document.getElementById('user-info');
81
+ if (userInfoElement) {
82
+ userInfoElement.textContent = 'Anonymous';
83
+ }
84
  } else {
85
  console.log('No conversation history or ID, starting fresh');
86
+ const userInfoElement = document.getElementById('user-info');
87
+ if (userInfoElement) {
88
+ userInfoElement.textContent = 'Anonymous';
89
+ }
90
  }
91
 
92
  autoResizeTextarea();
 
103
  async function checkAuth() {
104
  const token = localStorage.getItem('token');
105
  if (!token) {
106
+ console.log('No auth token found in localStorage');
107
+ return { authenticated: false, user: null };
108
  }
109
  try {
110
  const response = await fetch('/api/verify-token', {
111
+ method: 'GET',
112
+ headers: {
113
+ 'Authorization': `Bearer ${token}`,
114
+ 'Accept': 'application/json'
115
+ }
116
  });
117
+ const data = await response.json();
118
+ if (response.ok && data.status === 'valid') {
119
+ console.log('Auth token verified, user:', data.user);
120
+ return { authenticated: true, user: data.user };
121
  } else {
122
+ console.log('Token verification failed:', data.detail || response.status);
123
  localStorage.removeItem('token');
124
+ return { authenticated: false, user: null };
125
  }
126
  } catch (error) {
127
  console.error('Error verifying token:', error);
128
  localStorage.removeItem('token');
129
+ return { authenticated: false, user: null };
130
  }
131
  }
 
132
  // Handle session for non-logged-in users
133
  async function handleSession() {
134
  const sessionId = sessionStorage.getItem('session_id');
 
1279
  // Function to check if text contains Arabic characters
1280
  function isArabicText(text) {
1281
  return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
1282
+ }
templates/login.html CHANGED
@@ -427,14 +427,14 @@
427
  localStorage.setItem('theme', 'light');
428
  }
429
 
430
- // Check authentication status on page load
431
  async function checkAuthStatus() {
 
 
 
 
 
432
  try {
433
- const token = localStorage.getItem('token');
434
- if (!token) {
435
- console.log('No token found in localStorage');
436
- return false;
437
- }
438
  const response = await fetch('/api/verify-token', {
439
  method: 'GET',
440
  headers: {
@@ -442,17 +442,17 @@
442
  'Accept': 'application/json'
443
  }
444
  });
445
- if (response.ok) {
446
- console.log('User is authenticated, redirecting to /chat');
447
- window.location.href = '/chat';
448
  return true;
449
  } else {
450
- console.log('Token verification failed, clearing token');
451
  localStorage.removeItem('token');
452
  return false;
453
  }
454
  } catch (error) {
455
- console.error('Error checking auth status:', error);
456
  localStorage.removeItem('token');
457
  return false;
458
  }
@@ -566,7 +566,7 @@
566
  });
567
  }
568
 
569
- // Check for OAuth callback on load (for web)
570
  window.addEventListener('load', async () => {
571
  console.log('Page loaded, checking for auth status and OAuth callback');
572
  if (!navigator.onLine) {
@@ -582,38 +582,38 @@
582
  errorMsg.classList.remove('hidden');
583
  console.error('OAuth error from URL:', error);
584
  } else if (code) {
585
- console.log('OAuth code detected in URL, letting server handle callback');
586
- // The server handles /auth/google/callback or /auth/github/callback
587
- // Check auth status to detect the set cookie
588
- const isAuthenticated = await checkAuthStatus();
589
- if (!isAuthenticated) {
590
- // Fetch the callback response to get the access_token
591
- const provider = window.location.pathname.includes('google') ? 'google' : 'github';
592
- try {
593
- const response = await fetch(window.location.href, {
594
- method: 'GET',
595
- headers: { 'Accept': 'application/json' }
596
- });
597
- if (response.ok) {
598
- const data = await response.json();
599
- if (data.access_token) {
600
- localStorage.setItem('token', data.access_token);
601
- console.log(`${provider} login successful, token saved`);
602
  window.location.href = '/chat';
603
  } else {
604
- throw new Error('No access token received');
605
  }
606
  } else {
607
- const errorData = await response.json();
608
- errorMsg.textContent = errorData.detail || `Failed to complete ${provider} login`;
609
- errorMsg.classList.remove('hidden');
610
- console.error(`Failed to complete ${provider} login:`, errorData);
611
  }
612
- } catch (error) {
613
- errorMsg.textContent = `Failed to process ${provider} login. Please try again.`;
 
614
  errorMsg.classList.remove('hidden');
615
- console.error(`Error processing ${provider} callback:`, error);
616
  }
 
 
 
 
617
  }
618
  } else {
619
  await checkAuthStatus();
 
427
  localStorage.setItem('theme', 'light');
428
  }
429
 
430
+ // Check authentication status
431
  async function checkAuthStatus() {
432
+ const token = localStorage.getItem('token');
433
+ if (!token) {
434
+ console.log('No token found in localStorage');
435
+ return false;
436
+ }
437
  try {
 
 
 
 
 
438
  const response = await fetch('/api/verify-token', {
439
  method: 'GET',
440
  headers: {
 
442
  'Accept': 'application/json'
443
  }
444
  });
445
+ const data = await response.json();
446
+ if (response.ok && data.status === 'valid') {
447
+ console.log('Token is valid, user:', data.user);
448
  return true;
449
  } else {
450
+ console.log('Token verification failed:', data.detail || 'Invalid token');
451
  localStorage.removeItem('token');
452
  return false;
453
  }
454
  } catch (error) {
455
+ console.error('Error verifying token:', error);
456
  localStorage.removeItem('token');
457
  return false;
458
  }
 
566
  });
567
  }
568
 
569
+ // Check for OAuth callback on load
570
  window.addEventListener('load', async () => {
571
  console.log('Page loaded, checking for auth status and OAuth callback');
572
  if (!navigator.onLine) {
 
582
  errorMsg.classList.remove('hidden');
583
  console.error('OAuth error from URL:', error);
584
  } else if (code) {
585
+ console.log('OAuth code detected in URL, processing callback');
586
+ const provider = window.location.pathname.includes('google') ? 'google' : 'github';
587
+ try {
588
+ const response = await fetch(window.location.href, {
589
+ method: 'GET',
590
+ headers: { 'Accept': 'application/json' }
591
+ });
592
+ if (response.ok) {
593
+ const data = await response.json();
594
+ if (data.access_token) {
595
+ localStorage.setItem('token', data.access_token);
596
+ console.log(`${provider} login successful, token saved`);
597
+ const isAuthenticated = await checkAuthStatus();
598
+ if (isAuthenticated) {
599
+ console.log('Redirecting to /chat');
 
 
600
  window.location.href = '/chat';
601
  } else {
602
+ throw new Error('Authentication failed after receiving token');
603
  }
604
  } else {
605
+ throw new Error('No access token received');
 
 
 
606
  }
607
+ } else {
608
+ const errorData = await response.json();
609
+ errorMsg.textContent = errorData.detail || `Failed to complete ${provider} login`;
610
  errorMsg.classList.remove('hidden');
611
+ console.error(`Failed to complete ${provider} login:`, errorData);
612
  }
613
+ } catch (error) {
614
+ errorMsg.textContent = `Failed to process ${provider} login. Please try again.`;
615
+ errorMsg.classList.remove('hidden');
616
+ console.error(`Error processing ${provider} callback:`, error);
617
  }
618
  } else {
619
  await checkAuthStatus();