Mark-Lasfar commited on
Commit
c0bbb85
·
1 Parent(s): 06e9f8e

Update Model

Browse files
Files changed (2) hide show
  1. api/auth.py +214 -193
  2. main.py +7 -0
api/auth.py CHANGED
@@ -3,9 +3,10 @@ from fastapi_users.authentication import CookieTransport, JWTStrategy, Authentic
3
  from fastapi_users.db import SQLAlchemyUserDatabase
4
  from httpx_oauth.clients.google import GoogleOAuth2
5
  from httpx_oauth.clients.github import GitHubOAuth2
6
- from api.database import User, OAuthAccount, get_user_db
7
  from fastapi_users.manager import BaseUserManager, IntegerIDMixin
8
- from fastapi import Depends, Request, FastAPI, HTTPException
 
9
  from fastapi_users.models import UP
10
  from typing import Optional
11
  import os
@@ -13,6 +14,10 @@ import logging
13
  from api.database import User, OAuthAccount
14
  from api.models import UserRead, UserCreate, UserUpdate
15
  from sqlalchemy import select
 
 
 
 
16
 
17
  # Setup logging
18
  logger = logging.getLogger(__name__)
@@ -43,9 +48,7 @@ GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET")
43
 
44
  # Log OAuth credentials status
45
  logger.info("GOOGLE_CLIENT_ID is set: %s", bool(GOOGLE_CLIENT_ID))
46
- logger.info("GOOGLE_CLIENT_SECRET is set: %s", bool(GOOGLE_CLIENT_SECRET))
47
  logger.info("GITHUB_CLIENT_ID is set: %s", bool(GITHUB_CLIENT_ID))
48
- logger.info("GITHUB_CLIENT_SECRET is set: %s", bool(GITHUB_CLIENT_SECRET))
49
 
50
  if not all([GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET]):
51
  logger.error("One or more OAuth environment variables are missing.")
