feat: Implement OAuth2 authentication with Google and GitHub
- Added OAuth2 endpoints for Google and GitHub authentication. - Created OAuth service to handle provider interactions and user info retrieval. - Implemented user OAuth repository for managing user OAuth links in the database. - Updated auth service to support linking existing users and creating new users via OAuth. - Added CORS middleware to allow frontend access. - Created tests for OAuth endpoints and service functionality. - Introduced environment configuration for OAuth client IDs and secrets. - Added logging for OAuth operations and error handling.
This commit is contained in:
@@ -10,6 +10,7 @@ from app.core.config import settings
|
||||
from app.core.logging import get_logger
|
||||
from app.models.user import User
|
||||
from app.repositories.user import UserRepository
|
||||
from app.repositories.user_oauth import UserOauthRepository
|
||||
from app.schemas.auth import (
|
||||
AuthResponse,
|
||||
TokenResponse,
|
||||
@@ -17,6 +18,7 @@ from app.schemas.auth import (
|
||||
UserRegisterRequest,
|
||||
UserResponse,
|
||||
)
|
||||
from app.services.oauth import OAuthUserInfo
|
||||
from app.utils.auth import JWTUtils, PasswordUtils
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -29,6 +31,7 @@ class AuthService:
|
||||
"""Initialize the auth service."""
|
||||
self.session = session
|
||||
self.user_repo = UserRepository(session)
|
||||
self.oauth_repo = UserOauthRepository(session)
|
||||
|
||||
async def register(self, request: UserRegisterRequest) -> AuthResponse:
|
||||
"""Register a new user."""
|
||||
@@ -203,7 +206,7 @@ class AuthService:
|
||||
|
||||
# Check if refresh token is expired
|
||||
if user.refresh_token_expires_at and datetime.now(
|
||||
UTC
|
||||
UTC,
|
||||
) > user.refresh_token_expires_at.replace(tzinfo=UTC):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
@@ -272,3 +275,127 @@ class AuthService:
|
||||
created_at=user.created_at,
|
||||
updated_at=user.updated_at,
|
||||
)
|
||||
|
||||
async def oauth_login(self, oauth_user_info: OAuthUserInfo) -> AuthResponse:
|
||||
"""Handle OAuth login - link or create user."""
|
||||
logger.info(
|
||||
"OAuth login attempt for %s with provider %s",
|
||||
oauth_user_info.email,
|
||||
oauth_user_info.provider,
|
||||
)
|
||||
|
||||
# Check if user already has OAuth link for this provider
|
||||
existing_oauth = await self.oauth_repo.get_by_provider_user_id(
|
||||
oauth_user_info.provider,
|
||||
oauth_user_info.provider_user_id,
|
||||
)
|
||||
|
||||
if existing_oauth:
|
||||
# User exists with this OAuth provider, get the user
|
||||
user = await self.user_repo.get_by_id(existing_oauth.user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Linked user not found",
|
||||
)
|
||||
|
||||
# Refresh user to avoid greenlet issues
|
||||
await self.session.refresh(user)
|
||||
|
||||
# Update OAuth record with latest info
|
||||
oauth_update_data = {
|
||||
"email": oauth_user_info.email,
|
||||
"name": oauth_user_info.name,
|
||||
"picture": oauth_user_info.picture,
|
||||
}
|
||||
await self.oauth_repo.update(existing_oauth, oauth_update_data)
|
||||
|
||||
logger.info(
|
||||
"OAuth login successful for existing user: %s",
|
||||
oauth_user_info.email,
|
||||
)
|
||||
else:
|
||||
# Check if user exists by email
|
||||
user = await self.user_repo.get_by_email(oauth_user_info.email)
|
||||
|
||||
if user:
|
||||
# Refresh user to avoid greenlet issues
|
||||
await self.session.refresh(user)
|
||||
|
||||
# Store user picture value to avoid greenlet issues later
|
||||
current_user_picture = user.picture
|
||||
|
||||
# Link existing user to OAuth provider
|
||||
oauth_data = {
|
||||
"user_id": user.id,
|
||||
"provider": oauth_user_info.provider,
|
||||
"provider_user_id": oauth_user_info.provider_user_id,
|
||||
"email": oauth_user_info.email,
|
||||
"name": oauth_user_info.name,
|
||||
"picture": oauth_user_info.picture,
|
||||
}
|
||||
await self.oauth_repo.create(oauth_data)
|
||||
|
||||
# Update user profile with OAuth info if needed
|
||||
user_update_data = {}
|
||||
if not current_user_picture and oauth_user_info.picture:
|
||||
user_update_data["picture"] = oauth_user_info.picture
|
||||
if user_update_data:
|
||||
await self.user_repo.update(user, user_update_data)
|
||||
# Refresh user after update to avoid greenlet issues
|
||||
await self.session.refresh(user)
|
||||
|
||||
logger.info(
|
||||
"Linked existing user %s to OAuth provider %s",
|
||||
oauth_user_info.email,
|
||||
oauth_user_info.provider,
|
||||
)
|
||||
else:
|
||||
# Create new user
|
||||
user_data = {
|
||||
"email": oauth_user_info.email,
|
||||
"name": oauth_user_info.name,
|
||||
"picture": oauth_user_info.picture,
|
||||
"is_active": True,
|
||||
# No password for OAuth users
|
||||
"password_hash": None,
|
||||
}
|
||||
|
||||
user = await self.user_repo.create(user_data)
|
||||
|
||||
# Create OAuth link
|
||||
oauth_data = {
|
||||
"user_id": user.id,
|
||||
"provider": oauth_user_info.provider,
|
||||
"provider_user_id": oauth_user_info.provider_user_id,
|
||||
"email": oauth_user_info.email,
|
||||
"name": oauth_user_info.name,
|
||||
"picture": oauth_user_info.picture,
|
||||
}
|
||||
await self.oauth_repo.create(oauth_data)
|
||||
|
||||
logger.info(
|
||||
"Created new user %s from OAuth provider %s",
|
||||
oauth_user_info.email,
|
||||
oauth_user_info.provider,
|
||||
)
|
||||
|
||||
# Refresh user to avoid greenlet issues and check if user is active
|
||||
await self.session.refresh(user)
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Account is deactivated",
|
||||
)
|
||||
|
||||
# Generate access token
|
||||
token = self._create_access_token(user)
|
||||
|
||||
# Create response
|
||||
user_response = await self.create_user_response(user)
|
||||
|
||||
logger.info(
|
||||
"OAuth login completed successfully for user: %s",
|
||||
oauth_user_info.email,
|
||||
)
|
||||
return AuthResponse(user=user_response, token=token)
|
||||
|
||||
Reference in New Issue
Block a user