From 9abaf4fd2a8c2370f9a820d369c15d6e5b8842ce Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Sat, 2 Aug 2025 00:05:55 +0200 Subject: [PATCH] add (backend) google calendar connection flow --- surfsense_backend/app/db.py | 29 +++++ .../routes/google_calendar_connector_route.py | 121 ++++++++++++++++++ .../app/schemas/google_calendar_accounts.py | 26 ++++ 3 files changed, 176 insertions(+) create mode 100644 surfsense_backend/app/routes/google_calendar_connector_route.py create mode 100644 surfsense_backend/app/schemas/google_calendar_accounts.py diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index d749b3b..33a4057 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -46,6 +46,7 @@ class DocumentType(str, Enum): JIRA_CONNECTOR = "JIRA_CONNECTOR" CONFLUENCE_CONNECTOR = "CONFLUENCE_CONNECTOR" CLICKUP_CONNECTOR = "CLICKUP_CONNECTOR" + GOOGLE_CALENDAR_CONNECTOR = "GOOGLE_CALENDAR_CONNECTOR" class SearchSourceConnectorType(str, Enum): @@ -60,6 +61,7 @@ class SearchSourceConnectorType(str, Enum): JIRA_CONNECTOR = "JIRA_CONNECTOR" CONFLUENCE_CONNECTOR = "CONFLUENCE_CONNECTOR" CLICKUP_CONNECTOR = "CLICKUP_CONNECTOR" + GOOGLE_CALENDAR_CONNECTOR = "GOOGLE_CALENDAR_CONNECTOR" class ChatType(str, Enum): @@ -244,6 +246,21 @@ class SearchSourceConnector(BaseModel, TimestampMixin): user = relationship("User", back_populates="search_source_connectors") +class GoogleCalendarAccount(BaseModel): + __tablename__ = "google_calendar_accounts" + + user_id = Column( + UUID(as_uuid=True), + ForeignKey("user.id", ondelete="CASCADE"), + nullable=False, + unique=True, + ) + + access_token = Column(String, nullable=False) + refresh_token = Column(String, nullable=False) + user = relationship("User", back_populates="calendar_account") + + class LLMConfig(BaseModel, TimestampMixin): __tablename__ = "llm_configs" @@ -297,6 +314,12 @@ if config.AUTH_TYPE == "GOOGLE": search_source_connectors = relationship( "SearchSourceConnector", back_populates="user" ) + calendar_account = relationship( + "GoogleCalendarAccount", + back_populates="user", + uselist=False, + cascade="all, delete-orphan", + ) llm_configs = relationship( "LLMConfig", back_populates="user", @@ -331,6 +354,12 @@ else: search_source_connectors = relationship( "SearchSourceConnector", back_populates="user" ) + calendar_account = relationship( + "GoogleCalendarAccount", + back_populates="user", + uselist=False, + cascade="all, delete-orphan", + ) llm_configs = relationship( "LLMConfig", back_populates="user", diff --git a/surfsense_backend/app/routes/google_calendar_connector_route.py b/surfsense_backend/app/routes/google_calendar_connector_route.py new file mode 100644 index 0000000..d4eee5d --- /dev/null +++ b/surfsense_backend/app/routes/google_calendar_connector_route.py @@ -0,0 +1,121 @@ +# app/routes/google_calendar.py + +import base64 +import json +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi.responses import RedirectResponse +from google_auth_oauthlib.flow import Flow +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from app.config import config +from app.db import GoogleCalendarAccount, User, get_async_session +from app.users import current_active_user + +router = APIRouter() + +SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"] +REDIRECT_URI = config.GOOGLE_CALENDAR_REDIRECT_URI + + +def get_google_flow(): + try: + return Flow.from_client_config( + { + "web": { + "client_id": config.GOOGLE_OAUTH_CLIENT_ID, + "client_secret": config.GOOGLE_OAUTH_CLIENT_SECRET, + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "redirect_uris": [REDIRECT_URI], + } + }, + scopes=SCOPES, + redirect_uri=REDIRECT_URI, + ) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to create Google flow: {e!s}" + ) from e + + +@router.get("/auth/google/calendar/connector/init/") +async def connect_calendar(space_id: int, user: User = Depends(current_active_user)): + try: + if not space_id: + raise HTTPException(status_code=400, detail="space_id is required") + + flow = get_google_flow() + + # Encode space_id and user_id in state + state_payload = json.dumps( + { + "space_id": space_id, + "user_id": str(user.id), + } + ) + state_encoded = base64.urlsafe_b64encode(state_payload.encode()).decode() + + auth_url, _ = flow.authorization_url( + access_type="offline", + prompt="consent", + include_granted_scopes="true", + state=state_encoded, + ) + return {"auth_url": auth_url} + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to initiate Google OAuth: {e!s}" + ) from e + + +@router.get("/auth/google/calendar/connector/callback/") +async def calendar_callback( + request: Request, + code: str, + state: str, + session: AsyncSession = Depends(get_async_session), +): + try: + # Decode and parse the state + decoded_state = base64.urlsafe_b64decode(state.encode()).decode() + data = json.loads(decoded_state) + + user_id = UUID(data["user_id"]) + space_id = data["space_id"] + + flow = get_google_flow() + flow.fetch_token(code=code) + + creds = flow.credentials + token = creds.token + refresh_token = creds.refresh_token + + existing = await session.scalar( + select(GoogleCalendarAccount).where( + GoogleCalendarAccount.user_id == user_id + ) + ) + if existing: + existing.access_token = token + existing.refresh_token = refresh_token or existing.refresh_token + else: + session.add( + GoogleCalendarAccount( + user_id=user_id, + access_token=token, + refresh_token=refresh_token, + ) + ) + + await session.commit() + + return RedirectResponse( + f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/add/google-calendar-connector?success=true" + ) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to complete Google OAuth: {e!s}" + ) from e diff --git a/surfsense_backend/app/schemas/google_calendar_accounts.py b/surfsense_backend/app/schemas/google_calendar_accounts.py new file mode 100644 index 0000000..32e8355 --- /dev/null +++ b/surfsense_backend/app/schemas/google_calendar_accounts.py @@ -0,0 +1,26 @@ +from uuid import UUID + +from pydantic import BaseModel, ConfigDict + + +class GoogleCalendarAccountBase(BaseModel): + user_id: UUID + access_token: str + refresh_token: str + + +class GoogleCalendarAccountCreate(GoogleCalendarAccountBase): + pass + + +class GoogleCalendarAccountUpdate(BaseModel): + access_token: str + refresh_token: str + + +class GoogleCalendarAccountRead(BaseModel): + user_id: UUID + access_token: str + refresh_token: str + + model_config = ConfigDict(from_attributes=True)