|
|
| import base64 |
| import json |
| import logging |
| import os |
| from typing import Any, Dict, Optional |
| from urllib.parse import urlencode |
| from fastapi import HTTPException, Request |
| from pydantic import BaseModel |
|
|
| |
| try: |
| from backend.core.encryption import decrypt_data, encrypt_data |
| except ImportError: |
| |
| def encrypt_data(data: str) -> str: |
| return base64.b64encode(data.encode()).decode() |
| def decrypt_data(data: str) -> str: |
| return base64.b64decode(data.encode()).decode() |
|
|
| logger = logging.getLogger(__name__) |
|
|
| class OAuthState(BaseModel): |
| integration_type: str |
| service_id: str |
| user_id: str |
| redirect_path: Optional[str] = None |
| extra_data: Optional[Dict[str, Any]] = None |
|
|
| class UniversalAuthHandler: |
| def __init__(self): |
| self.app_domain = os.getenv("APP_DOMAIN", "localhost:3000") |
| self.url_scheme = os.getenv("URL_SCHEME", "http") |
| |
| if self.app_domain.endswith("/"): |
| self.app_domain = self.app_domain[:-1] |
| |
| self.callback_url = f"{self.url_scheme}://{self.app_domain}/api/v1/integrations/universal/callback" |
|
|
| def generate_oauth_url(self, |
| auth_url: str, |
| client_id: str, |
| scopes: list, |
| state_payload: OAuthState, |
| extra_params: Dict[str, str] = None) -> str: |
| """ |
| Generates a standard OAuth 2.0 URL pointing to the provider, |
| but with our Universal Callback URL as the redirect_uri. |
| """ |
| |
| state_json = state_payload.json() |
| encrypted_state = encrypt_data(state_json) |
| |
| |
| params = { |
| "response_type": "code", |
| "client_id": client_id, |
| "redirect_uri": self.callback_url, |
| "scope": " ".join(scopes), |
| "state": encrypted_state |
| } |
| |
| if extra_params: |
| params.update(extra_params) |
| |
| |
| query_string = urlencode(params) |
| return f"{auth_url}?{query_string}" |
|
|
| async def handle_callback(self, code: str, state: str) -> Dict[str, Any]: |
| """ |
| Processes the callback from the provider. |
| Decodes state to determine which integration strategy to use for token exchange. |
| """ |
| try: |
| |
| decrypted_state_json = decrypt_data(state) |
| state_data = json.loads(decrypted_state_json) |
| oauth_state = OAuthState(**state_data) |
| |
| logger.info(f"Processing callback for service: {oauth_state.service_id} ({oauth_state.integration_type})") |
| |
| |
| |
| return { |
| "code": code, |
| "state": oauth_state, |
| "callback_url": self.callback_url |
| } |
| |
| except Exception as e: |
| logger.error(f"Callback processing failed: {str(e)}") |
| raise HTTPException(status_code=400, detail="Invalid OAuth state or callback data") |
|
|
| |
| universal_auth = UniversalAuthHandler() |
|
|