check if a google calendar exixst before adding it , in the add page

This commit is contained in:
CREDO23 2025-08-02 05:36:43 +02:00
parent edf46e4de1
commit ad0a1e5c97
7 changed files with 62 additions and 311 deletions

View file

@ -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, "🔎")

View file

@ -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

View file

@ -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,

View file

@ -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<typeof googleCalendarConnectorFormSchema>;
// 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<Calendar[]>([]);
const [credentials, setCredentials] = useState<OAuthCredentials | null>(null);
const [doesConnectorExist, setDoesConnectorExist] = useState(false);
// Initialize the form
const form = useForm<GoogleCalendarConnectorFormValues>({
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() {
<div>
<h1 className="text-3xl font-bold tracking-tight">Connect Google Calendar</h1>
<p className="text-muted-foreground">
Connect your Google Calendar to search events, meetings and schedules.
Connect your Google Calendar to search events.
</p>
</div>
</div>
</div>
{/* OAuth Connection Card */}
{!isConnected ? (
{!doesConnectorExist ? (
<Card>
<CardHeader>
<CardTitle>Connect Your Google Account</CardTitle>
@ -285,167 +155,35 @@ export default function GoogleCalendarConnectorPage() {
/* Configuration Form Card */
<Card>
<CardHeader>
<CardTitle>Configure Google Calendar Connector</CardTitle>
<CardDescription>
Your Google account is connected! Now select which calendars to include and give
your connector a name.
</CardDescription>
<CardTitle> Your Google calendar is successfully connected!</CardTitle>
</CardHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<CardContent className="space-y-6">
{/* Connector Name */}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Connector Name</FormLabel>
<FormControl>
<Input placeholder="My Google Calendar" {...field} />
</FormControl>
<FormDescription>
A friendly name to identify this connector.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* Calendar Selection */}
<FormField
control={form.control}
name="calendar_ids"
render={() => (
<FormItem>
<div className="mb-4">
<FormLabel className="text-base">Select Calendars</FormLabel>
<FormDescription>
Choose which calendars you want to include in your search results.
</FormDescription>
</div>
{calendars.map((calendar) => (
<FormField
key={calendar.id}
control={form.control}
name="calendar_ids"
render={({ field }) => {
return (
<FormItem
key={calendar.id}
className="flex flex-row items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
checked={field.value?.includes(calendar.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, calendar.id])
: field.onChange(
field.value?.filter((value) => value !== calendar.id)
);
}}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
{calendar.summary}
{calendar.primary && (
<span className="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
Primary
</span>
)}
</FormLabel>
{calendar.description && (
<FormDescription className="text-xs">
{calendar.description}
</FormDescription>
)}
</div>
</FormItem>
);
}}
/>
))}
<FormMessage />
</FormItem>
)}
/>
</CardContent>
<CardFooter className="flex justify-between">
<Button
type="button"
variant="outline"
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting || form.watch("calendar_ids").length === 0}
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
<>
<Check className="mr-2 h-4 w-4" />
Create Connector
</>
)}
</Button>
</CardFooter>
</form>
</Form>
</Card>
)}
{/* Help Section */}
<Card className="mt-6">
<CardHeader>
<CardTitle className="text-lg">How It Works</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h4 className="font-medium mb-2">1. Connect Your Account</h4>
<p className="text-sm text-muted-foreground">
Click "Connect Your Google Account" to start the secure OAuth process. You'll be
redirected to Google to sign in.
</p>
</div>
<div>
<h4 className="font-medium mb-2">2. Grant Permissions</h4>
<p className="text-sm text-muted-foreground">
Google will ask for permission to read your calendar events. We only request
read-only access to keep your data safe.
</p>
</div>
<div>
<h4 className="font-medium mb-2">3. Select Calendars</h4>
<p className="text-sm text-muted-foreground">
Choose which calendars you want to include in your search results. You can select
multiple calendars.
</p>
</div>
<div>
<h4 className="font-medium mb-2">4. Start Searching</h4>
<p className="text-sm text-muted-foreground">
Once connected, your calendar events will be indexed and searchable alongside your
other content.
</p>
</div>
{isConnected && (
<div className="mt-4 p-3 bg-green-50 border border-green-200 rounded-md">
<p className="text-sm text-green-800">
Your Google account is successfully connected! You can now configure your
connector above.
{!doesConnectorExist && (
<Card className="mt-6">
<CardHeader>
<CardTitle className="text-lg">How It Works</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h4 className="font-medium mb-2">1. Connect Your Account</h4>
<p className="text-sm text-muted-foreground">
Click "Connect Your Google Account" to start the secure OAuth process. You'll be
redirected to Google to sign in.
</p>
</div>
)}
</CardContent>
</Card>
<div>
<h4 className="font-medium mb-2">2. Grant Permissions</h4>
<p className="text-sm text-muted-foreground">
Google will ask for permission to read your calendar events. We only request
read-only access to keep your data safe.
</p>
</div>
</CardContent>
</Card>
)}
</motion.div>
</div>
);

View file

@ -8,8 +8,8 @@ import {
IconBrandSlack,
IconBrandWindows,
IconBrandZoom,
IconChecklist,
IconCalendar,
IconChecklist,
IconChevronDown,
IconChevronRight,
IconLayoutKanban,

View file

@ -7,8 +7,8 @@ import {
IconBrandNotion,
IconBrandSlack,
IconBrandYoutube,
IconChecklist,
IconCalendar,
IconChecklist,
IconLayoutKanban,
IconTicket,
} from "@tabler/icons-react";

View file

@ -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);