Dama12 commited on
Commit
420d660
·
1 Parent(s): cebbd4f

Fix Google Auth and Registration flow

Browse files
app/api/routes/auth.py CHANGED
@@ -11,6 +11,7 @@ from app.core.security import create_access_token
11
  from app.db.repositories.user_repo import UserRepository
12
  from datetime import timedelta
13
  import uuid
 
14
 
15
  router = APIRouter()
16
 
@@ -35,17 +36,24 @@ async def login_google(request: Request):
35
  detail="Google OAuth is not configured on the server."
36
  )
37
 
38
- # Always use the explicit setting — never derive from request.url
39
- # (Hugging Face proxies strip https causes redirect_uri mismatch)
40
  redirect_uri = settings.GOOGLE_REDIRECT_URI
 
 
 
 
 
 
 
41
 
42
  logger.info(f"Initiating Google OAuth login")
43
  logger.info(f"Using Google Redirect URI: {redirect_uri}")
44
  logger.info(f"Request Host Header: {request.headers.get('host')}")
45
- logger.info(f"Request X-Forwarded-Proto: {request.headers.get('x-forwarded-proto')}")
46
-
47
- # Tell Authlib the real scheme so the state URL is correct too
48
- request.scope["scheme"] = "https"
49
 
50
  try:
51
  return await oauth.google.authorize_redirect(request, redirect_uri)
@@ -57,19 +65,22 @@ async def login_google(request: Request):
57
  )
58
 
59
 
60
- from app.core.logging import logger
61
-
62
  @router.get("/google/callback")
63
  async def auth_google(request: Request, db: Session = Depends(get_db)):
64
  """Gère le retour de Google après authentification"""
65
  logger.info("Google callback route reached")
66
 
67
- # Ensure scheme is https for callback as well (important for Authlib internal state validation)
68
- request.scope["scheme"] = "https"
69
-
 
 
 
 
 
 
70
  try:
71
  logger.info(f"Callback full URL: {request.url}")
72
- logger.info(f"Callback Params: {dict(request.query_params)}")
73
 
74
  token = await oauth.google.authorize_access_token(request)
75
  user_info = token.get('userinfo')
@@ -82,7 +93,7 @@ async def auth_google(request: Request, db: Session = Depends(get_db)):
82
  )
83
 
84
  email = user_info.get('email')
85
- name = user_info.get('name') or email.split('@')[0] if email else "User"
86
 
87
  logger.info(f"Google login successful for email: {email}")
88
 
@@ -93,19 +104,10 @@ async def auth_google(request: Request, db: Session = Depends(get_db)):
93
  detail="Email not provided by Google."
94
  )
95
 
96
- user_repo = UserRepository(db)
97
- user = user_repo.get_by_email(email)
 
98
 
99
- if not user:
100
- logger.info(f"Creating new user for email: {email}")
101
- user = user_repo.create_user(
102
- email=email,
103
- name=name,
104
- hashed_password=None, # OAuth users don't have a local password
105
- institution=None,
106
- role="user"
107
- )
108
-
109
  access_token = create_access_token(
110
  data={"sub": str(user.id)},
111
  expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
 
11
  from app.db.repositories.user_repo import UserRepository
12
  from datetime import timedelta
13
  import uuid
14
+ from app.core.logging import logger
15
 
16
  router = APIRouter()
17
 
 
36
  detail="Google OAuth is not configured on the server."
37
  )
38
 
39
+ # If settings.GOOGLE_REDIRECT_URI is set, use it.
40
+ # Otherwise, derive it from the current request URL (base) + callback path.
41
  redirect_uri = settings.GOOGLE_REDIRECT_URI
42
+ if not redirect_uri:
43
+ # Construct redirect URI from the request base URL
44
+ base_url = str(request.base_url).rstrip('/')
45
+ # Ensure we use https if we are behind a proxy that terminates SSL
46
+ if "hf.space" in base_url or request.headers.get("x-forwarded-proto") == "https":
47
+ base_url = base_url.replace("http://", "https://")
48
+ redirect_uri = f"{base_url}/api/v1/auth/google/callback"
49
 
