diff --git a/surfsense_backend/app/agents/researcher/utils.py b/surfsense_backend/app/agents/researcher/utils.py index b2d3dcd..21969e6 100644 --- a/surfsense_backend/app/agents/researcher/utils.py +++ b/surfsense_backend/app/agents/researcher/utils.py @@ -47,6 +47,7 @@ def get_connector_emoji(connector_name: str) -> str: "DISCORD_CONNECTOR": "🗨️", "TAVILY_API": "🔍", "LINKUP_API": "🔗", + "GOOGLE_CALENDAR_CONNECTOR": "📅", } return connector_emojis.get(connector_name, "🔎") diff --git a/surfsense_backend/app/config/__init__.py b/surfsense_backend/app/config/__init__.py index b71b113..38a1f3e 100644 --- a/surfsense_backend/app/config/__init__.py +++ b/surfsense_backend/app/config/__init__.py @@ -41,11 +41,14 @@ class Config: NEXT_FRONTEND_URL = os.getenv("NEXT_FRONTEND_URL") - # AUTH: Google OAuth + # Auth AUTH_TYPE = os.getenv("AUTH_TYPE") + # Google OAuth GOOGLE_OAUTH_CLIENT_ID = os.getenv("GOOGLE_OAUTH_CLIENT_ID") GOOGLE_OAUTH_CLIENT_SECRET = os.getenv("GOOGLE_OAUTH_CLIENT_SECRET") + + # Google Calendar redirect URI GOOGLE_CALENDAR_REDIRECT_URI = os.getenv("GOOGLE_CALENDAR_REDIRECT_URI") # LLM instances are now managed per-user through the LLMConfig system diff --git a/surfsense_backend/app/routes/google_calendar_add_connector_route.py b/surfsense_backend/app/routes/google_calendar_add_connector_route.py index d78d6dd..fea69d1 100644 --- a/surfsense_backend/app/routes/google_calendar_add_connector_route.py +++ b/surfsense_backend/app/routes/google_calendar_add_connector_route.py @@ -1,22 +1,28 @@ # app/routes/google_calendar.py - import base64 import json -from sqlite3 import IntegrityError +import logging from uuid import UUID -from venv import logger from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import RedirectResponse from google_auth_oauthlib.flow import Flow -from jsonschema import ValidationError +from pydantic import ValidationError +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from app.config import config -from app.db import SearchSourceConnector, User, get_async_session +from app.db import ( + SearchSourceConnector, + SearchSourceConnectorType, + User, + get_async_session, +) from app.users import current_active_user +logger = logging.getLogger(__name__) + router = APIRouter() SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"] @@ -100,7 +106,8 @@ async def calendar_callback( result = await session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type == "GOOGLE_CALENDAR_CONNECTOR", + SearchSourceConnector.connector_type + == SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR, ) ) existing_connector = result.scalars().first() @@ -111,7 +118,7 @@ async def calendar_callback( ) db_connector = SearchSourceConnector( name="Google Calendar Connector", - connector_type="GOOGLE_CALENDAR_CONNECTOR", + connector_type=SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR, config=creds_dict, user_id=user_id, is_indexable=True, diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx index 64c32ef..fb04a27 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx @@ -19,128 +19,36 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { Checkbox } from "@/components/ui/checkbox"; import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors"; - -// Define the form schema with Zod -const googleCalendarConnectorFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - calendar_ids: z.array(z.string()).min(1, { - message: "At least one calendar must be selected.", - }), -}); - -// Define the type for the form values -type GoogleCalendarConnectorFormValues = z.infer; - -// Interface for calendar data -interface Calendar { - id: string; - summary: string; - description?: string; - primary?: boolean; - access_role: string; - time_zone?: string; -} - -// Interface for OAuth credentials -interface OAuthCredentials { - client_id: string; - client_secret: string; - refresh_token: string; - access_token: string; -} + type SearchSourceConnector, + useSearchSourceConnectors, +} from "@/hooks/useSearchSourceConnectors"; export default function GoogleCalendarConnectorPage() { const router = useRouter(); const params = useParams(); - const searchParams = useSearchParams(); const searchSpaceId = params.search_space_id as string; - const isSuccess = searchParams.get("success") === "true"; - - const { createConnector } = useSearchSourceConnectors(); - const [isSubmitting, setIsSubmitting] = useState(false); const [isConnecting, setIsConnecting] = useState(false); - const [isConnected, setIsConnected] = useState(false); - const [calendars, setCalendars] = useState([]); - const [credentials, setCredentials] = useState(null); + const [doesConnectorExist, setDoesConnectorExist] = useState(false); - // Initialize the form - const form = useForm({ - resolver: zodResolver(googleCalendarConnectorFormSchema), - defaultValues: { - name: "", - calendar_ids: [], - }, - }); + const { fetchConnectors } = useSearchSourceConnectors(); useEffect(() => { - if (isSuccess) { - toast.success("Google Calendar connector created successfully!"); - } - }, [isSuccess]); - - // Check for OAuth callback parameters - useEffect(() => { - const success = searchParams.get("success"); - const error = searchParams.get("error"); - const message = searchParams.get("message"); - const sessionKey = searchParams.get("session_key"); - - if (success === "true" && sessionKey) { - // Fetch OAuth data from backend - fetchOAuthData(sessionKey); - } else if (error) { - toast.error(message || "Failed to connect to Google Calendar"); - } - }, [searchParams]); - - // Fetch OAuth data from backend - const fetchOAuthData = async (sessionKey: string) => { - try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/auth/google/session?session_key=${sessionKey}`, - { - method: "GET", - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - } + fetchConnectors().then((data) => { + const connector = data.find( + (c: SearchSourceConnector) => c.connector_type === "GOOGLE_CALENDAR_CONNECTOR" ); - - if (!response.ok) { - throw new Error("Failed to fetch OAuth data"); + if (connector) { + setDoesConnectorExist(true); } - - const data = await response.json(); - - setCredentials(data.credentials); - setCalendars(data.calendars); - setIsConnected(true); - toast.success("Successfully connected to Google Calendar!"); - } catch (error) { - console.error("Error fetching OAuth data:", error); - toast.error("Failed to retrieve Google Calendar data"); - } - }; + }); + }, []); // Handle Google OAuth connection const handleConnectGoogle = async () => { - setIsConnecting(true); try { - // Call backend to initiate OAuth flow + setIsConnecting(true); + // Call backend to initiate authorization flow const response = await fetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/auth/google/calendar/connector/add/?space_id=${searchSpaceId}`, { @@ -162,46 +70,8 @@ export default function GoogleCalendarConnectorPage() { } catch (error) { console.error("Error connecting to Google:", error); toast.error("Failed to connect to Google Calendar"); - setIsConnecting(false); - } - }; - - // Handle form submission - const onSubmit = async (values: GoogleCalendarConnectorFormValues) => { - if (!isConnected || !credentials) { - toast.error("Please connect your Google account first"); - return; - } - - if (values.calendar_ids.length === 0) { - toast.error("Please select at least one calendar"); - return; - } - - setIsSubmitting(true); - try { - await createConnector({ - name: values.name, - connector_type: "GOOGLE_CALENDAR_CONNECTOR", - config: { - GOOGLE_CALENDAR_CLIENT_ID: credentials.client_id, - GOOGLE_CALENDAR_CLIENT_SECRET: credentials.client_secret, - GOOGLE_CALENDAR_REFRESH_TOKEN: credentials.refresh_token, - GOOGLE_CALENDAR_CALENDAR_IDS: values.calendar_ids, - }, - is_indexable: true, - last_indexed_at: null, - }); - - toast.success("Google Calendar connector created successfully!"); - - // Navigate back to connectors page - router.push(`/dashboard/${searchSpaceId}/connectors`); - } catch (error) { - console.error("Error creating connector:", error); - toast.error(error instanceof Error ? error.message : "Failed to create connector"); } finally { - setIsSubmitting(false); + setIsConnecting(false); } }; @@ -228,14 +98,14 @@ export default function GoogleCalendarConnectorPage() {

