gateway/modules/interfaces/googleInterface.py
2025-05-22 00:48:56 +02:00

287 lines
No EOL
10 KiB
Python

"""
Google interface for handling Google authentication and API operations.
"""
import logging
import requests
from typing import Dict, Any, Optional, Tuple
from datetime import datetime
import secrets
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import Flow
from google.auth.transport.requests import Request
import os
from modules.shared.configuration import APP_CONFIG
from modules.interfaces.googleModel import GoogleToken, GoogleUserInfo, GoogleConfig
from modules.connectors.connectorDbJson import DatabaseConnector
from modules.interfaces.googleAccess import GoogleAccess
from modules.interfaces.gatewayInterface import getRootUser
logger = logging.getLogger(__name__)
# Singleton factory for GoogleInterface instances per context
_googleInterfaces = {}
# Root interface instance
_rootGoogleInterface = None
class GoogleInterface:
"""Interface for Google authentication and API operations"""
def __init__(self, currentUser: Dict[str, Any] = None):
"""Initialize the Google interface"""
# Initialize variables
self.currentUser = currentUser
self.mandateId = currentUser.get("mandateId") if currentUser else None
self.userId = currentUser.get("id") if currentUser else None
self.access = None # Will be set when user context is provided
# Initialize configuration
self.clientId = APP_CONFIG.get("Service_GOOGLE_CLIENT_ID")
self.clientSecret = APP_CONFIG.get("Service_GOOGLE_CLIENT_SECRET")
self.redirectUri = APP_CONFIG.get("Service_GOOGLE_REDIRECT_URI")
self.authorityUrl = "https://accounts.google.com"
self.tokenUrl = "https://oauth2.googleapis.com/token"
self.userInfoUrl = "https://www.googleapis.com/oauth2/v3/userinfo"
self.scopes = ["openid", "profile", "email"]
# Initialize database
self._initializeDatabase()
# Initialize OAuth2 flow
self.flow = Flow.from_client_config(
{
"web": {
"client_id": self.clientId,
"client_secret": self.clientSecret,
"auth_uri": f"{self.authorityUrl}/o/oauth2/auth",
"token_uri": self.tokenUrl,
"redirect_uris": [self.redirectUri]
}
},
scopes=self.scopes
)
# Set user context if provided
if currentUser:
self.setUserContext(currentUser)
def _initializeDatabase(self):
"""Initializes the database connection."""
try:
# Get configuration values with defaults
dbHost = APP_CONFIG.get("DB_GOOGLE_HOST", "data")
dbDatabase = APP_CONFIG.get("DB_GOOGLE_DATABASE", "google")
dbUser = APP_CONFIG.get("DB_GOOGLE_USER")
dbPassword = APP_CONFIG.get("DB_GOOGLE_PASSWORD_SECRET")
# Ensure the database directory exists
os.makedirs(dbHost, exist_ok=True)
self.db = DatabaseConnector(
dbHost=dbHost,
dbDatabase=dbDatabase,
dbUser=dbUser,
dbPassword=dbPassword,
mandateId=self.mandateId,
userId=self.userId
)
# Set context
self.db.updateContext(self.mandateId, self.userId)
logger.info("Database initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize database: {str(e)}")
raise
def initiateLogin(self) -> str:
"""Initiate Google login flow"""
try:
# Generate auth URL
auth_url, _ = self.flow.authorization_url(
access_type="offline",
include_granted_scopes="true",
state=self._generateState()
)
return auth_url
except Exception as e:
logger.error(f"Error initiating Google login: {str(e)}")
return None
def handleAuthCallback(self, code: str) -> Optional[GoogleToken]:
"""Handle Google OAuth callback"""
try:
# Exchange code for token
self.flow.fetch_token(code=code)
credentials = self.flow.credentials
# Get user info
user_info = self.getUserInfoFromToken(credentials.token)
if not user_info:
return None
# Create token model
token = GoogleToken(
access_token=credentials.token,
refresh_token=credentials.refresh_token,
expires_in=credentials.expiry.timestamp() - datetime.now().timestamp(),
token_type=credentials.token_type,
expires_at=credentials.expiry.timestamp(),
user_info=user_info.model_dump(),
mandateId=self.mandateId,
userId=self.userId
)
return token
except Exception as e:
logger.error(f"Error handling auth callback: {str(e)}")
return None
def verifyToken(self, token: str) -> bool:
"""Verify Google token"""
try:
# Get user info from token
user_info = self.getUserInfoFromToken(token)
if not user_info:
return False
# Get current user's Google connection
user = self.db.getRecordset("users", recordFilter={"id": self.userId})[0]
google_connection = next((conn for conn in user.get("connections", [])
if conn.get("authority") == "google"), None)
if not google_connection:
return False
# Verify the token belongs to this user
return user_info.id == google_connection.get("externalId")
except Exception as e:
logger.error(f"Error verifying Google token: {str(e)}")
return False
def getUserInfoFromToken(self, token: str) -> Optional[GoogleUserInfo]:
"""Get user info from Google API"""
try:
# Call Google API
response = requests.get(
self.userInfoUrl,
headers={"Authorization": f"Bearer {token}"}
)
if response.status_code != 200:
logger.error(f"Failed to get user info: {response.text}")
return None
data = response.json()
# Create user info model
return GoogleUserInfo(
id=data["sub"], # Google uses 'sub' as the unique identifier
email=data["email"],
name=data.get("name", ""),
picture=data.get("picture") # Google provides profile picture URL
)
except Exception as e:
logger.error(f"Error getting user info: {str(e)}")
return None
def refreshToken(self, refresh_token: str) -> Optional[GoogleToken]:
"""Refresh Google token"""
try:
# Create credentials object
credentials = Credentials(
None, # No access token
refresh_token=refresh_token,
token_uri=self.tokenUrl,
client_id=self.clientId,
client_secret=self.clientSecret
)
# Refresh token
credentials.refresh(Request())
# Get user info
user_info = self.getUserInfoFromToken(credentials.token)
if not user_info:
return None
# Create token model
token = GoogleToken(
access_token=credentials.token,
refresh_token=credentials.refresh_token or refresh_token,
expires_in=credentials.expiry.timestamp() - datetime.now().timestamp(),
token_type=credentials.token_type,
expires_at=credentials.expiry.timestamp(),
user_info=user_info.model_dump(),
mandateId=self.mandateId,
userId=self.userId
)
return token
except Exception as e:
logger.error(f"Error refreshing token: {str(e)}")
return None
def _generateState(self) -> str:
"""Generate secure state token"""
return secrets.token_urlsafe(32)
def setUserContext(self, currentUser: Dict[str, Any]):
"""Set user context for the interface"""
if not currentUser:
logger.info("Initializing interface without user context")
return
self.currentUser = currentUser
self.mandateId = currentUser.get("mandateId")
self.userId = currentUser.get("id")
if not self.mandateId or not self.userId:
raise ValueError("Invalid user context: mandateId and id are required")
# Initialize access control with user context
self.access = GoogleAccess(self.currentUser, self.db)
# Update database context
self.db.updateContext(self.mandateId, self.userId)
logger.debug(f"User context set: userId={self.userId}")
def getRootInterface() -> GoogleInterface:
"""
Returns a GoogleInterface instance with root privileges.
This is used for initial setup and user creation.
"""
global _rootGoogleInterface
if _rootGoogleInterface is None:
# Get root user from gateway
rootUser = getRootUser()
_rootGoogleInterface = GoogleInterface(rootUser)
return _rootGoogleInterface
def getInterface(currentUser: Dict[str, Any] = None) -> GoogleInterface:
"""
Returns a GoogleInterface instance.
If currentUser is provided, initializes with user context.
Otherwise, returns an instance with only database access.
"""
# Create new instance if not exists
if "default" not in _googleInterfaces:
_googleInterfaces["default"] = GoogleInterface(currentUser or {})
interface = _googleInterfaces["default"]
if currentUser:
interface.setUserContext(currentUser)
else:
logger.info("Returning interface without user context")
return interface