50
  logger.info(f"Initiating Google OAuth login")
51
  logger.info(f"Using Google Redirect URI: {redirect_uri}")
52
  logger.info(f"Request Host Header: {request.headers.get('host')}")
53
+
54
+ # Ensure Authlib uses https for the state param if we are in production
55
+ if "https" in redirect_uri:
56
+ request.scope["scheme"] = "https"
57
 
58
  try:
59
  return await oauth.google.authorize_redirect(request, redirect_uri)
 
65
  )
66
 
67
 
 
 
68
  @router.get("/google/callback")
69
  async def auth_google(request: Request, db: Session = Depends(get_db)):
70
  """Gère le retour de Google après authentification"""
71
  logger.info("Google callback route reached")
72
 
73
+ # Determine the redirect_uri used during the initial request
74
+ redirect_uri = settings.GOOGLE_REDIRECT_URI
75
+ if not redirect_uri:
76
+ base_url = str(request.base_url).rstrip('/')
77
+ if "hf.space" in base_url or request.headers.get("x-forwarded-proto") == "https":
78
+ base_url = base_url.replace("http://", "https://")
79
+ request.scope["scheme"] = "https"
80
+ redirect_uri = f"{base_url}/api/v1/auth/google/callback"
81
+
82
  try:
83
  logger.info(f"Callback full URL: {request.url}")
 
84
 
85
  token = await oauth.google.authorize_access_token(request)
86
  user_info = token.get('userinfo')
 
93
  )
94
 
95
  email = user_info.get('email')
96
+ name = user_info.get('name') or (email.split('@')[0] if email else "User")
97
 
98
  logger.info(f"Google login successful for email: {email}")
99
 
 
104
  detail="Email not provided by Google."
105
  )
106
 
107
+ from app.services.user_service import UserService
108
+ service = UserService(db)
109
+ user = service.register_user_oauth(email=email, name=name)
110
 
 
 
 
 
 
 
 
 
 
 
