Mark-Lasfar commited on
Commit
4c0bb97
·
1 Parent(s): a7b1069

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

Browse files
Files changed (2) hide show
  1. api/auth.py +25 -50
  2. static/js/chat.js +61 -53
api/auth.py CHANGED
@@ -1,9 +1,9 @@
1
  from fastapi_users import FastAPIUsers
2
- from fastapi_users.authentication import CookieTransport, JWTStrategy, AuthenticationBackend
3
  from httpx_oauth.clients.google import GoogleOAuth2
4
  from httpx_oauth.clients.github import GitHubOAuth2
5
  from fastapi_users.manager import BaseUserManager, IntegerIDMixin
6
- from fastapi import Depends, Request, FastAPI, Response
7
  from fastapi.responses import JSONResponse, RedirectResponse
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
  from sqlalchemy import select
@@ -22,7 +22,8 @@ from api.models import UserRead, UserCreate, UserUpdate
22
  # إعداد اللوقينج
23
  logger = logging.getLogger(__name__)
24
 
25
- cookie_transport = CookieTransport(cookie_max_age=3600, cookie_name="fastapiusersauth")
 
26
 
27
  SECRET = os.getenv("JWT_SECRET")
28
  if not SECRET or len(SECRET) < 32:
@@ -34,7 +35,7 @@ def get_jwt_strategy() -> JWTStrategy:
34
 
35
  auth_backend = AuthenticationBackend(
36
  name="jwt",
37
- transport=cookie_transport,
38
  get_strategy=get_jwt_strategy,
39
  )
40
 
@@ -44,7 +45,6 @@ GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
44
  GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID")
45
  GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET")
46
 
47
- # تحقق من توافر بيانات الاعتماد
48
  if not all([GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET]):
49
  logger.error("One or more OAuth environment variables are missing.")
50
  raise ValueError("All OAuth credentials are required.")
@@ -141,7 +141,6 @@ class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
141
  await self.on_after_login(user, request)
142
  return user
143
 
144
- # استدعاء user manager من get_user_db
145
  async def get_user_manager(user_db: CustomSQLAlchemyUserDatabase = Depends(get_user_db)):
146
  yield UserManager(user_db)
147
 
