refactor: streamline authentication handling and UI feedback for connectors showing re-authentication button in the config edit area as well

This commit is contained in:
Anish Sarkar 2026-03-20 19:36:55 +05:30
parent 283b4194cc
commit f938c4018e
3 changed files with 111 additions and 150 deletions

View file

@ -1,7 +1,5 @@
"use client"; "use client";
import { cn } from "@/lib/utils";
import { useAtomValue } from "jotai";
import { import {
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
@ -11,15 +9,11 @@ import {
FolderClosed, FolderClosed,
Image, Image,
Presentation, Presentation,
RefreshCw,
X, X,
} from "lucide-react"; } from "lucide-react";
import type { FC } from "react"; import type { FC } from "react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { ComposioDriveFolderTree } from "@/components/connectors/composio-drive-folder-tree"; import { ComposioDriveFolderTree } from "@/components/connectors/composio-drive-folder-tree";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
Select, Select,
@ -29,7 +23,6 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { authenticatedFetch } from "@/lib/auth-utils";
import type { ConnectorConfigProps } from "../index"; import type { ConnectorConfigProps } from "../index";
interface SelectedFolder { interface SelectedFolder {
@ -89,13 +82,10 @@ function getFileIconFromName(fileName: string, className: string = "size-3.5 shr
return <File className={`${className} text-gray-500`} />; return <File className={`${className} text-gray-500`} />;
} }
const COMPOSIO_REAUTH_ENDPOINT = "/api/v1/auth/composio/connector/reauth";
export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({ export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({
connector, connector,
onConfigChange, onConfigChange,
}) => { }) => {
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
const isIndexable = connector.config?.is_indexable as boolean; const isIndexable = connector.config?.is_indexable as boolean;
const existingFolders = const existingFolders =
@ -107,40 +97,10 @@ export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({
const [selectedFolders, setSelectedFolders] = useState<SelectedFolder[]>(existingFolders); const [selectedFolders, setSelectedFolders] = useState<SelectedFolder[]>(existingFolders);
const [selectedFiles, setSelectedFiles] = useState<SelectedFolder[]>(existingFiles); const [selectedFiles, setSelectedFiles] = useState<SelectedFolder[]>(existingFiles);
const [indexingOptions, setIndexingOptions] = useState<IndexingOptions>(existingIndexingOptions); const [indexingOptions, setIndexingOptions] = useState<IndexingOptions>(existingIndexingOptions);
const [reauthing, setReauthing] = useState(false);
const [authError, setAuthError] = useState(false); const [authError, setAuthError] = useState(false);
const isAuthExpired = connector.config?.auth_expired === true || authError; const isAuthExpired = connector.config?.auth_expired === true || authError;
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}${COMPOSIO_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;
} else if (data.success) {
toast.success("Authentication refreshed successfully.");
setAuthError(false);
}
} catch {
toast.error("Failed to initiate re-authentication.");
} finally {
setReauthing(false);
}
}, [searchSpaceId, connector.id]);
const handleAuthError = useCallback(() => { const handleAuthError = useCallback(() => {
setAuthError(true); setAuthError(true);
}, []); }, []);
@ -276,24 +236,13 @@ export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({
</div> </div>
)} )}
{isAuthExpired && ( {isAuthExpired && (
<div className="flex items-center gap-2"> <p className="text-xs text-amber-600 dark:text-amber-500">
<p className="text-xs text-amber-600 dark:text-amber-500"> Your Google Drive authentication has expired. Please re-authenticate using the button below.
Your Google Drive authentication has expired. </p>
</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}
>
<RefreshCw className={cn("size-3.5", reauthing && "animate-spin")} />
Re-authenticate
</Button>
</div>
)}
{isEditMode ? ( {isEditMode ? (
<div className="space-y-2"> <div className="space-y-2">
<button <button
type="button" type="button"

View file

@ -1,22 +1,16 @@
"use client"; "use client";
import { cn } from "@/lib/utils";
import { useAtomValue } from "jotai";
import { import {
File, File,
FileSpreadsheet, FileSpreadsheet,
FileText, FileText,
FolderClosed, FolderClosed,
Image, Image,
Presentation, Presentation,
RefreshCw,
X, X,
} from "lucide-react"; } from "lucide-react";
import type { FC } from "react"; import type { FC } from "react";
import { useCallback, useEffect, useState } 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 { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
@ -28,7 +22,6 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { authenticatedFetch } from "@/lib/auth-utils";
import { type PickerResult, useGooglePicker } from "@/hooks/use-google-picker"; import { type PickerResult, useGooglePicker } from "@/hooks/use-google-picker";
import type { ConnectorConfigProps } from "../index"; import type { ConnectorConfigProps } from "../index";
@ -89,10 +82,7 @@ function getFileIconFromName(fileName: string, className: string = "size-3.5 shr
return <File className={`${className} text-gray-500`} />; 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 }) => { export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfigChange }) => {
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
const existingFolders = (connector.config?.selected_folders as SelectedItem[] | undefined) || []; const existingFolders = (connector.config?.selected_folders as SelectedItem[] | undefined) || [];
const existingFiles = (connector.config?.selected_files as SelectedItem[] | undefined) || []; const existingFiles = (connector.config?.selected_files as SelectedItem[] | undefined) || [];
const existingIndexingOptions = const existingIndexingOptions =
@ -101,33 +91,6 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfi
const [selectedFolders, setSelectedFolders] = useState<SelectedItem[]>(existingFolders); const [selectedFolders, setSelectedFolders] = useState<SelectedItem[]>(existingFolders);
const [selectedFiles, setSelectedFiles] = useState<SelectedItem[]>(existingFiles); const [selectedFiles, setSelectedFiles] = useState<SelectedItem[]>(existingFiles);
const [indexingOptions, setIndexingOptions] = useState<IndexingOptions>(existingIndexingOptions); 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(() => { useEffect(() => {
const folders = (connector.config?.selected_folders as SelectedItem[] | undefined) || []; const folders = (connector.config?.selected_folders as SelectedItem[] | undefined) || [];
@ -268,40 +231,26 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfi
</div> </div>
)} )}
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
onClick={openPicker} onClick={openPicker}
disabled={pickerLoading || isAuthExpired} 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" 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 && <Spinner size="xs" className="mr-1.5" />} {pickerLoading && <Spinner size="xs" className="mr-1.5" />}
{totalSelected > 0 ? "Change Selection" : "Select from Google Drive"} {totalSelected > 0 ? "Change Selection" : "Select from Google Drive"}
</Button> </Button>
{(pickerError || isAuthExpired) && ( {pickerError && !isAuthExpired && (
<div className="flex flex-col gap-2"> <p className="text-xs text-destructive">{pickerError}</p>
{pickerError && !isAuthExpired && ( )}
<p className="text-xs text-destructive">{pickerError}</p>
)} {isAuthExpired && (
{isAuthExpired && ( <p className="text-xs text-amber-600 dark:text-amber-500">
<div className="flex items-center gap-2"> Your Google Drive authentication has expired. Please re-authenticate using the button below.
<p className="text-xs text-amber-600 dark:text-amber-500"> </p>
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}
>
<RefreshCw className={cn("size-3.5", reauthing && "animate-spin")} />
Re-authenticate
</Button>
</div>
)}
</div>
)}
</div> </div>
{/* Indexing Options */} {/* Indexing Options */}

View file

@ -1,11 +1,16 @@
"use client"; "use client";
import { useAtomValue } from "jotai";
import { ArrowLeft, Info, RefreshCw, Trash2 } from "lucide-react"; import { ArrowLeft, Info, RefreshCw, Trash2 } from "lucide-react";
import { type FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { type FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { authenticatedFetch } from "@/lib/auth-utils";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { DateRangeSelector } from "../../components/date-range-selector"; import { DateRangeSelector } from "../../components/date-range-selector";
import { PeriodicSyncConfig } from "../../components/periodic-sync-config"; import { PeriodicSyncConfig } from "../../components/periodic-sync-config";
@ -13,6 +18,17 @@ import { SummaryConfig } from "../../components/summary-config";
import { getConnectorDisplayName } from "../../tabs/all-connectors-tab"; import { getConnectorDisplayName } from "../../tabs/all-connectors-tab";
import { getConnectorConfigComponent } from "../index"; import { getConnectorConfigComponent } from "../index";
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",
[EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
[EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
[EnumConnectorName.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
};
interface ConnectorEditViewProps { interface ConnectorEditViewProps {
connector: SearchSourceConnector; connector: SearchSourceConnector;
startDate: Date | undefined; startDate: Date | undefined;
@ -60,6 +76,41 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
onConfigChange, onConfigChange,
onNameChange, onNameChange,
}) => { }) => {
const searchSpaceIdAtom = useAtomValue(activeSearchSpaceIdAtom);
const isAuthExpired = connector.config?.auth_expired === true;
const reauthEndpoint = REAUTH_ENDPOINTS[connector.connector_type];
const [reauthing, setReauthing] = useState(false);
const handleReauth = useCallback(async () => {
const spaceId = searchSpaceId ?? searchSpaceIdAtom;
if (!spaceId || !reauthEndpoint) return;
setReauthing(true);
try {
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
const url = new URL(`${backendUrl}${reauthEndpoint}`);
url.searchParams.set("connector_id", String(connector.id));
url.searchParams.set("space_id", String(spaceId));
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;
} else if (data.success) {
toast.success(data.message ?? "Authentication refreshed successfully.");
window.location.reload();
}
} catch {
toast.error("Failed to initiate re-authentication.");
} finally {
setReauthing(false);
}
}, [searchSpaceId, searchSpaceIdAtom, reauthEndpoint, connector.id]);
// Get connector-specific config component // Get connector-specific config component
const ConnectorConfigComponent = useMemo( const ConnectorConfigComponent = useMemo(
() => getConnectorConfigComponent(connector.connector_type), () => getConnectorConfigComponent(connector.connector_type),
@ -169,29 +220,30 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
</p> </p>
</div> </div>
</div> </div>
{/* Quick Index Button - shown for indexable connectors that have been configured */} {/* Quick Index Button - hidden when auth is expired */}
{connector.is_indexable && {connector.is_indexable &&
onQuickIndex && ( onQuickIndex &&
<Button !isAuthExpired && (
variant="secondary" <Button
size="sm" variant="secondary"
onClick={handleQuickIndex} size="sm"
disabled={isQuickIndexing || isIndexing || isSaving || isDisconnecting} onClick={handleQuickIndex}
className="text-xs sm:text-sm bg-slate-400/10 dark:bg-white/10 hover:bg-slate-400/20 dark:hover:bg-white/20 border-slate-400/20 dark:border-white/20 w-full sm:w-auto" disabled={isQuickIndexing || isIndexing || isSaving || isDisconnecting}
> className="text-xs sm:text-sm bg-slate-400/10 dark:bg-white/10 hover:bg-slate-400/20 dark:hover:bg-white/20 border-slate-400/20 dark:border-white/20 w-full sm:w-auto"
{isQuickIndexing || isIndexing ? ( >
<> {isQuickIndexing || isIndexing ? (
<RefreshCw className="mr-2 h-4 w-4 animate-spin" /> <>
Syncing <RefreshCw className="mr-2 h-4 w-4 animate-spin" />
</> Syncing
) : ( </>
<> ) : (
<RefreshCw className="mr-2 h-4 w-4" /> <>
Quick Index <RefreshCw className="mr-2 h-4 w-4" />
</> Quick Index
)} </>
</Button> )}
)} </Button>
)}
</div> </div>
</div> </div>
@ -349,6 +401,16 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
Disconnect Disconnect
</Button> </Button>
)} )}
{isAuthExpired && reauthEndpoint ? (
<Button
onClick={handleReauth}
disabled={reauthing || isDisconnecting}
className="text-xs sm:text-sm flex-1 sm:flex-initial h-12 sm:h-auto py-3 sm:py-2 bg-amber-600 hover:bg-amber-700 text-white"
>
<RefreshCw className={cn("size-3.5", reauthing && "animate-spin")} />
Re-authenticate
</Button>
) : (
<Button <Button
onClick={onSave} onClick={onSave}
disabled={isSaving || isDisconnecting} disabled={isSaving || isDisconnecting}
@ -363,6 +425,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
"Save Changes" "Save Changes"
)} )}
</Button> </Button>
)}
</div> </div>
</div> </div>
); );