111
  access_token = create_access_token(
112
  data={"sub": str(user.id)},
113
  expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
app/api/routes/users.py CHANGED
@@ -38,6 +38,7 @@ async def create_user(user: UserCreate, db: Session = Depends(get_db)):
38
  @router.post("/register", response_model=dict, status_code=status.HTTP_201_CREATED)
39
  async def register(user: UserCreate, db: Session = Depends(get_db)):
40
  """Enregistre un nouvel utilisateur avec mot de passe"""
 
41
  try:
42
  service = UserService(db)
43
  new_user = service.register_user(
@@ -47,6 +48,7 @@ async def register(user: UserCreate, db: Session = Depends(get_db)):
47
  institution=user.institution,
48
  role=user.role
49
  )
 
50
  return {
51
  "message": "User created successfully",
52
  "user": {
@@ -56,20 +58,29 @@ async def register(user: UserCreate, db: Session = Depends(get_db)):
56
  }
57
  }
58
  except ValueError as e:
 
59
  raise HTTPException(
60
  status_code=status.HTTP_400_BAD_REQUEST,
61
  detail=str(e)
62
  )
 
 
 
 
 
 
63
 
64
 
65
  @router.post("/login", response_model=dict)
66
  async def login(user_login: UserLogin, db: Session = Depends(get_db)):
67
  """Connecte un utilisateur par email/password et retourne un JWT"""
 
68
  try:
69
  service = UserService(db)
70
  user = service.login_user(user_login.email, user_login.password)
71
 
72
  if not user:
 
73
  raise HTTPException(
74
  status_code=status.HTTP_401_UNAUTHORIZED,
75
  detail="Invalid email or password."
@@ -79,12 +90,15 @@ async def login(user_login: UserLogin, db: Session = Depends(get_db)):
79
  data={"sub": str(user.id)},
80
  expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
81
  )
 
82
  return {"access_token": token, "token_type": "bearer", "user": {"id": str(user.id), "name": user.name, "email": user.email}}
83
  except HTTPException:
84
  raise
85
  except Exception as e:
 
86
  # Fallback to Demo Mode ONLY if explicitly enabled or for testing connection issues
87
  if ("OperationalError" in str(type(e)) or "connection" in str(e).lower()) and settings.ENVIRONMENT == "development":
 
88
  mock_id = str(uuid.uuid4())
89
  token = create_access_token(
90
  data={"sub": mock_id, "demo": True},
@@ -102,6 +116,7 @@ async def login(user_login: UserLogin, db: Session = Depends(get_db)):
102
  @router.post("/register-and-login", response_model=dict, status_code=status.HTTP_201_CREATED)
103
  async def register_and_login(user: UserCreate, db: Session = Depends(get_db)):
104
  """Enregistre un nouvel utilisateur et retourne un JWT directement"""
 
105
  try:
106
  service = UserService(db)
107
  new_user = service.register_user(
@@ -115,12 +130,14 @@ async def register_and_login(user: UserCreate, db: Session = Depends(get_db)):
115
  data={"sub": str(new_user.id)},
116
  expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
117
  )
 
118
  return {"access_token": token, "token_type": "bearer", "user": {"id": str(new_user.id), "name": new_user.name, "email": new_user.email}}
119
  except Exception as e:
120
- logger.error(f"Registration error: {str(e)}")
121
 
122
  # Capture conflict errors (user already exists)
123
  if isinstance(e, ValueError) or "unique constraint" in str(e).lower() or "already exists" in str(e).lower():
 
124
  raise HTTPException(
125
  status_code=status.HTTP_409_CONFLICT,
126
  detail="User with this email already exists."
@@ -144,7 +161,7 @@ async def register_and_login(user: UserCreate, db: Session = Depends(get_db)):
144
  "mode": "demo"
145
  }
146
 
147
- logger.exception("Unexpected error during registration")
148
  raise HTTPException(
149
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
150
  detail=f"Registration failed: {str(e)}"
 
38
  @router.post("/register", response_model=dict, status_code=status.HTTP_201_CREATED)
39
  async def register(user: UserCreate, db: Session = Depends(get_db)):
40
  """Enregistre un nouvel utilisateur avec mot de passe"""
41
+ logger.info(f"Registering new user: {user.email}")
42
  try:
43
  service = UserService(db)
44
  new_user = service.register_user(
 
48
  institution=user.institution,
49
  role=user.role
50
  )
51
+ logger.info(f"User registered successfully: {user.email}")
52
  return {
53
  "message": "User created successfully",
54
  "user": {
 
58
  }
59
  }
60
  except ValueError as e:
61
+ logger.warning(f"Registration conflict for {user.email}: {str(e)}")
62
  raise HTTPException(
63
  status_code=status.HTTP_400_BAD_REQUEST,
64
  detail=str(e)
65
  )
66
+ except Exception as e:
67
+ logger.error(f"Unexpected error during registration for {user.email}: {str(e)}", exc_info=True)
68
+ raise HTTPException(
69
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
70
+ detail=f"Registration failed: {str(e)}"
71
+ )
72
 
73
 
74
  @router.post("/login", response_model=dict)
75
  async def login(user_login: UserLogin, db: Session = Depends(get_db)):
76
  """Connecte un utilisateur par email/password et retourne un JWT"""
77
+ logger.info(f"Login attempt for: {user_login.email}")
78
  try:
79
  service = UserService(db)
80
  user = service.login_user(user_login.email, user_login.password)
81
 
82
  if not user:
83
+ logger.warning(f"Failed login attempt for: {user_login.email}")
84
  raise HTTPException(
85
  status_code=status.HTTP_401_UNAUTHORIZED,
86
  detail="Invalid email or password."
 
90
  data={"sub": str(user.id)},
91
  expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
92
  )
93
+ logger.info(f"Successful login for: {user_login.email}")
94
  return {"access_token": token, "token_type": "bearer", "user": {"id": str(user.id), "name": user.name, "email": user.email}}
95
  except HTTPException:
96
  raise
97
  except Exception as e:
98
+ logger.error(f"Error during login for {user_login.email}: {str(e)}", exc_info=True)
99
  # Fallback to Demo Mode ONLY if explicitly enabled or for testing connection issues
100
  if ("OperationalError" in str(type(e)) or "connection" in str(e).lower()) and settings.ENVIRONMENT == "development":
101
+ logger.warning("Database unavailable. Falling back to Demo Mode for login.")
102
  mock_id = str(uuid.uuid4())
103
  token = create_access_token(
104
  data={"sub": mock_id, "demo": True},
 
116
  @router.post("/register-and-login", response_model=dict, status_code=status.HTTP_201_CREATED)
117
  async def register_and_login(user: UserCreate, db: Session = Depends(get_db)):
118
  """Enregistre un nouvel utilisateur et retourne un JWT directement"""
119
+ logger.info(f"Register-and-login attempt for: {user.email}")
120
  try:
121
  service = UserService(db)
122
  new_user = service.register_user(
 
130
  data={"sub": str(new_user.id)},
131
  expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
132
  )
133
+ logger.info(f"Successful register-and-login for: {user.email}")
134
  return {"access_token": token, "token_type": "bearer", "user": {"id": str(new_user.id), "name": new_user.name, "email": new_user.email}}
135
  except Exception as e:
136
+ logger.error(f"Registration error for {user.email}: {str(e)}")
137
 
138
  # Capture conflict errors (user already exists)
139
  if isinstance(e, ValueError) or "unique constraint" in str(e).lower() or "already exists" in str(e).lower():
140
+ logger.warning(f"Registration conflict: user {user.email} already exists")
141
  raise HTTPException(
142
  status_code=status.HTTP_409_CONFLICT,
143
  detail="User with this email already exists."
 
161
  "mode": "demo"
162
  }
163
 
164
+ logger.exception(f"Unexpected error during registration for {user.email}")
165
  raise HTTPException(
166
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
167
  detail=f"Registration failed: {str(e)}"
app/core/settings.py CHANGED
@@ -76,7 +76,8 @@ class Settings(BaseSettings):
76
  # Google OAuth
77
  GOOGLE_CLIENT_ID: Optional[str] = None
78
  GOOGLE_CLIENT_SECRET: Optional[str] = None
79
- GOOGLE_REDIRECT_URI: str = "https://dama12-scientific-backend.hf.space/api/v1/auth/google/callback"
 
80
 
81
  model_config = SettingsConfigDict(
82
  env_file=".env",
 
76
  # Google OAuth
77
  GOOGLE_CLIENT_ID: Optional[str] = None
78
  GOOGLE_CLIENT_SECRET: Optional[str] = None
79
+ # If not set, the app will try to construct it from the request URL
80
+ GOOGLE_REDIRECT_URI: Optional[str] = None
81
 
82
  model_config = SettingsConfigDict(
83
  env_file=".env",
app/services/user_service.py CHANGED
@@ -24,6 +24,23 @@ class UserService:
24
  hashed_password = hash_password(password)
25
  return self.user_repo.create_user(email, name, hashed_password, institution, role)
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  def login_user(self, email: str, password: str):
28
  """Connecte un utilisateur"""
29
  user = self.user_repo.authenticate(email, password)
 
24
  hashed_password = hash_password(password)
25
  return self.user_repo.create_user(email, name, hashed_password, institution, role)
26
 
27
+ def register_user_oauth(self, email: str, name: str):
28
+ """
29
+ Enregistre ou récupère un utilisateur via OAuth.
30
+ Si l'utilisateur n'existe pas, il est créé sans mot de passe.
31
+ """
32
+ user = self.user_repo.get_by_email(email)
33
+ if not user:
34
+ # Create new user for OAuth
35
+ user = self.user_repo.create_user(
36
+ email=email,
37
+ name=name,
38
+ hashed_password=None, # No local password for OAuth-only accounts
39
+ institution="OAuth",
40
+ role="user"
41
+ )
42
+ return user
43
+
44
  def login_user(self, email: str, password: str):
45
  """Connecte un utilisateur"""
46
  user = self.user_repo.authenticate(email, password)