feat: implement re-authentication flows for Google Drive, Gmail, and Calendar connectors

- Added re-authentication endpoints for Google Drive, Gmail, and Calendar connectors to handle expired authentication.
- Enhanced the UI to prompt users for re-authentication when their credentials are expired.
- Updated backend logic to mark connectors as 'auth_expired' and manage re-authentication requests effectively.
- Improved error handling for authentication failures across Google connectors.
This commit is contained in:
Anish Sarkar 2026-03-19 17:51:59 +05:30
parent 36f4709225
commit c9deae940c
9 changed files with 432 additions and 98 deletions

View file

@ -3,6 +3,7 @@
import { useAtomValue, useSetAtom } from "jotai";
import { AlertTriangle, Cable, Settings } from "lucide-react";
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms";
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
import {
@ -215,11 +216,8 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
return (
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open && pickerOpen) return;
handleOpenChange(open);
}}
modal={!pickerOpen}
modal={false}
onOpenChange={handleOpenChange}
>
{showTrigger && (
<TooltipIconButton
@ -256,8 +254,26 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
</TooltipIconButton>
)}
{isOpen &&
createPortal(
<div
className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
aria-hidden="true"
onClick={() => {
if (!pickerOpen) handleOpenChange(false);
}}
/>,
document.body
)}
<DialogContent
onFocusOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => {
if (pickerOpen) e.preventDefault();
}}
onPointerDownOutside={(e) => {
if (pickerOpen) e.preventDefault();
}}
className="max-w-3xl w-[95vw] sm:w-full h-[75vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border ring-0 dark:ring-0 bg-muted dark:bg-muted text-foreground focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 [&>button]:right-4 sm:[&>button]:right-12 [&>button]:top-6 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button_svg]:size-5 select-none"
>
<DialogTitle className="sr-only">Manage Connectors</DialogTitle>
@ -336,20 +352,26 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
}}
onDisconnect={() => handleDisconnectConnector(() => refreshConnectors())}
onBack={handleBackFromEdit}
onQuickIndex={
editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR"
? () => {
startIndexing(editingConnector.id);
handleQuickIndexConnector(
editingConnector.id,
editingConnector.connector_type,
stopIndexing,
startDate,
endDate
);
}
: undefined
}
onQuickIndex={(() => {
const cfg = connectorConfig || editingConnector.config;
const isDrive = editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR";
const hasDriveItems = isDrive
? ((cfg?.selected_folders as unknown[]) ?? []).length > 0 ||
((cfg?.selected_files as unknown[]) ?? []).length > 0
: true;
if (!hasDriveItems) return undefined;
return () => {
startIndexing(editingConnector.id);
handleQuickIndexConnector(
editingConnector.id,
editingConnector.connector_type,
stopIndexing,
startDate,
endDate
);
};
})()}
onConfigChange={setConnectorConfig}
onNameChange={setConnectorName}
/>

View file