@@ -152,7 +151,6 @@ fastapi_users = FastAPIUsers[User, int](
152
 
153
  current_active_user = fastapi_users.current_user(active=True, optional=True)
154
 
155
- # دالة مساعدة لتوليد JWT token يدويًا
156
  async def generate_jwt_token(user: User, secret: str, lifetime_seconds: int) -> str:
157
  payload = {
158
  "sub": str(user.id),
@@ -161,8 +159,6 @@ async def generate_jwt_token(user: User, secret: str, lifetime_seconds: int) ->
161
  }
162
  return jwt.encode(payload, secret, algorithm="HS256")
163
 
164
- # --- إضافة custom authorize endpoints (يرجع JSON مع authorization_url) ---
165
-
166
  async def custom_google_authorize(
167
  state: Optional[str] = None,
168
  oauth_client=Depends(lambda: google_oauth_client),
@@ -191,8 +187,6 @@ async def custom_github_authorize(
191
  "authorization_url": authorization_url
192
  }, status_code=200)
193
 
194
- # --- Custom Callback endpoints ---
195
-
196
  async def custom_oauth_callback(
197
  code: str,
198
  state: Optional[str] = None,
@@ -204,15 +198,12 @@ async def custom_oauth_callback(
204
  ):
205
  logger.debug(f"Processing Google callback with code: {code}, state: {state}")
206
  try:
207
- # تحقق من الـ state لو موجود (اختياري لـ CSRF protection)
208
  if state:
209
  logger.debug(f"Received state: {state}")
210
 
211
- # Get access token
212
  token_data = await oauth_client.get_access_token(code, redirect_url)
213
  access_token = token_data["access_token"]
214
 
215
- # Manually fetch user info from Google API
216
  async with httpx.AsyncClient() as client:
217
  user_info_response = await client.get(
218
  "https://www.googleapis.com/oauth2/v3/userinfo",
@@ -234,30 +225,25 @@ async def custom_oauth_callback(
234
  is_verified_by_default=True,
235
  )
236
 
237
- # توليد الـ JWT token يدويًا
238
  token = await generate_jwt_token(user, SECRET, 3600)
 
 
 
 
 
 
 
 
 
 
239
 
240
- # ضبط الـ cookie يدويًا
241
- response.set_cookie(
242
- key="fastapiusersauth",
243
- value=token,
244
- max_age=3600,
245
- httponly=True,
246
- samesite="lax",
247
- secure=True,
248
- )
249
-
250
- # تحقق إذا كان الطلب من التطبيق (Capacitor) أو الويب
251
  is_app = request.headers.get("X-Capacitor-App", False)
252
-
253
  if is_app:
254
- # للتطبيق: إرجاع JSON مع الـ token
255
  return JSONResponse(content={
256
  "message": "Google login successful",
257
  "access_token": token
258
  }, status_code=200)
259
  else:
260
- # للويب: إرجاع redirect إلى /chat مع الـ token كـ query parameter
261
  return RedirectResponse(url=f"/chat?access_token={token}", status_code=303)
262
 
263
  except Exception as e:
@@ -275,15 +261,12 @@ async def custom_github_oauth_callback(
275
  ):
276
  logger.debug(f"Processing GitHub callback with code: {code}, state: {state}")
277
  try:
278
- # تحقق من الـ state لو موجود (اختياري لـ CSRF protection)
279
  if state:
280
  logger.debug(f"Received state: {state}")
281
 
282
- # Get access token
283
  token_data = await oauth_client.get_access_token(code, redirect_url)
284
  access_token = token_data["access_token"]
285
 
286
- # Manually fetch user info from GitHub API
287
  async with httpx.AsyncClient() as client:
288
  user_info_response = await client.get(
289
  "https://api.github.com/user",
@@ -296,7 +279,6 @@ async def custom_github_oauth_callback(
296
  raise ValueError(f"Failed to fetch user info: {user_info_response.text}")
297
  user_info = user_info_response.json()
298
 
299
- # Get email if not in user info
300
  email = user_info.get("email")
301
  if not email:
302
  email_response = await client.get(
@@ -323,37 +305,31 @@ async def custom_github_oauth_callback(
323
  is_verified_by_default=True,
324
  )
325
 
326
- # توليد الـ JWT token يدويًا
327
  token = await generate_jwt_token(user, SECRET, 3600)
 
 
 
 
 
 
 
 
 
 
328
 
329
- # ضبط الـ cookie يدويًا
330
- response.set_cookie(
331
- key="fastapiusersauth",
332
- value=token,
333
- max_age=3600,
334
- httponly=True,
335
- samesite="lax",
336
- secure=True,
337
- )
338
-
339
- # تحقق إذا كان الطلب من التطبيق (Capacitor) أو الويب
340
  is_app = request.headers.get("X-Capacitor-App", False)
341
-
342
  if is_app:
343
- # للتطبيق: إرجاع JSON مع الـ token
344
  return JSONResponse(content={
345
  "message": "GitHub login successful",
346
  "access_token": token
347
  }, status_code=200)
348
  else:
349
- # للويب: إرجاع redirect إلى /chat مع الـ token كـ query parameter
350
  return RedirectResponse(url=f"/chat?access_token={token}", status_code=303)
351
 
352
  except Exception as e:
353
  logger.error(f"Error in GitHub OAuth callback: {str(e)}")
354
  return JSONResponse(content={"detail": str(e)}, status_code=400)
355
 
356
- # تضمين الراوترات داخل التطبيق
357
  def get_auth_router(app: FastAPI):
358
  app.include_router(fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"])
359
  app.include_router(fastapi_users.get_register_router(UserRead, UserCreate), prefix="/auth", tags=["auth"])
@@ -361,7 +337,6 @@ def get_auth_router(app: FastAPI):
361
  app.include_router(fastapi_users.get_verify_router(UserRead), prefix="/auth", tags=["auth"])
362
  app.include_router(fastapi_users.get_users_router(UserRead, UserUpdate), prefix="/users", tags=["users"])
363
 
364
- # إضافة الـ custom OAuth endpoints يدويًا
365
  app.get("/auth/google/authorize")(custom_google_authorize)
366
  app.get("/auth/google/callback")(custom_oauth_callback)
367
  app.get("/auth/github/authorize")(custom_github_authorize)
 
1
  from fastapi_users import FastAPIUsers
2
+ from fastapi_users.authentication import BearerTransport, JWTStrategy, AuthenticationBackend
3
  from httpx_oauth.clients.google import GoogleOAuth2
4
  from httpx_oauth.clients.github import GitHubOAuth2
5
  from fastapi_users.manager import BaseUserManager, IntegerIDMixin
6
+ from fastapi import Depends, Request, Response, FastAPI
7
  from fastapi.responses import JSONResponse, RedirectResponse
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
  from sqlalchemy import select
 
22
  # إعداد اللوقينج
23
  logger = logging.getLogger(__name__)
24
 
25
+ # استخدام BearerTransport بدل CookieTransport
26
+ bearer_transport = BearerTransport(tokenUrl="/auth/jwt/login")
27
 
28
  SECRET = os.getenv("JWT_SECRET")
29
  if not SECRET or len(SECRET) < 32:
 
35
 
36
  auth_backend = AuthenticationBackend(
37
  name="jwt",
38
+ transport=bearer_transport, # تغيير إلى BearerTransport
39
  get_strategy=get_jwt_strategy,
40
  )
41
 
 
45
  GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID")
46
  GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET")
47
 
 
48
  if not all([GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET]):
49
  logger.error("One or more OAuth environment variables are missing.")
50
  raise ValueError("All OAuth credentials are required.")
 
141
  await self.on_after_login(user, request)
142
  return user
143
 
 
144
  async def get_user_manager(user_db: CustomSQLAlchemyUserDatabase = Depends(get_user_db)):
145
  yield UserManager(user_db)
146
 
 
151
 
152
  current_active_user = fastapi_users.current_user(active=True, optional=True)
153
 
 
154
  async def generate_jwt_token(user: User, secret: str, lifetime_seconds: int) -> str:
155
  payload = {
156
  "sub": str(user.id),
 
159
  }
160
  return jwt.encode(payload, secret, algorithm="HS256")
161
 
 
 
162
  async def custom_google_authorize(
163
  state: Optional[str] = None,
164
  oauth_client=Depends(lambda: google_oauth_client),
 
187
  "authorization_url": authorization_url
188
  }, status_code=200)
189
 
 
 
190
  async def custom_oauth_callback(
191
  code: str,
192
  state: Optional[str] = None,
 
198
  ):
199
  logger.debug(f"Processing Google callback with code: {code}, state: {state}")
200
  try:
 
201
  if state:
202
  logger.debug(f"Received state: {state}")
203
 
 
204
  token_data = await oauth_client.get_access_token(code, redirect_url)
205
  access_token = token_data["access_token"]
206
 
 
207
  async with httpx.AsyncClient() as client:
208
  user_info_response = await client.get(
209
  "https://www.googleapis.com/oauth2/v3/userinfo",
 
225
  is_verified_by_default=True,
226
  )
227
 
 
228
  token = await generate_jwt_token(user, SECRET, 3600)
229
+
230
+ # ما نضبطش cookie لأننا بنستخدم Bearer token
231
+ # response.set_cookie(
232
+ # key="fastapiusersauth",
233
+ # value=token,
234
+ # max_age=3600,
235
+ # httponly=True,
236
+ # samesite="lax",
237
+ # secure=True,
238
+ # )
239
 
 
 
 
 
 
 
 
 
 
 
 
240
  is_app = request.headers.get("X-Capacitor-App", False)
 
241
  if is_app:
 
242
  return JSONResponse(content={
243
  "message": "Google login successful",
244
  "access_token": token
245
  }, status_code=200)
246
  else:
 
247
  return RedirectResponse(url=f"/chat?access_token={token}", status_code=303)
248
 
249
  except Exception as e:
 
261
  ):
262
  logger.debug(f"Processing GitHub callback with code: {code}, state: {state}")
263
  try:
 
264
  if state:
265
  logger.debug(f"Received state: {state}")
266
 
 
267
  token_data = await oauth_client.get_access_token(code, redirect_url)
268
  access_token = token_data["access_token"]
269
 
 
270
  async with httpx.AsyncClient() as client:
271
  user_info_response = await client.get(
272
  "https://api.github.com/user",
 
279
  raise ValueError(f"Failed to fetch user info: {user_info_response.text}")
280
  user_info = user_info_response.json()
281
 
 
282
  email = user_info.get("email")
283
  if not email:
284
  email_response = await client.get(
 
305
  is_verified_by_default=True,
306
  )
307
 
 
308
  token = await generate_jwt_token(user, SECRET, 3600)
309
+
310
+ # ما نضبطش cookie لأننا بنستخدم Bearer token
311
+ # response.set_cookie(
312
+ # key="fastapiusersauth",
313
+ # value=token,
314
+ # max_age=3600,
315
+ # httponly=True,
316
+ # samesite="lax",
317
+ # secure=True,
318
+ # )
319
 
 
 
 
 
 
 
 
 
 
 
 
320
  is_app = request.headers.get("X-Capacitor-App", False)
 
321
  if is_app:
 
322
  return JSONResponse(content={
323
  "message": "GitHub login successful",
324
  "access_token": token
325
  }, status_code=200)
326
  else:
 
327
  return RedirectResponse(url=f"/chat?access_token={token}", status_code=303)
328
 
329
  except Exception as e:
330
  logger.error(f"Error in GitHub OAuth callback: {str(e)}")
331
  return JSONResponse(content={"detail": str(e)}, status_code=400)
332
 
 
333
  def get_auth_router(app: FastAPI):
334
  app.include_router(fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"])
335
  app.include_router(fastapi_users.get_register_router(UserRead, UserCreate), prefix="/auth", tags=["auth"])
 
337
  app.include_router(fastapi_users.get_verify_router(UserRead), prefix="/auth", tags=["auth"])
338
  app.include_router(fastapi_users.get_users_router(UserRead, UserUpdate), prefix="/users", tags=["users"])
339
 
 
340
  app.get("/auth/google/authorize")(custom_google_authorize)
341
  app.get("/auth/google/callback")(custom_oauth_callback)
342
  app.get("/auth/github/authorize")(custom_github_authorize)
static/js/chat.js CHANGED
@@ -54,20 +54,15 @@ let abortController = null;
54
 
55
 
56
  async function checkAuth() {
57
- // تحقق من وجود access_token في query parameters
58
  const urlParams = new URLSearchParams(window.location.search);
59
  const accessTokenFromUrl = urlParams.get('access_token');
60
  if (accessTokenFromUrl) {
61
  console.log('Access token found in URL, saving to localStorage');
62
  localStorage.setItem('token', accessTokenFromUrl);
63
- // إزالة access_token من الـ URL عشان الأمان
64
  window.history.replaceState({}, document.title, '/chat');
65
  }
66
 
67
- // تحقق من وجود token في localStorage
68
  let token = localStorage.getItem('token');
69
-
70
- // لو مفيش token في localStorage، حاول استخرج الـ token من الـ cookie
71
  if (!token && typeof Cookies !== 'undefined') {
72
  token = Cookies.get('fastapiusersauth');
73
  if (token) {
@@ -111,6 +106,7 @@ async function checkAuth() {
111
  }
112
  }
113
 
 
114
  async function handleSession() {
115
  const sessionId = sessionStorage.getItem('session_id');
116
  if (!sessionId) {
@@ -123,67 +119,79 @@ async function handleSession() {
123
  return sessionId;
124
  }
125
 
 
126
  window.addEventListener('load', async () => {
127
  console.log('Chat page loaded, checking authentication');
128
- AOS.init({
129
- duration: 800,
130
- easing: 'ease-out-cubic',
131
- once: true,
132
- offset: 50,
133
- });
 
134
 
135
- // Force chat view to be visible immediately
136
- enterChatView(true);
137
 
138
- const authResult = await checkAuth();
139
- const userInfoElement = document.getElementById('user-info');
140
- if (authResult.authenticated) {
141
- console.log('User authenticated:', authResult.user);
142
- if (userInfoElement) {
143
- userInfoElement.textContent = `Welcome, ${authResult.user.email}`;
144
- }
145
- if (currentConversationId) {
146
- console.log('Authenticated user, loading conversation with ID:', currentConversationId);
147
- await loadConversation(currentConversationId);
148
- }
149
- } else {
150
- console.log('User not authenticated, handling as anonymous');
151
- if (userInfoElement) {
152
- userInfoElement.textContent = 'Anonymous';
153
- }
154
- await handleSession();
155
- if (conversationHistory.length > 0) {
156
- console.log('Restoring conversation history from sessionStorage:', conversationHistory);
157
- conversationHistory.forEach(msg => {
158
- console.log('Adding message from history:', msg);
159
- addMsg(msg.role, msg.content);
160
- });
161
  } else {
162
- console.log('No conversation history, starting fresh');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  }
164
- }
165
 
166
- autoResizeTextarea();
167
- updateSendButtonState();
168
- if (uiElements.swipeHint) {
169
- setTimeout(() => {
170
- uiElements.swipeHint.style.display = 'none';
171
- }, 3000);
 
 
 
 
 
 
172
  }
173
- setupTouchGestures();
174
  });
175
-
176
  // Update send button state
177
  function updateSendButtonState() {
178
- if (uiElements.sendBtn && uiElements.input && uiElements.fileInput && uiElements.audioInput) {
179
- const hasInput = uiElements.input.value.trim() !== '' ||
180
- uiElements.fileInput.files.length > 0 ||
181
- uiElements.audioInput.files.length > 0;
182
- uiElements.sendBtn.disabled = !hasInput || isRequestActive || isRecording;
183
- console.log('Send button state updated:', { hasInput, isRequestActive, isRecording, disabled: uiElements.sendBtn.disabled });
184
- }
 
 
185
  }
186
 
 
187
  // Render markdown content with RTL support
188
  function renderMarkdown(el) {
189
  const raw = el.dataset.text || '';
 
54
 
55
 
56
  async function checkAuth() {
 
57
  const urlParams = new URLSearchParams(window.location.search);
58
  const accessTokenFromUrl = urlParams.get('access_token');
59
  if (accessTokenFromUrl) {
60
  console.log('Access token found in URL, saving to localStorage');
61
  localStorage.setItem('token', accessTokenFromUrl);
 
62
  window.history.replaceState({}, document.title, '/chat');
63
  }
64
 
 
65
  let token = localStorage.getItem('token');
 
 
66
  if (!token && typeof Cookies !== 'undefined') {
67
  token = Cookies.get('fastapiusersauth');
68
  if (token) {
 
106
  }
107
  }
108
 
109
+
110
  async function handleSession() {
111
  const sessionId = sessionStorage.getItem('session_id');
112
  if (!sessionId) {
 
119
  return sessionId;
120
  }
121
 
122
+
123
  window.addEventListener('load', async () => {
124
  console.log('Chat page loaded, checking authentication');
125
+ try {
126
+ AOS.init({
127
+ duration: 800,
128
+ easing: 'ease-out-cubic',
129
+ once: true,
130
+ offset: 50,
131
+ });
132
 
133
+ enterChatView(true);
 
134
 
135
+ const authResult = await checkAuth();
136
+ const userInfoElement = document.getElementById('user-info');
137
+ if (authResult.authenticated) {
138
+ console.log('User authenticated:', authResult.user);
139
+ if (userInfoElement) {
140
+ userInfoElement.textContent = `Welcome, ${authResult.user.email}`;
141
+ } else {
142
+ console.warn('user-info element not found');
143
+ }
144
+ if (typeof currentConversationId !== 'undefined' && currentConversationId) {
145
+ console.log('Authenticated user, loading conversation with ID:', currentConversationId);
146
+ await loadConversation(currentConversationId);
147
+ }
 
 
 
 
 
 
 
 
 
 
148
  } else {
149
+ console.log('User not authenticated, handling as anonymous');
150
+ if (userInfoElement) {
151
+ userInfoElement.textContent = 'Anonymous';
152
+ } else {
153
+ console.warn('user-info element not found');
154
+ }
155
+ await handleSession();
156
+ if (typeof conversationHistory !== 'undefined' && conversationHistory.length > 0) {
157
+ console.log('Restoring conversation history from sessionStorage:', conversationHistory);
158
+ conversationHistory.forEach(msg => {
159
+ console.log('Adding message from history:', msg);
160
+ addMsg(msg.role, msg.content);
161
+ });
162
+ } else {
163
+ console.log('No conversation history, starting fresh');
164
+ }
165
  }
 
166
 
167
+ autoResizeTextarea();
168
+ updateSendButtonState();
169
+ if (uiElements.swipeHint) {
170
+ setTimeout(() => {
171
+ uiElements.swipeHint.style.display = 'none';
172
+ }, 3000);
173
+ } else {
174
+ console.warn('swipeHint element not found');
175
+ }
176
+ setupTouchGestures();
177
+ } catch (error) {
178
+ console.error('Error in window.load handler:', error);
179
  }
 
180
  });
 
181
  // Update send button state
182
  function updateSendButtonState() {
183
+ if (uiElements.sendBtn && uiElements.input && uiElements.fileInput && uiElements.audioInput) {
184
+ const hasInput = uiElements.input.value.trim() !== '' ||
185
+ uiElements.fileInput.files.length > 0 ||
186
+ uiElements.audioInput.files.length > 0;
187
+ uiElements.sendBtn.disabled = !hasInput || isRequestActive || isRecording;
188
+ console.log('Send button state updated:', { hasInput, isRequestActive, isRecording, disabled: uiElements.sendBtn.disabled });
189
+ } else {
190
+ console.warn('One or more uiElements are missing:', uiElements);
191
+ }
192
  }
193
 
194
+
195
  // Render markdown content with RTL support
196
  function renderMarkdown(el) {
197
  const raw = el.dataset.text || '';