Connect Google Calendar

- Connect your Google Calendar to search events, meetings and schedules. + Connect your Google Calendar to search events.

{/* OAuth Connection Card */} - {!isConnected ? ( + {!doesConnectorExist ? ( Connect Your Google Account @@ -285,167 +155,35 @@ export default function GoogleCalendarConnectorPage() { /* Configuration Form Card */ - Configure Google Calendar Connector - - Your Google account is connected! Now select which calendars to include and give - your connector a name. - + ✅ Your Google calendar is successfully connected! -
- - - {/* Connector Name */} - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> - - {/* Calendar Selection */} - ( - -
- Select Calendars - - Choose which calendars you want to include in your search results. - -
- {calendars.map((calendar) => ( - { - return ( - - - { - return checked - ? field.onChange([...field.value, calendar.id]) - : field.onChange( - field.value?.filter((value) => value !== calendar.id) - ); - }} - /> - -
- - {calendar.summary} - {calendar.primary && ( - - Primary - - )} - - {calendar.description && ( - - {calendar.description} - - )} -
-
- ); - }} - /> - ))} - -
- )} - /> -
- - - - -
-
)} {/* Help Section */} - - - How It Works - - -
-

1. Connect Your Account

-

- Click "Connect Your Google Account" to start the secure OAuth process. You'll be - redirected to Google to sign in. -

-
-
-

2. Grant Permissions

-

- Google will ask for permission to read your calendar events. We only request - read-only access to keep your data safe. -

-
-
-

3. Select Calendars

-

- Choose which calendars you want to include in your search results. You can select - multiple calendars. -

-
-
-

4. Start Searching

-

- Once connected, your calendar events will be indexed and searchable alongside your - other content. -

-
- {isConnected && ( -
-

- ✅ Your Google account is successfully connected! You can now configure your - connector above. + {!doesConnectorExist && ( + + + How It Works + + +

+

1. Connect Your Account

+

+ Click "Connect Your Google Account" to start the secure OAuth process. You'll be + redirected to Google to sign in.

- )} - - +
+

2. Grant Permissions

+

+ Google will ask for permission to read your calendar events. We only request + read-only access to keep your data safe. +

+
+ + + )}
); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx index 2a7855d..c6ec629 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx @@ -8,8 +8,8 @@ import { IconBrandSlack, IconBrandWindows, IconBrandZoom, - IconChecklist, IconCalendar, + IconChecklist, IconChevronDown, IconChevronRight, IconLayoutKanban, diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx index 5ba8cbd..dd226c7 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx @@ -7,8 +7,8 @@ import { IconBrandNotion, IconBrandSlack, IconBrandYoutube, - IconChecklist, IconCalendar, + IconChecklist, IconLayoutKanban, IconTicket, } from "@tabler/icons-react"; diff --git a/surfsense_web/hooks/useSearchSourceConnectors.ts b/surfsense_web/hooks/useSearchSourceConnectors.ts index 5785abd..0c59a3b 100644 --- a/surfsense_web/hooks/useSearchSourceConnectors.ts +++ b/surfsense_web/hooks/useSearchSourceConnectors.ts @@ -86,6 +86,8 @@ export const useSearchSourceConnectors = (lazy: boolean = false) => { // Update connector source items when connectors change updateConnectorSourceItems(data); + + return data; } catch (err) { setError(err instanceof Error ? err : new Error("An unknown error occurred")); console.error("Error fetching search source connectors:", err);