@@ -54,214 +57,219 @@ if not all([GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GITHUB_CLIENT_ID, GITHUB_CLI
54
  google_oauth_client = GoogleOAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET)
55
  github_oauth_client = GitHubOAuth2(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET)
56
 
 
 
 
57
  class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
58
  reset_password_token_secret = SECRET
59
  verification_token_secret = SECRET
60
 
61
- async def oauth_callback(
62
- self,
63
- oauth_name: str,
64
- access_token: str,
65
- account_id: str,
66
- account_email: str,
67
- expires_at: Optional[int] = None,
68
- refresh_token: Optional[str] = None,
69
- request: Optional[Request] = None,
70
- *,
71
- associate_by_email: bool = False,
72
- is_verified_by_default: bool = False,
73
- ) -> UP:
74
- logger.info(f"Processing OAuth callback for {oauth_name} with account_id: {account_id}, email: {account_email}")
75
-
76
- # Validate inputs
77
- if not account_email or not account_id:
78
- logger.error(f"Invalid OAuth callback data: email={account_email}, account_id={account_id}")
79
- raise ValueError("Invalid OAuth callback data: email and account_id are required.")
80
 
81
- try:
82
- # Custom query to fetch existing OAuth account (sync)
83
- statement = select(OAuthAccount).where(
84
- (OAuthAccount.oauth_name == oauth_name) & (OAuthAccount.account_id == account_id)
85
- )
86
- result = self.user_db.session.execute(statement)
87
- existing_oauth_account = result.scalars().first()
88
 
89
- user = None
90
- if existing_oauth_account is not None:
91
- logger.info(f"Found existing OAuth account for {oauth_name}, account_id: {account_id}")
92
- user = existing_oauth_account.user
93
- if user is None:
94
- logger.warning(f"No user associated with existing OAuth account. Creating new user.")
95
- # Create new user and link (sync)
96
- user_dict = {
97
- "email": account_email,
98
- "hashed_password": self.password_helper.hash("dummy_password"),
99
- "is_active": True,
100
- "is_verified": is_verified_by_default,
101
- }
102
- user = User(**user_dict)
103
- self.user_db.session.add(user)
104
- self.user_db.session.commit()
105
- self.user_db.session.refresh(user)
106
- existing_oauth_account.user_id = user.id
107
- # Update the existing account safely
108
- try:
109
- self.user_db.session.commit()
110
- logger.info(f"Linked new user to existing OAuth account: {user.email}")
111
- except Exception as update_e:
112
- self.user_db.session.rollback()
113
- logger.error(f"Failed to update existing OAuth account: {update_e}")
114
- raise ValueError(f"Failed to link user to OAuth account: {update_e}")
115
- else:
116
- # Update existing OAuth account if needed (handle None return from update_oauth_account)
117
- try:
118
- updated_user = self.user_db.update_oauth_account(user, existing_oauth_account, {
119
- "oauth_name": oauth_name,
120
- "access_token": access_token,
121
- "account_id": account_id,
122
- "account_email": account_email,
123
- "expires_at": expires_at,
124
- "refresh_token": refresh_token,
125
- }) # sync
126
- if updated_user is None:
127
- logger.warning("update_oauth_account returned None. Using original user.")
128
- updated_user = user
129
- user = updated_user
130
- except Exception as update_e:
131
- logger.error(f"Error in update_oauth_account: {update_e}")
132
- # Fallback: just use the existing user
133
- user = user # Keep original
134
- elif associate_by_email:
135
- logger.info(f"Associating by email: {account_email}")
136
- # Safe get_by_email (sync) - NO AWAIT
137
- user = self.user_db.get_by_email(account_email)
138
- if user is None:
139
- logger.info(f"No user found for email {account_email}. Creating new user.")
140
- user_dict = {
141
- "email": account_email,
142
- "hashed_password": self.password_helper.hash("dummy_password"),
143
- "is_active": True,
144
- "is_verified": is_verified_by_default,
145
- }
146
- try:
147
- # Create user manually to avoid any async issues
148
- user = User(**user_dict)
149
- self.user_db.session.add(user)
150
- self.user_db.session.commit()
151
- self.user_db.session.refresh(user)
152
- logger.info(f"Created new user for email: {user.email}")
153
- except Exception as create_e:
154
- self.user_db.session.rollback()
155
- logger.error(f"Failed to create user for email {account_email}: {create_e}")
156
- raise ValueError(f"Failed to create user: {create_e}")
157
-
158
- # Create and link OAuth account
159
  oauth_account = OAuthAccount(
160
- oauth_name=oauth_name,
161
- access_token=access_token,
162
  account_id=account_id,
163
  account_email=account_email,
164
- expires_at=expires_at,
165
- refresh_token=refresh_token,
166
  user_id=user.id
167
  )
168
- try:
169
- self.user_db.session.add(oauth_account)
170
- self.user_db.session.commit()
171
- logger.info(f"Associated OAuth account with user: {user.email}")
172
- except Exception as link_e:
173
- self.user_db.session.rollback()
174
- logger.error(f"Failed to associate OAuth account: {link_e}")
175
- raise ValueError(f"Failed to link OAuth account: {link_e}")
176
  else:
177
- # Create new user (default case)
178
- logger.info(f"Creating new user for email: {account_email}")
179
- user_dict = {
180
- "email": account_email,
181
- "hashed_password": self.password_helper.hash("dummy_password"),
182
- "is_active": True,
183
- "is_verified": is_verified_by_default,
184
- }
185
- try:
186
- # Create user manually to avoid any async issues
187
- user = User(**user_dict)
188
- self.user_db.session.add(user)
189
- self.user_db.session.commit()
190
- self.user_db.session.refresh(user)
191
- logger.info(f"Created new user: {user.email}")
192
- except Exception as create_e:
193
- self.user_db.session.rollback()
194
- logger.error(f"Failed to create user for email {account_email}: {create_e}")
195
- raise ValueError(f"Failed to create user: {create_e}")
196
-
197
- # Create and link OAuth account
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  oauth_account = OAuthAccount(
199
- oauth_name=oauth_name,
200
- access_token=access_token,
201
  account_id=account_id,
202
  account_email=account_email,
203
- expires_at=expires_at,
204
- refresh_token=refresh_token,
205
  user_id=user.id
206
  )
207
- try:
208
- self.user_db.session.add(oauth_account)
209
- self.user_db.session.commit()
210
- logger.info(f"Linked OAuth account to new user: {user.email}")
211
- except Exception as link_e:
212
- self.user_db.session.rollback()
213
- logger.error(f"Failed to link OAuth account: {link_e}")
214
- raise ValueError(f"Failed to link OAuth account: {link_e}")
215
-
216
- # Safe check for user.is_active - التحقق الآمن من is_active
217
- if user is None:
218
- logger.error("User is still None after all attempts. Cannot proceed.")
219
- raise ValueError("Failed to retrieve or create user.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
- if not user.is_active:
222
- logger.warning(f"User {user.email} is inactive. Activating...")
223
- user.is_active = True
224
- try:
225
- self.user_db.session.commit()
226
- logger.info(f"Activated inactive user: {user.email}")
227
- except Exception as activate_e:
228
- self.user_db.session.rollback()
229
- logger.error(f"Failed to activate user: {activate_e}")
230
- raise ValueError(f"Failed to activate user: {activate_e}")
231
-
232
- logger.info(f"Returning user: {user.email} (active: {user.is_active})")
233
- return await self.on_after_login(user, request)
234
-
235
  except Exception as e:
236
- # Rollback on any error
237
- if self.user_db.session.in_transaction():
238
- self.user_db.session.rollback()
239
- logger.error(f"OAuth callback failed: {e}")
240
- raise
241
 
242
- async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
243
- yield UserManager(user_db)
244
-
245
- from fastapi_users.router.oauth import get_oauth_router
246
-
247
- google_oauth_router = get_oauth_router(
248
- google_oauth_client,
249
- auth_backend,
250
- get_user_manager,
251
- state_secret=SECRET,
252
- associate_by_email=True,
253
- redirect_url="https://mgzon-mgzon-app.hf.space/auth/google/callback",
254
- )
255
-
256
- github_oauth_router = get_oauth_router(
257
- github_oauth_client,
258
- auth_backend,
259
- get_user_manager,
260
- state_secret=SECRET,
261
- associate_by_email=True,
262
- redirect_url="https://mgzon-mgzon-app.hf.space/auth/github/callback",
263
- )
264
 
 
265
  fastapi_users = FastAPIUsers[User, int](
266
  get_user_manager,
267
  [auth_backend],
@@ -270,10 +278,23 @@ fastapi_users = FastAPIUsers[User, int](
270
  current_active_user = fastapi_users.current_user(active=True, optional=True)
271
 
272
  def get_auth_router(app: FastAPI):
273
- app.include_router(google_oauth_router, prefix="/auth/google", tags=["auth"])
274
- app.include_router(github_oauth_router, prefix="/auth/github", tags=["auth"])
 
 
 
275
  app.include_router(fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"])
276
  app.include_router(fastapi_users.get_register_router(UserRead, UserCreate), prefix="/auth", tags=["auth"])
277
  app.include_router(fastapi_users.get_reset_password_router(), prefix="/auth", tags=["auth"])
278
  app.include_router(fastapi_users.get_verify_router(UserRead), prefix="/auth", tags=["auth"])
279
  app.include_router(fastapi_users.get_users_router(UserRead, UserUpdate), prefix="/users", tags=["users"])
 
 
 
 
 
 
 
 
 
 
 
3
  from fastapi_users.db import SQLAlchemyUserDatabase
4
  from httpx_oauth.clients.google import GoogleOAuth2
5
  from httpx_oauth.clients.github import GitHubOAuth2
6
+ from api.database import User, OAuthAccount, get_user_db, get_db
7
  from fastapi_users.manager import BaseUserManager, IntegerIDMixin
8
+ from fastapi import Depends, Request, FastAPI, HTTPException, status
9
+ from fastapi.responses import RedirectResponse
10
  from fastapi_users.models import UP
11
  from typing import Optional
12
  import os
 
14
  from api.database import User, OAuthAccount
15
  from api.models import UserRead, UserCreate, UserUpdate
16
  from sqlalchemy import select
17
+ from sqlalchemy.orm import Session
18
+ from datetime import datetime
19
+ import secrets
20
+ import json
21
 
22
  # Setup logging
23
  logger = logging.getLogger(__name__)
 
48
 
49
  # Log OAuth credentials status
50
  logger.info("GOOGLE_CLIENT_ID is set: %s", bool(GOOGLE_CLIENT_ID))
 
51
  logger.info("GITHUB_CLIENT_ID is set: %s", bool(GITHUB_CLIENT_ID))
 
52
 
53
  if not all([GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET]):
54
  logger.error("One or more OAuth environment variables are missing.")
 
57
  google_oauth_client = GoogleOAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET)
58
  github_oauth_client = GitHubOAuth2(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET)
59
 
60
+ # OAuth state secret
61
+ OAUTH_STATE_SECRET = SECRET
62
+
63
  class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
64
  reset_password_token_secret = SECRET
65
  verification_token_secret = SECRET
66
 
67
+ # Standard user manager methods (for JWT only)
68
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
+ async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
71
+ yield UserManager(user_db)
72
+
73
+ # Custom OAuth routers without fastapi_users
74
+ from fastapi import APIRouter
 
 
75
 
76
+ def get_custom_oauth_router():
77
+ router = APIRouter()
78
+
79
+ # Store state in session
80
+ @router.get("/google/authorize")
81
+ async def google_authorize(request: Request):
82
+ state = secrets.token_urlsafe(32)
83
+ request.session["oauth_state"] = state
84
+ request.session["oauth_provider"] = "google"
85
+ redirect_uri = "https://mgzon-mgzon-app.hf.space/auth/google/callback"
86
+ url = google_oauth_client.get_authorization_url(
87
+ redirect_uri, state=state, scope=["openid", "email", "profile"]
88
+ )
89
+ return RedirectResponse(url)
90
+
91
+ @router.get("/google/callback")
92
+ async def google_callback(request: Request, db: Session = Depends(get_db)):
93
+ state = request.query_params.get("state")
94
+ code = request.query_params.get("code")
95
+
96
+ # Verify state
97
+ if not state or state != request.session.get("oauth_state"):
98
+ logger.error("OAuth state mismatch")
99
+ return RedirectResponse("/login?error=Invalid state")
100
+
101
+ provider = request.session.get("oauth_provider")
102
+ if provider != "google":
103
+ return RedirectResponse("/login?error=Invalid provider")
104
+
105
+ try:
106
+ # Get token and user info
107
+ token = await google_oauth_client.get_access_token(code, redirect_uri="https://mgzon-mgzon-app.hf.space/auth/google/callback")
108
+ user_info = await google_oauth_client.get_id_email(token)
109
+
110
+ account_id = user_info["id"]
111
+ account_email = user_info["email"]
112
+ logger.info(f"Google OAuth success: account_id={account_id}, email={account_email}")
113
+
114
+ # Find or create user
115
+ user = db.query(User).filter(User.email == account_email).first()
116
+ if user is None:
117
+ # Create new user
118
+ user = User(
119
+ email=account_email,
120
+ hashed_password=UserManager(None).password_helper.hash("dummy_password"), # Dummy password
121
+ is_active=True,
122
+ is_verified=True,
123
+ )
124
+ db.add(user)
125
+ db.commit()
126
+ db.refresh(user)
127
+ logger.info(f"Created new user: {user.email}")
128
+
129
+ # Find or create OAuth account
130
+ oauth_account = db.query(OAuthAccount).filter(
131
+ OAuthAccount.oauth_name == "google",
132
+ OAuthAccount.account_id == account_id
133
+ ).first()
134
+
135
+ if oauth_account is None:
 
 
 
 
 
 
 
 
 
 
136
  oauth_account = OAuthAccount(
137
+ oauth_name="google",
138
+ access_token=token["access_token"],
139
  account_id=account_id,
140
  account_email=account_email,
 
 
141
  user_id=user.id
142
  )
143
+ db.add(oauth_account)
144
+ db.commit()
145
+ logger.info(f"Created OAuth account for user: {user.email}")
 
 
 
 
 
146
  else:
147
+ # Update existing OAuth account
148
+ oauth_account.access_token = token["access_token"]
149
+ oauth_account.account_email = account_email
150
+ db.commit()
151
+ logger.info(f"Updated OAuth account for user: {user.email}")
152
+
153
+ # Create JWT token using fastapi_users
154
+ user_manager = UserManager(get_user_db(db))
155
+ jwt_token = await user_manager.create_access_token(user)
156
+
157
+ # Set cookie
158
+ response = RedirectResponse("/chat")
159
+ response.set_cookie(
160
+ key="access_token",
161
+ value=jwt_token,
162
+ max_age=3600,
163
+ httponly=True,
164
+ secure=True,
165
+ samesite="lax"
166
+ )
167
+
168
+ logger.info(f"OAuth login successful for user: {user.email}")
169
+ return response
170
+
171
+ except Exception as e:
172
+ logger.error(f"Google OAuth callback failed: {e}")
173
+ return RedirectResponse(f"/login?error={str(e)}")
174
+
175
+ @router.get("/github/authorize")
176
+ async def github_authorize(request: Request):
177
+ state = secrets.token_urlsafe(32)
178
+ request.session["oauth_state"] = state
179
+ request.session["oauth_provider"] = "github"
180
+ redirect_uri = "https://mgzon-mgzon-app.hf.space/auth/github/callback"
181
+ url = github_oauth_client.get_authorization_url(
182
+ redirect_uri, state=state, scope=["user:email"]
183
+ )
184
+ return RedirectResponse(url)
185
+
186
+ @router.get("/github/callback")
187
+ async def github_callback(request: Request, db: Session = Depends(get_db)):
188
+ state = request.query_params.get("state")
189
+ code = request.query_params.get("code")
190
+
191
+ # Verify state
192
+ if not state or state != request.session.get("oauth_state"):
193
+ logger.error("OAuth state mismatch")
194
+ return RedirectResponse("/login?error=Invalid state")
195
+
196
+ provider = request.session.get("oauth_provider")
197
+ if provider != "github":
198
+ return RedirectResponse("/login?error=Invalid provider")
199
+
200
+ try:
201
+ # Get token and user info
202
+ token = await github_oauth_client.get_access_token(code, redirect_uri="https://mgzon-mgzon-app.hf.space/auth/github/callback")
203
+ user_info = await github_oauth_client.get_id_email(token)
204
+
205
+ account_id = user_info["id"]
206
+ account_email = user_info["email"]
207
+ logger.info(f"GitHub OAuth success: account_id={account_id}, email={account_email}")
208
+
209
+ # Find or create user
210
+ user = db.query(User).filter(User.email == account_email).first()
211
+ if user is None:
212
+ # Create new user
213
+ user = User(
214
+ email=account_email,
215
+ hashed_password=UserManager(None).password_helper.hash("dummy_password"), # Dummy password
216
+ is_active=True,
217
+ is_verified=True,
218
+ )
219
+ db.add(user)
220
+ db.commit()
221
+ db.refresh(user)
222
+ logger.info(f"Created new user: {user.email}")
223
+
224
+ # Find or create OAuth account
225
+ oauth_account = db.query(OAuthAccount).filter(
226
+ OAuthAccount.oauth_name == "github",
227
+ OAuthAccount.account_id == account_id
228
+ ).first()
229
+
230
+ if oauth_account is None:
231
  oauth_account = OAuthAccount(
232
+ oauth_name="github",
233
+ access_token=token["access_token"],
234
  account_id=account_id,
235
  account_email=account_email,
 
 
236
  user_id=user.id
237
  )
238
+ db.add(oauth_account)
239
+ db.commit()
240
+ logger.info(f"Created OAuth account for user: {user.email}")
241
+ else:
242
+ # Update existing OAuth account
243
+ oauth_account.access_token = token["access_token"]
244
+ oauth_account.account_email = account_email
245
+ db.commit()
246
+ logger.info(f"Updated OAuth account for user: {user.email}")
247
+
248
+ # Create JWT token using fastapi_users
249
+ user_manager = UserManager(get_user_db(db))
250
+ jwt_token = await user_manager.create_access_token(user)
251
+
252
+ # Set cookie
253
+ response = RedirectResponse("/chat")
254
+ response.set_cookie(
255
+ key="access_token",
256
+ value=jwt_token,
257
+ max_age=3600,
258
+ httponly=True,
259
+ secure=True,
260
+ samesite="lax"
261
+ )
262
+
263
+ logger.info(f"OAuth login successful for user: {user.email}")
264
+ return response
265
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  except Exception as e:
267
+ logger.error(f"GitHub OAuth callback failed: {e}")
268
+ return RedirectResponse(f"/login?error={str(e)}")
 
 
 
269
 
270
+ return router
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
+ # Standard fastapi_users setup for JWT only (no OAuth)
273
  fastapi_users = FastAPIUsers[User, int](
274
  get_user_manager,
275
  [auth_backend],
 
278
  current_active_user = fastapi_users.current_user(active=True, optional=True)
279
 
280
  def get_auth_router(app: FastAPI):
281
+ # Add custom OAuth router
282
+ custom_oauth_router = get_custom_oauth_router()
283
+ app.include_router(custom_oauth_router, prefix="/auth", tags=["auth"])
284
+
285
+ # Add standard fastapi_users routes (without OAuth)
286
  app.include_router(fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"])
287
  app.include_router(fastapi_users.get_register_router(UserRead, UserCreate), prefix="/auth", tags=["auth"])
288
  app.include_router(fastapi_users.get_reset_password_router(), prefix="/auth", tags=["auth"])
289
  app.include_router(fastapi_users.get_verify_router(UserRead), prefix="/auth", tags=["auth"])
290
  app.include_router(fastapi_users.get_users_router(UserRead, UserUpdate), prefix="/users", tags=["users"])
291
+
292
+ # Add logout endpoint
293
+ @get_auth_router
294
+ def logout_endpoint():
295
+ @router.get("/logout")
296
+ async def logout(request: Request):
297
+ request.session.clear()
298
+ response = RedirectResponse("/login")
299
+ response.delete_cookie("access_token")
300
+ return response
main.py CHANGED
@@ -109,6 +109,13 @@ app.add_middleware(
109
  app.include_router(api_router)
110
  get_auth_router(app) # Add OAuth and auth routers
111
 
 
 
 
 
 
 
 
112
  # Debug routes endpoint
113
  @app.get("/debug/routes", response_class=PlainTextResponse)
114
  async def debug_routes():
 
109
  app.include_router(api_router)
110
  get_auth_router(app) # Add OAuth and auth routers
111
 
112
+ # Add logout endpoint
113
+ @app.get("/logout")
114
+ async def logout(request: Request):
115
+ request.session.clear()
116
+ response = RedirectResponse("/login")
117
+ response.delete_cookie("access_token")
118
+ return response
119
  # Debug routes endpoint
120
  @app.get("/debug/routes", response_class=PlainTextResponse)
121
  async def debug_routes():