@ -1,5 +1,6 @@
"use client";
import { useAtomValue } from "jotai";
import {
ChevronDown,
ChevronRight,
@ -10,10 +11,13 @@ import {
Image,
Loader2,
Presentation,
RefreshCw,
X,
} from "lucide-react";
import type { FC } from "react";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import {
@ -23,7 +27,9 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch";
import { authenticatedFetch } from "@/lib/auth-utils";
import { type PickerResult, useGooglePicker } from "@/hooks/use-google-picker";
import type { ConnectorConfigProps } from "../index";
@ -84,7 +90,10 @@ function getFileIconFromName(fileName: string, className: string = "size-3.5 shr
return <File className={`${className} text-gray-500`} />;
}
const DRIVE_REAUTH_ENDPOINT = "/api/v1/auth/google/drive/connector/reauth";
export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfigChange }) => {
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
const existingFolders = (connector.config?.selected_folders as SelectedItem[] | undefined) || [];
const existingFiles = (connector.config?.selected_files as SelectedItem[] | undefined) || [];
const existingIndexingOptions =
@ -93,6 +102,33 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfi
const [selectedFolders, setSelectedFolders] = useState<SelectedItem[]>(existingFolders);
const [selectedFiles, setSelectedFiles] = useState<SelectedItem[]>(existingFiles);
const [indexingOptions, setIndexingOptions] = useState<IndexingOptions>(existingIndexingOptions);
const [reauthing, setReauthing] = useState(false);
const handleReauth = useCallback(async () => {
if (!searchSpaceId) return;
setReauthing(true);
try {
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
const url = new URL(`${backendUrl}${DRIVE_REAUTH_ENDPOINT}`);
url.searchParams.set("connector_id", String(connector.id));
url.searchParams.set("space_id", String(searchSpaceId));
url.searchParams.set("return_url", window.location.pathname);
const response = await authenticatedFetch(url.toString());
if (!response.ok) {
const data = await response.json().catch(() => ({}));
toast.error(data.detail ?? "Failed to initiate re-authentication.");
return;
}
const data = await response.json();
if (data.auth_url) {
window.location.href = data.auth_url;
}
} catch {
toast.error("Failed to initiate re-authentication.");
} finally {
setReauthing(false);
}
}, [searchSpaceId, connector.id]);
useEffect(() => {
const folders = (connector.config?.selected_folders as SelectedItem[] | undefined) || [];
@ -141,6 +177,10 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfi
onPicked: handlePicked,
});
const isAuthExpired =
connector.config?.auth_expired === true ||
(!!pickerError && pickerError.toLowerCase().includes("authentication expired"));
const handleIndexingOptionChange = (key: keyof IndexingOptions, value: number | boolean) => {
const newOptions = { ...indexingOptions, [key]: value };
setIndexingOptions(newOptions);
@ -229,18 +269,44 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfi
</div>
)}
<Button
type="button"
variant="outline"
onClick={openPicker}
disabled={pickerLoading}
className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 hover:bg-slate-400/10 dark:hover:bg-white/10 text-xs sm:text-sm h-8 sm:h-9"
>
{pickerLoading && <Loader2 className="size-3.5 mr-1.5 animate-spin" />}
{totalSelected > 0 ? "Change Selection" : "Select from Google Drive"}
</Button>
<Button
type="button"
variant="outline"
onClick={openPicker}
disabled={pickerLoading || isAuthExpired}
className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 hover:bg-slate-400/10 dark:hover:bg-white/10 text-xs sm:text-sm h-8 sm:h-9"
>
{pickerLoading && <Loader2 className="size-3.5 mr-1.5 animate-spin" />}
{totalSelected > 0 ? "Change Selection" : "Select from Google Drive"}
</Button>
{pickerError && <p className="text-xs text-destructive">{pickerError}</p>}
{(pickerError || isAuthExpired) && (
<div className="flex flex-col gap-2">
{pickerError && !isAuthExpired && (
<p className="text-xs text-destructive">{pickerError}</p>
)}
{isAuthExpired && (
<div className="flex items-center gap-2">
<p className="text-xs text-amber-600 dark:text-amber-500">
Your Google Drive authentication has expired.
</p>
<Button
size="sm"
className="h-7 text-[11px] px-3 rounded-lg font-medium bg-amber-600 hover:bg-amber-700 text-white border-0 shadow-xs shrink-0"
onClick={handleReauth}
disabled={reauthing}
>
{reauthing ? (
<Spinner size="xs" />
) : (
<RefreshCw className="size-3.5" />
)}
Re-authenticate
</Button>
</div>
)}
</div>
)}
</div>
{/* Indexing Options */}

View file

@ -169,10 +169,9 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
</p>
</div>
</div>
{/* Quick Index Button - only show for indexable connectors, but not for Google Drive (requires folder selection) */}
{/* Quick Index Button - shown for indexable connectors that have been configured */}
{connector.is_indexable &&
onQuickIndex &&
connector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && (
onQuickIndex && (
<Button
variant="secondary"
size="sm"

View file

@ -19,6 +19,9 @@ import { getConnectorDisplayName } from "../tabs/all-connectors-tab";
const REAUTH_ENDPOINTS: Partial<Record<string, string>> = {
[EnumConnectorName.LINEAR_CONNECTOR]: "/api/v1/auth/linear/connector/reauth",
[EnumConnectorName.NOTION_CONNECTOR]: "/api/v1/auth/notion/connector/reauth",
[EnumConnectorName.GOOGLE_DRIVE_CONNECTOR]: "/api/v1/auth/google/drive/connector/reauth",
[EnumConnectorName.GOOGLE_GMAIL_CONNECTOR]: "/api/v1/auth/google/gmail/connector/reauth",
[EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR]: "/api/v1/auth/google/calendar/connector/reauth",
};
interface ConnectorAccountsListViewProps {